[ 1 ] [ 2 ] [ 3 ] [ 4 ]
(The latest version of this document, along with source code, can be found at http://www.stevenblack.com)
Use a consistent interface wherever your software is to be hooked, controlled, or extended.
Flexible and extensible software is inherently more valuable than software that isn’t. But flexibility and extensibility don’t just happen – these qualities must be purposefully composed and exposed. Hook operations do that.
Hook: a message or a call, seeded throughout a system, to accommodate future implementations.
Hook Operation: the code that implements a hook.
Hook Operation Design Pattern: defines interfaces and mechanisms, and conveys the nuances of hook operations.
Addin: A configurable service invoked by a hook operation.
Hook operations do nothing by default; they are used by exception. A hook is a separate and distinct process, introduced as needed by hook operations that provide for such adaptation. The hook, which is a future operation introduced into the system, needs to be sequenced sensibly into the stream of native behavior, and absorbed quickly and predictably. Also, a hook operation is overhead, so it should have should have excellent no-op response.
Here’s some advice: systematically seed events and methods with hook operations, publish an interface to configure the hooks, and make tactical use of the addins invoked by the hooks for generalized and ad-hoc control and extensibility purposes.
Toolkits and frameworks are composed of general-purpose classes that are later adapted by others. Toolkits provide for code and component reuse, and frameworks usually cast a wider net and can provide architectural reuse. In both cases, flexibility and extensibility are virtues. How to best provide flexibility and extensibility?
In Visual FoxPro, instance programming is one way toolkits and frameworks are sometimes adapted. When you work in the VFP Screen Designer, or in non-class objects in the VFP Class Designer, you are instance programming; that is, you are adding user-code to object instances. This is usually fine and it works, but instance programming has the following disadvantages:
Subclassing is another common way toolkits and frameworks are meant to be adapted. Adaptation by subclassing, however, presents similar problems:
That last point is a killer. Subclassing constrains framework creators in future releases. Framework creators have no control of implementation-specific subclassing. This puts framework creators in a difficult position: on one hand they want to perpetually improve their classes, but of course each change could break somebody's code somewhere.
Hook operations can often be a nice alternative to instance programming and subclassing.
To illustrate a simple hook, consider a control with the click method in Listing 1. The built-in click method is preceded by the hook operation THIS.PreClickHook(). The PreClickHook() method is a good place for users to place custom processing in subclasses without polluting the built-in Click behavior.
Listing 1. A simple hook. In this example the user can either instance-program or subclass and place code in the PreClickHook() method. Later we'll see other alternatives (and better method naming) to implement hooks.
Listing 1 is only a little better than hookless alternatives because its mechanism relies on instance programming and subclassing, which we’ve shown is problematic. If we could implement hooks without subclassing, we would see the following benefits:
Listing 2 shows how we could modify Listing 1 to engineer hooks without instance programming or subclassing. Instead of delegating to a local method, the hook calls out to an object reference which can be dynamically configured.
Listing 2. Another simple hook. In this example the hook is implemented by reference to a separate hook object.
Object Hooks are hooks that are fulfilled by attaching behavior objects to instances. See figure 1. A Visual FoxPro example using object hooks is presented in the Examples section.
Figure 1: Object hooks illustrated, here for a single Click() method. In diagram A the instance is not hooked and functions as normal. In diagram B the instance is hooked by a hook object whose non-blank methods extend (or possibly override) the code in the instance.
Procedural Hooks are hooks that are fulfilled procedurally. This fulfillment may be a straight hard-coded call, but it can also be abstracted and metadata-driven. The sophistication level is up to you. See figure 2. A Visual FoxPro example using procedural hooks to manage addins is presented in the Examples section.
Figure 2: A metadata-based procedural hook illustrated. Here a call to a Hook Manager triggers a lookup which returns the name of a program to execute. Since hooks are used by exception, normally the lookup returns nothing.
Hooks are appropriate in places where you want a program to be adaptable. Where is that? Two forces determine where to locate hook operations: First, hook operations are most valuable at critical junctures in the program. Critical junctions are usually adjacent to significant state transformations, or proximate to control points in the system. In other words, exactly where you would want to hook the process. It’s too bad that critical junctures are difficult to identify, qualify, and document. After all, criticality depends entirely on individual implementations, especially for the framework re-user.
Secondly, and rather fortunately, usability suggests that hooks should be placed at predictable junctures. Predictable junctures are architecturally predictable places , easily conveyed patterns of positioning, places like at the start and end of methods, or at common junctures, like upon startup, and in some other events. Predictable junctures are easy to locate, and require much less documentation and conveyance effort than critical junctures.
If your code is brief, your predictable junctures are likely to be proximate to critical junctures (bonus!), and their predictability makes them very usable. How close are predictable junctures to critical ones? The briefer the method code, the likelier predictable junctures are proximate to critical junctures.
Bottom line: If implementers are not able to identify where hook operations are, they’ll have a harder time creating the adaptations they require. The main benefit of hooking at predictable junctures is the hooks are much easier to identify and document.
Besides, it’s just easier to place hook operations at predictable junctures and not fret about putting hooks sensibly at every critical juncture.
Do yourself a favor and don't hooking all your predictable junctures. Every method of every object doesn't need a hook, and since the hook calls involve processing, it's best to use some common sense. Some common events I hook include Click(), DblClick(), RightClick(), DragDrop(), GotFocus() and a few others. For obvious reasons, I don't recommend hooking quasi-continuous events such as MouseMove() and Paint().
[ 1 ] [ 2 ] [ 3 ] [ 4 ]