What exactly is a software hook? whatis.com provides the following definition:
In programming, a hook is a place and usually an interface provided in packaged code that allows a programmer to insert customized programming.
A hook may also be known as a plug-in, as indicated by the following definition from Wikipedia:
In computing, a plug-in (or plugin, add-in, addin, add-on, addon, or extension) is a software component that adds a specific feature to an existing computer program. When a program supports plug-ins, it enables customization.
In other words where you have a processing flow you provide the ability for a developer to interrupt that processing flow by saying:
When you get to this place you should execute this code
I recently came across an article on How CodeIgniter's Hook System Works, and I was fascinated to see how much effort it requires. After a quick search on the interweb thingy I found Best way to do PHP hooks and Best way to allow plugins for a PHP application which propose the use of the observer or mediator pattern, or having a hook object which contains the customisable methods, or even having a separate script with a name that indicates where and when it should be executed.
This is not how it's done in the RADICORE framework. As a long-time follower of the KISS principle I prefer to avoid unnecessary complications.
Before you can interrupt a processing flow you must have a processing flow to begin with, plus a method of interrupting that flow on demand. The RADICORE framework was designed specifically for the development of web-based database applications which utilise HTML forms at the front end and a relational database at the back end. It uses a combination of the 3-Tier Architecture and Model-View-Controller design pattern where the application logic is split into the following parts:
As each user transaction (use case) accesses one or more database tables in some way this led me to the following implementation decisions:
When a Controller calls one of the insertRecord(), getData(), updateRecord() or deleteRecord() methods you will see that it actually steps through a series of internal methods in a pre-defined sequence. Those with a "_dml_" prefix will generate and execute the relevant SQL query. Those with a "_cm_" prefix are customisable methods which provide the hooks. These methods are defined in the abstract table class but do not contain any code, which means that when called they do nothing at all.
All the developer has to do in order to have some custom code executed at a point in the processing flow is to identify which customisable method will be called at that point, then copy the empty method from the abstract class into the relevant concrete class, then fill it with code. At run-time the software will execute the method in the concrete class (if it exists), otherwise it will execute the empty method in the abstract class.
There may be occasions where you have a different user transaction which needs different code in a particular customisable method. Instead of populating the same method with different sets of code which are executed depending on different conditions, I prefer to isolate the different code in its own subclass with their own naming convention.
If you find pretty pictures more interesting than boring text, then perhaps this series of UML diagrams will help you understand the interactions between the controller, abstract class, concrete class (the table subclass) and DAO (the DML class).
My critics, of whom there are many, take great delight in telling me that virtually everything I do is wrong, that I am not implementing the "correct" design patterns, or not implementing them "properly". Phooey. My use of an abstract table class from which every concrete table class is derived, plus my use these hook methods, fits the description of the Template Method pattern which was described in the Gang of Four book as follows:
Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.
ApplicabilityThe Template Method pattern should be used
- to implement the invariant parts of an algorithm once and leave it up to subclasses to implement the behaviour that can vary.
- when common behaviour among the subclasses should be factored and localized in a common class to avoid code duplication.
- to control subclass extensions. You can define a template method that calls "hook" operations at specific points, thereby permitting extensions only at these points.
Template methods are a fundamental technique for code reuse. They are particularly important in class libraries because they are the means for factoring out common behaviour in library classes.
Template methods lead to an inverted control structure that's sometimes referred to as "the Hollywood Principle", that is "Don't call us, we'll call you". This refers to how a parent class calls the operations of a subclass and not the other way around.
The Template Methods call the following kinds of operations:
- concrete operations (either on the ConcreteClass or on client classes).
- concrete AbstractClass operations (i.e., operations that are generally useful to subclasses).
- primitive operations (i.e., abstract operations).
- hook operations, which provide default behaviour that subclasses can extend if necessary. A hook operation often does nothing by default.
Notice that it refers to concrete (non-abstract) methods in an AbstractClass which are generally useful to subclasses. This means that my use of a single abstract table class which provides all the possible operations which may be performed on an unspecified database table, then having this inherited by every concrete table class which changes unspecified to a specific database table, is not as crazy as some people would have you believe.
In its overview of the Template Method Pattern this wikipedia article contains the following:
This pattern has two main parts, and typically uses object-oriented programming:
At run-time, a concrete class is instantiated. A main method inherited from the base class is called, which then may call other methods defined by both the base class and subclasses. This performs the overall algorithm in the same steps every time, but the details of some steps depend on which subclass was instantiated.
- The "template method", generally implemented as a base class (possibly an abstract class), which contains shared code and parts of the overall algorithm which are invariant. The template ensures that the overarching algorithm is always followed. In this class, "variant" portions are given a default implementation, or none at all.
- Concrete implementations of the abstract class, which fill in the empty or "variant" parts of the "template" with specific algorithms that vary from implementation to implementation.
This pattern is an example of inversion of control because the high-level code no longer determines what algorithms to run; a lower-level algorithm is instead selected at run-time.
If you look at this article under the paragraph heading "Naming Conventions" you will see the following:
In order to identify the primitive methods is it better to use a specific naming convention. For example the prefix "do" can be used for primitive methods. In a similar way the customizations hooks can have prefixes like "pre" and "post".
This means that my naming convention of "_cm_pre_XXX" and "_cm_post_XXX" for these hook methods is not so stange after all.
If what I have done is implemented a pattern that was recognised and published by acknowledged experts as far back as 1995, then how can I possibly be wrong? No design pattern ever comes with a single definitive implementation, it is merely a description of a recurring problem which identifies the requirements of an implementation, and it is up to the individual programmer to write code which fulfills those requirements. I may not have implemented this pattern in exactly the same way as my critics, but that is irrelevant. Perhaps it is the efficacy of their implementation which should be questioned instead of mine.
As you should be able to see the system of hooks provided in the RADICORE framework does not require any configuration file or complicated processing like the methods provided in other frameworks. Instead it provides a well-documented processing flow with a series of pre-defined yet empty methods which can be used as hooks for custom code. All the developer has to do is fill in the blanks where necessary. This technique relies on nothing except inheritance. Could it be any simpler?
Here endeth the lesson. Don't applaud, just throw money.