Tuesday, April 13, 2010

Practical Php Patterns: Template Method

This post is part of the Practical Php Pattern series.

The pattern of today is the Template Method one. Template Method is an inheritance solution to the problem of hooking into steps of the execution of an algorithm.
The pattern is very simple: the Template Method on an AbstractClass defines the algorithm by composing small hook methods, which can be implemented or overridden by different ConcreteClass that inherit from AbstractClass. The AbstractClass may provide the hooks as abstract methods, or as concrete methods with default implementations; in PHP their visibility is usually protected so that they are visible only to the hierarchy internal code.
An example of Template Method where a standard hook is used is the Factory Method pattern: businessMethod() is a Template Method which composes factoryMethod() as an hook.
From the design and testability point of view, Template Method should be used sparingly, and mostly in high-level components which acts more as a declarative layer and are not the subject of extensive unit testing. This means they should not contain much real logic.
In fact, the testing of AbstractClass is usually non-standard for programmers that has made an habit of TDD, but it is indeed possible: a custom subclass of AbstractClass is built specifically for the test, or a mock is generated which overrides only the hook methods. Testing the single ConcreteClasses is instead difficult as in every test you will throw in also the business logic of AbstractClass, which was factored out specifically to avoid dealing with it.
You can lie to the production code by factoring out business logic with an extends keyword and hiding it under the carpet, but you can't lie to unit tests that exercise this production code. So if you find yourself struggling with testing ConcreteClass instances in isolation, you may want to refactor into a Strategy pattern or a similar composition solution built with Dependency Injection in mind. The particular pattern depends on the semantics of your object graph.

The code sample deals with multiple implementations of binary operations, which only define the sources of the operands and the business logic of the actual operation, sharing the wiring code.
<?php
/**
 * The AbstractClass.
 */
abstract class BinaryOperation
{
    /**
     * These are three hooks defined, which should
     * provide the two numbers which the operation is
     * applied to and its business logic.
     */
    protected abstract function _getFirstNumber();
    protected abstract function _getSecondNumber();
    protected abstract function _operator($a, $b);

    /**
     * This is the Template Method.
     * It uses all the three hooks, but a typical
     * Template Method can coexist with other ones, and
     * share hooks with them.
     * @return numeric
     */
    public function getOperationResult()
    {
        $a = $this->_getFirstNumber();
        $b = $this->_getSecondNumber();
        return $this->_operator($a, $b);
    }
}

/**
 * A ConcreteClass.
 */
class Sum extends BinaryOperation
{
    private $_a;
    private $_b;

    public function __construct($a = 0, $b = 0)
    {
        $this->_a = $a;
        $this->_b = $b;
    }

    protected function _getFirstNumber()
    {
        return $this->_a;
    }

    protected function _getSecondNumber()
    {
        return $this->_b;
    }

    protected function _operator($a, $b)
    {
        return $a + $b;
    }
}

/**
 * A ConcreteClass.
 */
class NonNegativeSubtraction extends BinaryOperation
{
    private $_a;
    private $_b;

    public function __construct($a = 0, $b = 0)
    {
        $this->_a = $a;
        $this->_b = $b;
    }

    protected function _getFirstNumber()
    {
        return $this->_a;
    }

    protected function _getSecondNumber()
    {
        return min($this->_a, $this->_b);
    }

    protected function _operator($a, $b)
    {
        return $a - $b;
    }

}

// Client code
$sum = new Sum(84, 56);
echo $sum->getOperationResult(), "\n";
$nonNegativeSubtraction = new NonNegativeSubtraction(9, 14);
echo $nonNegativeSubtraction->getOperationResult(), "\n";

No comments:

ShareThis