As you work on consecutive projects, you will eventually find yourself coding the same functionality repeatedly. When you reach this stage, you’re ready to explore the idea of code reuse. Most development teams already have code or component libraries that they draw on for each new project. But reusing code and components is just the first step. I’ll show you how to raise reuse to the next level: reuse of an entire application.
If you create software for a particular business function or a specific vertical market, you will normally find enough overlap between customer requirements to reuse core code and components???but enough differences that you must still write the overall application anew for each customer.
Unfortunately, rewriting increases design and development time and complicates maintenance. For example, if you have multiple versions, propagating a bug fix or product enhancement requires separate changes and testing for each customer. Reusable components help; but when the core logic of all the applications is the same, wouldn’t it be better if you could reuse that as well?
The answer is that you can, by developing a core product in such a way that you can hook in different customization layers, without having to change the core code.
What is a Hook?
A “hook” is a place where you hang your customization code: it is a method call from your core product to a custom component. Your core product makes these calls at any point where a customization layer (the “hook component”) might need to know what is happening, or might want to modify core product behavior.
Typically, a hook method call passes data relevant to the current context to the Hook component, which processes the call and returns status information.
When designing a core product and hook component, be sure you make the pair as loosely coupled as possible, so that future changes to the core product don’t require changes to hook components already deployed. You must also ensure that the hook component will behave correctly even after adding new hook calls from the core product that weren’t envisioned when the hook component was deployed.
Identifying Core Functionality
To build a core application with hooks, the critical first step is deciding what goes into core. Putting too little functionality into the core product means you’ll have to duplicate work across projects as each project re-implements the common functionality. Duplicate code defeats the purpose of having a core application; however, including functionality that is not generic to all customers is even worse; you’ll eventually have to modify the core product, and that will cause problems for all projects that use the core.
Migrating functionality into your core product at a later date is significantly less painful than removing functionality you should never have put in, so err on the conservative side. Functionality that might be suitable for core product can start life as a shared component and be migrated in when you are ready.
The key to identifying core application functionality is to define the program flows that are common to all customers. Let’s look at an example. Suppose you’ve developed three systems, based around moving Workitems through a Workflow. In all three systems, users can “Next Step” a Workitem, which moves it from one step to the next in the workflow. The details differ as follows:
- Customer 1: the system just moves the workitem on to the next step, without performing any other processing.
- Customer 2: the system logs the workitem and step details onto a legacy system, and writes a reference key from the legacy system into the workitem.
- Customer 3: the system displays a checklist for the user to update before moving the workitem to the next step. If the user fails to mark all the “required” as completed, the system cancels the Next Step operation.
In this case, the customization layers need the ability to:
- Use and update core product objects (in this case, the workitem)
- Interact with the user
- Change core product program flow (cancel the current operation)
- Link to other applications or components
These requirements are typical, so consider them for all functions in your core product.
Defining the Hook Interface
The parameters you pass into the hook component vary depending on context, but rather than maintaining a large number of Hook interfaces, it’s better to create a single generic Hook interface. Use Variants or a ParamArray to accommodate the different data types you will be passing in. The first parameter is always a Hook identifier. The first thing the Hook component does is interpret the identifier and invoke the appropriate function or component. The code in Listing 1 does this using a Select?Case statement. For those of you familiar with COM, this technique is similar to calling methods by dispatch.
'Constants would normally be defined in a module or class Const NEXTSTEP_START = "NS-START" Const NEXTSTEP_SELECTED = "NS-SELECTED" Const HOOK_OK = "OK" Const HOOK_CANCEL = "CANCEL" '... 'User just selected Next Step operation sHookResponse = mobjHookComponent.CallIn(NEXTSTEP_START, _ mobjCurrentItem, mobjCurrentUser, "") ... 'User just selected the Step to move the item to sHookResponse = mobjHookComponent.CallIn(NEXTSTEP_SELECTED, _ mobjCurrentItem, mobjCurrentUser, sSelectedStep) Hook component Code: 'Note: Hook constants file would need to be included Function CallIn(ByVal sHookId as String, vParam1 as Variant, _ vParam2 as Variant, vParam3 as Variant) as String Dim sReturnValue as String Dim objWorkitem as clsWorkitem Dim objUser as clsUser Dim sSelectedStep as String On Error Goto CallIn_Handler Select Case sHookId Case NEXTSTEP_START 'Processing to be done when NextStep is first invoked Set objWorkitem = vParam1 Set objUser = vParam2 Call WriteLog( sHookId, "Item ID: " & _ objWorkitem.WorkitemID & ", User ID: " & _ objUser.UserID ) sHookId = HOOK_OK Case NEXTSTEP_SELECTED Set objWorkitem = vParam1 Set objUser = vParam2 sSelectedStep = vParam3 Call WriteLog( sHookId, "Item ID: " & _ objWorkitem.WorkitemID & ", User ID: " & _ objUser.UserID & ", Step: " & vParam3 ) '...do whatever processing is required... sHookId = HOOK_OK Case else 'Unknown hook, possibly introduced to product ' after this component was written. 'Just allow it, but do nothing. Call WriteLog( sHookId, "Unrecognised Hook" ) sReturnValue = HOOK_OK End Select CallIn = sReturnValue Exit Function CallIn_Handler: 'Handle the error... End Function Sub WriteLog( sHookID as String, sText as String ) Dim sFullText as String sFullText = Time$ & " " & sHookID & " " & sText Debug.Print sFullText 'Log to file used mainly for testing 'and for diagnosing production systems If IsFileLoggingOn(sHookID) then 'Output log info to text file... End If End Sub
You must be able to release new versions of your core product without having to recompile Hook components already in the field, so the Hook component should ignore any unrecognized hook identifier and simply return a “success” code (see the case else clause in Listing 1). By doing this, hook components only need to implement functionality for pertinent hook calls. If you later add additional hook points to your core product, existing Hook components will accept the new hook identifiers without any change of behavior.
In the Next Step example, the core product might call the hook at several points in execution. For example, just after the user requests the Next Step operation, and again after the user selects the name of the Step. The parameters for these two examples would be:
Based on this single example, three Variants (plus the hook identifier) might be enough, but you should always include more Variant parameters than you think you’ll need so that you can avoid having to change the interface in the future. Listing 1 shows some sample code for a Core Product, a Hook Component, and an example hook call.
To implement Customer Three’s checklist functionality, you would add a checklist form to the Hook Component project, and invoke it using this code:
Case NEXTSTEP_SELECTED '...Cast variables and Call WriteLog()... 'Show the checklist frmChecklist.Show vbModal If frmCheckList.AllRequiredItemsTicked Then sReturnValue = HOOK_OK Else MsgBox "You have not ticked all the " & _ "required items. " & _ "Next Step cannot continue", vbExclamation Call WriteLog( sHookId, "Cancel sent back -- " & _ "all required items not ticked" ) sReturnValue = HOOK_CANCEL End If ' ... more code
Hook Component effects on the Core Product
The core product delegates some control to the hook component by passing in arguments that might be changed, and by responding appropriately to status values that the hook component returns.
Data Object References
When the Hook component receives a reference to a data object, the Core product needs to handle whatever changes the Hook component makes. If you don’t want the Hook component to modify an object parameter, pass in a throwaway object or lock all or a portion of the object with a property setting before calling the method. If you allow changes, the object can validate them on the fly, or it can log the changes, letting the core product validate them when the method returns. For example, Customer Two’s hook needs to update the workitem object argument with a new reference key:
Case NEXTSTEP_SELECTED '...cast variables and call WriteLog()... sRefKey = GetMainframeKey(objWorkitem.WorkitemID) objWorkitem.Reference = sRefKey sReturnValue = HOOK_OK
Then the core product can query the returned workitem object:
mobjCurrentItem.ResetChangedFlag sHookResponse = _ mobjHookComponent.CallIn(NEXTSTEP_SELECTED, _ mobjCurrentUser, mobjCurrentItem, sSelectedStep) If mobjCurrentItem.Changed then 'Take appropriate action, such as validating 'and/or saving the changes... End If
The Hook component can pass back values that can influence Core Product logic. Implement this in a very finite way, for example, by specifying a set of flags that the Hook component can pass back (e.g. Cancel current operation, Display standard dialog, etc), otherwise you risk tight coupling between core and hook component. Having a predefined set of flags that does not change from customer to customer means you can write the core code to handle each of the possible return values, without knowing which of the responses any given Hook component core will return.
In a typical Windows forms-based application, the Hook component can launch its own VB form and interact with the user if required.
For Web applications, the core product and hook component will be running on a server, so a VB form is not an option. Instead, you might use one or more of the following:
- Return a URL to the core product, which launches a new browser window. All subsequent requests from that window could be handled by non-core components. Because this window is not modal, be aware that the user can toggle between your core product screen and the Hook window.
- A variation on the first option is to temporarily replace the core product’s browser with the contents of the hook’s URL. This is effectively a modal interface. Requests from this screen are handled by custom components. When control is returned to the core product, it restores its screen.
- Return HTML or XML back to the core product, which incorporates this directly onto a page it is building. This allows Hook interface changes to blend in better with the core product. However, the core product will need to divert requests from the “hook” portion of the screen to an appropriate custom component.
Binary compatibility ensures that your core product and your hooks always use the same interface. Note that typically you build the core product in the office on a dedicated build PC, while project teams often build hook components on-site, using whatever PC is available. This can sometimes cause hard to fix problems with Binary compatibility. If you run into this, try to synchronize the two build environments, including service pack numbers. If that doesn’t work or is not possible, drop binary compatibility and use the slower late binding as a last resort
A Hook component is a useful debugging and testing aid. As shown, your basic stub hook component should log all calls and parameters. Typically the responses to each hook id are custom-coded for each customer. However it is worthwhile to code a configurable hook component, just for internal use. This component’s behavior isn’t hard-coded; instead, its responses are controlled by a configuration file such as that in Listing 2 (which uses XML because it contains hierarchical information). Once you’ve coded a configurable hook component, you shouldn’t have to change it for testing different situations; you would just need to modify the configuration file. This makes it handy for developers, and particularly useful for testers, who typically should not be modifying code.
At a minimum, your configurable hook should allow you to specify a different return value for each hook identifier passed in. My example has refined this a bit by allowing behavior to be defined on a per-user basis. I’ve also added settings for pausing and for display a message box???these are useful for simulating delays that a real hook component might experience when calling out to a legacy system. Finally, I’ve included a setting that tells the hook component to change one of the parameters passed in (see Listing 2).
Keep in mind that you must deliver at least your stub hook component with your core product even if it has no functionality other than logging, because your core product needs to be able to call out to it.
The concept of Hooks is roughly based on a design pattern called “Template”. If you haven’t heard of design patterns before, get the classic Design Patterns, by Gamma, Helm, Johnson and Vlissides (often referred to as the “Gang of Four”). They assume you are using a language with full inheritance, although it is not necessary. With VB.NET we have that, but in many cases you can get the same effect in VB 6 with a little effort.
If you find their book a bit hard going, start instead with Visual Basic Design Patterns, by Stamatakis (Microsoft Press)???it doesn’t cover nearly as many patterns, but it is a good start for VB developers.
About the Author
Mike Cahn?is the technical partner at Indytech.ie, an Irish Web design house that specialises in Drupal websites. For the past 30 years, he has alternated between learning new technologies and communicating their essential concepts, through training, consultancy, mentoring and writing. You can reach him at [email protected].