by Unlicensed user
In the introductory first article to this in this series I said that most Clarion developers use embed points the wrong way, and by doing so they make their applications more difficult to maintain, test, debug and document. Almost every Clarion developer has done that; I've done it too. In these articles I intend to show how you can improve your code base by taking the majority of that code out of the embed points.
I'll be working through a number couple of examples, including the Invoice app which ships with Clarion. But in this article I'll focus on the TXA embed parser example I introduced in Part 1.
In any analysis of embed code , and how it might be better deployed, the first thing you need is a convenient way to look at just your embeds. That's not so easy , because, embeds being embeds, they're sprinkled throughout your application.
...
Unfortunately, that code isn't very pretty. Most of it is contained in just a couple of embed points. In the TakeAccepted method's data section there are some declarations:
Code Block |
---|
x long
vars group,pre()
procname string(200)
procFromABC string(60)
procCategory string(60)
embedname string(60)
embedPriority long
embedParam1 string(200)
embedParam2 string(200)
embedParam3 string(200)
whenLevel byte
end
dumptrace byte(0)
LastProcName like(procname)
lastEmbedName string(500)
currembedname string(500) |
And then a little later on in TakeAccepted
, at an embed that's called when the user presses the Import button, the TXA gets parsed. That code loops through the records in a previously created queue of TXA files (txaq
):
Code Block |
---|
?progressvar{prop:rangehigh} = records(txaq)
setcursor(cursor:wait)
loop x = 1 to records(txaq)
get(txaq,x)
?progressVar{prop:progress} = x-1
clear(ema:record)
EMA:TXA = txaq.name
Access:EmbedApp.Insert()
EmbedApp{prop:sql} = 'select last_insert_id()'
next(EmbedApp)
! Add the queue header record
access:TextFile.Close()
GLO:TextFileName = txaq.name
access:TextFile.Open()
Access:TextFile.UseFile()
set(TextFile)
ProcName = ''
state = 0
lineNo = 0
clear(procname)
clear(lastprocname)
clear(lastembedname)
clear(currembedname)
LOOP
next(TextFile)
if errorcode() then break.
dumptrace = false
lineNo += 1
CASE state
OF 0 ! search for the start of a module or procedure, or an embed
if sub(txt:rec,1,11) = '[PROCEDURE]'
clear(vars)
state = 10
elsif sub(txt:rec,1,8) = '[MODULE]'
clear(vars)
procName = '[MODULE]'
elsif sub(txt:rec,1,7) = 'EMBED %'
embedName = sub(txt:rec,7,len(txt:rec))
state = 30
elsif sub(txt:rec,1,8) = '[SOURCE]'
state = 50
end
OF 10 ! get procedure name details
if sub(txt:rec,1,4) = 'NAME'
procName = sub(txt:rec,6,len(txt:rec))
state = 11
end
do CheckForMissedEmbed
OF 11
if sub(txt:rec,1,8) = 'FROM ABC'
procFromABC = sub(txt:rec,10,len(txt:rec))
state = 12
end
do CheckForMissedEmbed
OF 12
if sub(txt:rec,1,8) = 'CATEGORY'
procCategory = sub(txt:rec,11,len(clip(txt:rec))-11)
end
state = 0
do CheckForMissedEmbed
of 30 ! Look for a first embed parameter
if sub(txt:rec,1,11) = '[INSTANCES]'
state = 41
elsif sub(txt:rec,1,8) = '[SOURCE]'
state = 50
end
of 41 ! Get first parameter
if sub(txt:rec,1,6) = 'WHEN '''
embedParam1 = sub(txt:rec,7,len(clip(txt:rec))-7)
WhenLevel = 1
!db.out('whenlevel=' & whenlevel)
end
state = 42
do CheckForMissedEmbed
of 42 ! Look for a second embed parameter
if sub(txt:rec,1,11) = '[INSTANCES]'
state = 43
elsif sub(txt:rec,1,8) = '[SOURCE]'
state = 50
end
of 43 ! Get second parameter
if sub(txt:rec,1,6) = 'WHEN '''
embedParam2 = sub(txt:rec,7,len(clip(txt:rec))-7)
WhenLevel = 2
end
state = 44
do CheckForMissedEmbed
of 44 ! Look for a third embed parameter
if sub(txt:rec,1,11) = '[INSTANCES]'
state = 45
elsif sub(txt:rec,1,8) = '[SOURCE]'
state = 50
!db.out('found PRIORITY')
end
of 45 ! Get third parameter
if sub(txt:rec,1,6) = 'WHEN '''
embedParam3 = sub(txt:rec,7,len(clip(txt:rec))-7)
WhenLevel = 3
!db.out('whenlevel=' & whenlevel)
end
state = 50
do CheckForMissedEmbed
of 50 ! look for the priority
if sub(txt:rec,1,8) = 'PRIORITY'
embedPriority = sub(txt:rec,10,len(txt:rec))
if lastprocname <> procname
! insert new EmbedProc record
clear(EMP:record)
EMP:Proc = procname
EMP:ProcFromABC = ProcFromABC
EMP:ProcCategory = ProcCategory
EMP:EmbedAppID = EMA:EmbedAppID
Access:EmbedProc.Insert()
EmbedProc{prop:sql} = 'select last_insert_id()'
next(EmbedProc)
lastprocname = procname
end
! Add the embed record
currEmbedName = clip(embedName) & clip(embedparam1) |
& clip(embedparam2) & clip(embedparam3) & embedpriority
if currEmbedName <> lastEmbedName
lastEmbedName = currEmbedName
EMB:EmbedProcID = EMP:EmbedProcID
EMB:Embed = EmbedName
EMB:Param1 = EmbedParam1
EMB:Param2 = EmbedParam2
EMB:Param3 = EmbedParam3
EMB:Priority = embedpriority
access:Embed.Insert()
end
state = 51
end
of 51
state = 60
do CheckForMissedEmbed
OF 60 ! capturing embed
! Quit when [END] encountered
if sub(txt:rec,1,1) = '['
if sub(txt:rec,1,5) = '[END]'
case WhenLevel
of 3
WhenLevel = 2
embedParam3 = ''
of 2
WhenLevel = 1
embedParam2 = ''
of 1
WhenLevel = 0
embedParam1 = ''
end
state = 0
elsif sub(txt:rec,1,8) = '[SOURCE]'
! look for another embed under this [EMBED] point
state = 50
else
! could be we're done
state = 0
end
elsif sub(txt:rec,1,6) = 'WHEN '''
case WhenLevel
of 0
! get the first param
embedParam1 = sub(txt:rec,7,len(clip(txt:rec))-7)
WhenLevel = 1
state = 42
of 1
! get the second param
embedParam2 = sub(txt:rec,7,len(clip(txt:rec))-7)
WhenLevel = 2
state = 50
of 2
state = 50
end
else
! write embed buffer
end
else
do CheckForMissedEmbed
END
END
Access:TextFile.Close()
end
?progressVar{prop:progress} = records(txaq)
setcursor() |
...
In the downloadable C7 zip , have a look at the ImportTXAs procedure in Embeds.app for the complete source (C6 version available on request).
What I had in mind for my new utility was something more along the lines of Figure 1.these lines:
Figure 1. A utility to display and write the embed list
My original parser's functionality was overkill for this new app; I really didn't need to build up an elaborate database of applications, procedures, and embed points. I just needed a list of embed points. But obviously I needed all of the parsing capabilities, gnarly though the code might be.
Unfortunately, there wasn't any way to use my original code unchanged, primarily because that code didn't actually extract the embedded source from the TXA . There - there was no need to capture the actual embed code since I was just logging embed usage. And I was motivated not to store the embed code: I had asked for TXA submissions which could contain sensitive information, and I didn't want to accidentally expose anyone's embed code to public view.
So what are the were my options? A few come came to mind:
- Just cut and paste the source. This is what a lot of us do, and it has the obvious drawbacks of creating multiple versions of the code to maintain. If I find a bug in my parsing code, I'll need to hunt down every place I've pasted a copy and make the change there.
- Put my original procedure in a DLL and call it as needed. In this case my DLL also presents a user interface, so that . That would result in some UI clunkiness in the app I envisioned in Figure 1, which doesn't need to call yet another window just to do the parsing.
- Put the common source in source files and INCLUDE them. I could even include just portions of the files using labels. The drawback here is that there's no way to know how the various sections of source code might be used, or how any bug fixes to that source might cause unexpected bugs. Using INCLUDE statements this way results in an almost complete loss of control over the source code.
- Create a template containing the source code. This certainly helps keep the code in one place, but it has a lot of negatives when it comes to maintaining and testing that code since you have to put the template in an app and generate the code, and then you have to port any changes back to the template.
And there are were other problems. Because my original app was tied to a particular data store (a PostgreSQL database), any re-use of that code would have to know the table definitions. Since Clarion only supports one dictionary per app, any apps that used this procedure would either need to use the dictionary or import the tables from that dictionary.
...
So what's the answer? If it's not a template, and not a multi-purpose generated procedure and not INCLUDEd source, what's left? Procedures and classes, that's what. But not just any procedures or classes. You want I wanted to write code that has had as few dependencies as possible.
Some of the dependencies you want I wanted to avoid:
- Files/tables - don't not tie your my code to a specific database
- Windows/controls - don't not tie your my code to a specific user interface element
- Other code - keep calls to other procedures/classes to a minimum
The code you do write in embed points can wire up the dependencies; keep your procedures and classes as clean as possible.
But that raises another question: So when should you use a class, and when a procedure?
In almost all cases a class is preferable to a procedure, in the same way that a procedure is almost always preferable to spaghetti code. A procedure presents a single point of entry and a single result. That's not to say you can't return multiple values from a procedure - you clearly can, as Steve Parker recently has showed. But procedures don't have the flexibility of classes.
...
I'm not going to go into all the details of how to create a class; I'll cover that in following articles. For now , just keep your eye on the transformation of the embedded code into a class, and don't worry excessively (yet) about exactly how it was done.
...
The parser's job will be to populate these queues , so that I end up with a TxaProcedureQueue containing one or more records. Each of those procedure records has one or more TxaEmbedQueue records, each of which has one or more TxaEmbedLineQueue records for each line in a given embed point. With that queue structure in hand I can easily update a database or create a text file, as I see fit.
At first blush this looks like an ideal task for a procedure. Pass in the name of the TXA and an empty queue and get back a filled queue: presto! But here's why I think the procedural solution is almost never hardly ever a good solution: there's almost always some new functionality you can add which doesn't fit into the existing procedure.
...