|
Looking back at the FSM mechanism we've built so far, we find a couple of things to be uneasy about. One is the requirement for an external DLL, or a hack using the CallWindowProc
API, but the most annoying restriction is that the action routines need to be coded in a BAS file. This is a serious barrier to the kind of encapsulation we should be consistently striving for in the new object-oriented Visual Basic philosophy.
Even our final 'real world' FSM example swept the encapsulation problem under the carpet by declaring that 'Other functions would be
implemented with their own FSMs, which is straightforward because the FSM was built as a class'. The unspoken implication here is that for each function we hang off the menu bar, we need to collect a number of forms
and a BAS file together as a unit. Ideally we'd confine the entire FSM definition to the parent form in such a structure, but the mechanism doesn't allow that. We can also conceive of other classes of application where
this kind of encapsulation would result in even more cohesive code - for example, where an FSM is used to control the enabling and disabling of a form's controls as we manipulate it.
There is a solution, which uses the CallByName function introduced in Visual Basic 6. To see why this works we need to look back at the foundation of our FSM mechanism, which is the idea of data-driven code
. Specifically, the code is built from a set of pre-defined chunks, but the order in which these chunks are invoked is defined by a data structure that we construct at run time (look back at the CFSM.TableEntry method
if you need to convince yourself of this). The two problems we encountered with implementing data-driven code in VB were the need to provide a CallMe
function and the fact that such a function could only work with addresses of routines that originated in BAS files. CallByName changes that by allowing us to invoke methods
of an object variable by supplying the method names as strings. This is a dynamic mechanism, so we don't need to know what the method names are at compile time. All we need to do is replace our table of function pointers with a table of strings, and we can build a much more object-oriented FSM class. By using CallByName we eliminate both problems with the
AddressOf mechanism at a stroke, although we pay a price in reduced efficiency because calls routed through CallByName are late bound (or perhaps late-late bound). The new mechanism prompts
two modifications to the FSM mechanism. The first is simply to modify our action table to take strings, and to change the guts of CFSM.Dispatcher to match:
nNewState = m_aatFSMTable(m_nCurrentState, lEvent).nNextState CallByName m_oActionServer, _ m_aatFSMTable(m_nCurrentState, lEvent).pcbAction, _
VbMethod The first line deals with the state table and remains as it was, but the second line changes to replace CallMe with CallByName
. You can look up the details of CallByName, but there are two important things to note about this new code. First, although it isn't obvious here, we can now legitimately use parameterless procedures for our action
routines (in fact, since CallByName allows an arbitrary set of parameters we could get more sophisticated by allowing configurable parameter lists). The second thing to note is the m_oActionServer
variable, which refers to an object that carries all of our action routines as methods. At first sight it may seem we haven't moved much closer to our goal of encapsulation, since we still need an extra file (albeit a class module) to host the actions. However, if we accept that we're usually going to host an FSM variable in a class or a form anyway, it only takes a short stretch to see that we can now include the actions
within that host module as public methods. To accommodate this new feature we need an additonal configuration method on the CFSM class: RegisterActionServer. This fits in neatly with
RegisterStates and RegisterEvents, and all it does is to save an object reference locally: Public Sub RegisterActionServer(ByVal oServer As Object)
Set m_oActionServer = oServer End Sub The FSM definition code in the host module now looks like this (using the Comment Stripper example again):
m_oFSM.RegisterActionServer Me <--- the important bit
m_oFSM.RegisterStates "OUTSIDE", "STARTING", "INSIDE", "ENDING"
m_oFSM.RegisterEvents "SLASH", "STAR", "OTHER"
m_oFSM.TableEntry "OUTSIDE", "SLASH", "STARTING", "OutsideSlash"
m_oFSM.TableEntry "OUTSIDE", "STAR", "OUTSIDE", "OutsideStar" m_oFSM.TableEntry "OUTSIDE", "OTHER", "OUTSIDE", "OutsideOther"
m_oFSM.TableEntry "STARTING", "SLASH", "STARTING", "StartingSlash" m_oFSM.TableEntry "STARTING", "STAR", "INSIDE", "StartingStar"
m_oFSM.TableEntry "STARTING", "OTHER", "OUTSIDE", "StartingOther"
m_oFSM.TableEntry "INSIDE", "SLASH", "INSIDE", "InsideSlash"
m_oFSM.TableEntry "INSIDE", "STAR", "ENDING", "InsideStar" m_oFSM.TableEntry "INSIDE", "OTHER", "INSIDE", "InsideOther"
m_oFSM.TableEntry "ENDING", "SLASH", "OUTSIDE", "EndingSlash" m_oFSM.TableEntry "ENDING", "STAR", "ENDING", "EndingStar"
m_oFSM.TableEntry "ENDING", "OTHER", "INSIDE", "EndingOther" (Notice that the last parameter to the TableEntry method is now a string, so there's no AddressOf
operator.) This mechanism still isn't perfect, since we have to expose the action routines as part of the host interface, but it's a lot closer to true encapsulation.
If you want to examine the code in detail, here's the complete source code for the Comment Stripper program using the new mechanism. |
|