Happy Hooking: Designing Software for Extensibility and Customization

Author: Ed Leafe
Websites: http://dabodev.com
    http://leafe.com
Email: ed at leafe.com

Introduction

Most code that is written doesn't exist in a vacuum: it is shared among developers, and can be modified for additional uses that were never envisioned when initially written. One of the biggest promises of Object Oriented design was the productivity gains that would be achieved through re-use of objects, but that hasn't really come to pass as many thought. Part of that is the "not invented here" syndrome: the distrust of any code that someone else wrote. But a large part of this is because it's simply too difficult to customize the reused code to perform the way you need it to.

As one of the main authors of Dabo, a framework for developing desktop database applications, not only do I have to anticipate that others may need to extend my code, I have to expect that the framework code is going to be subclassed and customized. After all, that's the whole point of a framework! If the Dabo classes are difficult to customize to a developer's needs, nobody would bother using it. As a result, Dabo uses the techniques discussed in this presentation extensively.

Who should care

Actually, nobody who only writes finished applications that will never be modified, or never be used by anyone else, needs to care much about this issue. After all, if the code you write is never going to be used by anyone else, you don't have to worry about making it easy for them to extend. But then again, how many times have you started a simple project, and have it turn into something much more involved, as others have thought of novel ways to take what you wrote and add to it? It's therefore never a good idea to assume that your code is a dead end. And certainly, if you write high-level code that is meant to be subclassed and specialized by others, these issues are especially critical to making your code robust and easy to customize.

Basics of Inheritance

In most object-oriented languages, behaviors are coded in methods of a class. When a subclass is defined, it automatically inherits the behaviors of its superclass. But this isn't very interesting - we need to change the behavior in a subclass to make it work differently than its superclass.

If a method is defined in a class, and a subclass doesn't re-define that method, the original behavior is preserved. But as soon as we define a method of the same name in a subclass, we are overriding the inherited behavior. If we want to completely replace that behavior, fine - that's exactly what happens. But most of the time in a good class design, we don't want to get rid of the inherited behavior; instead, we want to augment it by adding code to run in addition to the default code. There is no built-in way of doing this in Python (or most other OO languages); you have to explicitly call the superclass method every time. Failure to do that results in overriding, not augmenting, behavior, and can result in unpredictable and hard-to-find errors.

There are 3 places where you may need to insert new code in your subclass:
  1. Before the default behavior occurs
  2. After the default behavior occurs
  3. Somewhere in the middle of the default behavior.

The first two are pretty simple to handle. If you want to run your custom code before the default behavior, you would write your code like this:

class mySubclass(mySuperClass):
    def myMethod(self):
        """ Perform custom behavior first, then
        run the inherited behavior.
        """
        self.someCustomMethod()
        super(mySubClass, self).myMethod()

Similarly, adding your custom behavior after the default behavior simply requires calling the superclass behavior first:

class mySubclass(mySuperClass):
    def myMethod(self):
        """ Perform custom behavior after 
        the inherited behavior has run.
        """
        super(mySubClass, self).myMethod()
        self.someCustomMethod()

But what about the the third case? How do you handle that? The answer is simple: you can't if you haven't planned for it. That is, of course, if you don't count "copy/paste inheritance", a 'technique' used by many Visual Basic developers: it consists of copying the superclass code and pasting it into your subclass, and then proceeding to edit the pasted code! Needless to say, working this way is not recommended.

A better solution for dealing with these situations is the use of Hook Methods (explained below), which provide handy 'hooks' where developers can hang their custom code.

Hook methods

A hook method is an empty method that is placed in code with the intent of allowing easy addition of code in strategic places later on. Because it doesn't contain any code, it is never necessary to call up to the superclass when a subclass adds behavior to these hook methods. They are placed at points in the code where someone using your class will typically need to add or modify behavior. By careful addition of hook methods in your code, you can make your code much easier for developers to customize for their needs. And since hook methods by default do nothing, there is very little cost in adding them to your code.

The danger in overriding methods is in forgetting to call the inherited behavior, resulting in a subclass that doesn't do what you expected it to do. With hook methods, there is no behavior to inherit, and thus nothing to worry about; just write your code in any of the hook methods provided to you, and you're done.

Placing Hooks

How do you determine where to put hooks? Well, there is no formula other than experience. When creating a new framework, you will frequently have to go back to the original class code and add additional hooks as you learn more about how your class is used in practice.

Typically, though, there are some obvious choices. In Dabo, the primary place to provide hooks is in those methods that query, change or create data, as well as at the points where record navigation occurs. There are before- and after- hooks for each of these.

Another critical situation that hooks can handle well is the need to conditionally execute a method. In Dabo, we have adopted the standard that if a before- hook returns a non-empty value (usually a string), that indicates that the method should not execute. In those cases, an exception is raised, which is then caught by the UI tier, and an appropriate message is displayed to the user. This convention is used throughout the Dabo business object, making it easy to customize your app to enforce your security requirements, for example, and prevent users with insufficient authorization from being able to change data.

As an example, here is the delete() method of the business object class (somewhat simplified):

 def delete(self, startTransaction=False):
     """ Delete the current row of the data set.
     """
     errMsg = self.beforeDelete()
     if not errMsg:
         errMsg = self.beforePointerMove()
     if errMsg:
         raise dException.dException, errMsg

     cursor = self._getCurrentCursor()
     [some stuff removed]
     # This actually deletes the record from the database
        cursor.delete()
     if self.RowCount == 0:
         # Hook method for handling the deletion of the last record in the cursor.
            self.onDeleteLastRecord()

     [some more stuff removed for updating child bizobjs]
     # These methods provide post-processing hooks.
        self.afterPointerMove()
     self.afterDelete()

 def beforePointerMove(self): pass
 def beforeDelete(self): pass
 def onDeleteLastRecord(self): pass
 def afterPointerMove(self): pass
 def afterDelete(self): pass

Note that there are two pre-behavior hooks: beforeDelete() and beforePointerMove(). The former only gets called from this delete() process; the latter is called before any process that could move the record pointer. This way you can write one pre-hook method that will get fired before every operation that could potentially change the record pointer.

In a framework I wrote before Dabo, there was just the simple beforeDelete(), beforeFirst(), etc., methods written as pre-hooks. A few developers complained that they were writing the same code in each of these to handle some tasks that were needed in their apps. So I added the generic beforePointerMove() hook in addition to the method-specific pre-hooks in order to handle this case. When I was creating the Dabo classes, I carried over that experience and designed them in from the beginning. Listening to the people who are using your code is the best way to find out if you have designed it well, and where it could use some improvement.

There are also two post-behavior hooks; as with the pre-hooks, the afterPointerMove() hook provides a way to have a single method executed in every method that moves the record pointer position. There is also the afterDelete() hook for delete-specific modifications. The post-hook methods cannot change or prevent the default behavior. They exist to allow the developer to tack on additional behaviors; some examples might be logging or conditional email notification when the data changes in a significant way.

There is one more hook method call: onDeleteLastRecord(). Right after the record is deleted, developers may need to handle this before the child object processing is carried out. In the original version of the framework that Dabo was based on, there was no such hook, and several developers complained about the need to handle this case. By adding this hook they could now gracefully handle this case without breaking the rest of their code. Again, it was feedback from the people using the code that provided the impetus to add this hook.

Finally, note the actual hook method definitions. All they consist of is the pass keyword; they don't do anything. This is what makes them useful as hooks: developers are free to ignore the ones that they don't need, yet can overwrite the ones they do without fear of losing some inherited behavior. This makes customizing subclasses much simpler and less prone to error.

Sample Hook Methods

OK, fine, this all sounds great, but how do I actually use these hook methods? Here are a few ideas:

Here's a sample of what the code for the last case would look like:

def beforeNew(self):
    # Assume that the 'isAuthorized' method contains
    # the logic that determines what action the user
    # may take.
    if not self.isUserAuthorized("new"):
        return "You are not authorized to create new Account records"

Pretty simple, huh? No need to call the superclass version of the method; just add the code you need, and you're done! Note that this code takes advantage of the convention in Dabo that returning a non-empty string will raise an exception, which will prevent the new() method from executing. The exception contains the returned string, and the UI framework will catch that exception and display the message to the user. This demonstrates the power of designing a framework for extensibility: a couple of lines of code is all it takes to achieve some powerful and impressive results.

Conclusion

Programming code is never static; at least not the interesting stuff. Good class design will usually require that subclasses extend the original behavior. By planning for such extension, you can make your code more easily extended, and the resulting product will be much more robust. Hook methods are a powerful technique for building extensibility into the original design of your code.

 


Creative Commons License
This work is licensed under a Creative Commons License.