Sunday, May 17, 2015

Economy of Means: On the Elimination of Inheritance (2/6)

The path away from inheritance

This section shows more complex designs that are based on inheritance. In each case, I explain how it is possible to avoid inheritance.

Default behaviour

Writing some generic engine that drives plugins is a powerful way to factor code and speed up development. Consider an hypothetical case where plugins would have to comply with the following interface:
interface IEngineEventsListener {
    Statistics processStartup(Car);
    void processAcceleration(Car, int);
    void processDecelaration(Car, int);
    void processStop(Car);
    void processCrash(Car, Crash);
    // etc...
    void processEvent20(Driver driver);
}
As you can see, the interface is quite rich. And it can be pretty tedious to provide a full plugin implementation. Now suppose all actions are optional. So it has been decided to add a class which implements standard default behaviours for every method:
class DefaultEngineEventsListener
                    implements IEngineEventsListener {
    public Statistics processStartup(CarData) {
        /* return some default value */
    }
    public void processAcceleration(CarData, int) {
        /* do nothing */
    }
    // etc...
}
Plugin developers can now inherit from this class and override only the methods for which a specific behaviour is required.
To me, this pattern is acceptable only as long as the default behaviour is empty and there are no method with a return value. Otherwise, overridden methods and default values are a form of dead code. This goes against one of the minimalistic rules:
Never ship code which will not be executed in the production environment.
In practice, actual designs generally tend to belong to one of these two categories:
  • most plugins implement only one method, in which case methods can be registered independently,
  • most plugins implement most methods, in which case cost of implementing the whole interface every time is acceptable.
Additionally, the variety of methods can be reduced by having their signatures converge. Taking this idea to the extreme, one could substitute all methods by only one by merging all data under a common interface (the union type as described previously in section A matter of choice). But then, of course, the burden of type dispatch would shift onto the plugin implementer.
As a last note, less strongly typed programming languages such as python or javascript simply allow partial implementations. Also, the syntax to register individual methods (or closures) tends to be lighter.
 

 

Antagonism between class hierarchy and methods call chain

Let us do some cooking. A WitchSoup is a Recipe. Base class Recipe is abstract since, during initialization, it calls abstract method getAdditionalTasks:
abstract class Recipe {

    private List<Task> _tasks;

    public Recipe() {
        this._tasks = new ArrayList<Task>();
        this._tasks.add(new PrepareUstensils());
        this._tasks.addAll(getAdditionalTasks());
        this._tasks.add(new CleanKitchen());
    }

    abstract List<Task> getAdditionalTasks();

    public execute() {
        for (Task task in this._tasks) {
            task.executes();
        }
    }
}

class WitchSoup extends Recipe {

    public WitchSoup() {
        super();
    }

    List<Task> getAdditionalTasks() {
        List<Task> additionalTasks = new ArrayList<Task>();
        additionalTasks.add(new MeltButter());
        additionalTasks.add(new MashBeans());        
        additionalTasks.add(new Boil());
        return additionalTasks;
    }
}
For some of you, this code may seem awkward. Indeed, it is wrong in more than one way. First, every new recipe will result in the creation of one more class. This leads to extreme verbosity. Second, class Recipe is abstract so it can not be instantiated: it has zero value on its own.
Last and worse, a loop has now been introduced into the classes dependencies. Simple inheritance creates a dependence from a class to its parent. An abstract class also depends on its descendants. Hence, method getAdditionalTasks can be viewed as a callback in the interface which separates WitchSoup from Recipe. As you may have experienced when writing unit tests with mocks, interfaces with callbacks are always harder to reason with. Classes WitchSoup and Recipe are actually very tightly coupled and cannot any longer be thought of as separate entities.

Unfortunately, I sometimes find this pattern in real (admittedly poor) production code (often buried down under a few more obfuscation layers).
Here is how this mess could be cleaned up:
class Recipe {

    private List<Task> _tasks;

    public Recipe(List<Task> additionalTasks) {
        this._tasks = new ArrayList<Task>();
        this._tasks.add(new PrepareUstensils());
        this._tasks.addAll(additionalTasks);
        this._tasks.add(new CleanKitchen());
    }

    public execute() {
        for (Task task in this._tasks) {
            task.executes();
        }
    }
}

class RecipeBook {

    Recipe retrieveWitchSoupRecipe() {
        List<Task> additionalTasks = new ArrayList<Task>();
        additionalTasks.add(new MeltButter());
        additionalTasks.add(new MashBeans()); 
        additionalTasks.add(new Boil());
        return new Recipe(additionalTasks);
    }
}
Abstract method getAdditionalTasks was simply replaced by a standard parameter of the Recipe's constructor. As class Recipe is now concrete, the need for WitchSoup or any other specific Recipe descendant disappears. Each recipe is replaced by a different creation method belonging to the factory class RecipeBook. The class hierarchy has flattened dramatically.

Flavor network: graph of ingredients as paired in recipe


From template method to strategy

Let us deepen our understanding of keyword abstract by playing with the template method pattern. In the following code, method accumulate combines all the integers present in its list parameter. It calls the abstract method combine to process integers pairwise. Any combinator can be chosen by implementing various subclasses. For instance, class MaximumAccumulator combines the integers by choosing the maximum, while SumAccumulator performs the addition.
abstract class Accumulator {
    public int accumulate(List<Integer> input) {
        int result = 0;
        for (Integer element in input) {
            combine(result, input);
        }
        return result;
    }

    public abstract int combine(int element1, int element2);
}

class MaximumAccumulator extends Accumulator {
    public int combine(int element1, int element2) {
        if (element1 < element2) return element2;
        return element1;
    }
}

class SumAccumulator extends Accumulator {
    public int combine(int element1, int element2) {
        return element1 + element2;
    }
}
Using pattern strategy we can replace inheritance by both an interface and composition. First we introduce interface Combinator:
interface Combinator {
    int combine(int element1, int element2);
}
Then, the Accumulator class can accept a Combinator as a constructor argument instead of expecting subclasses. The class loses its abstract status:
class Accumulator {

    private Combinator combinator;

    public Accumulator(Combinator combinator) {
        this.combinator = combinator;
    }

    public int accumulate(List<Integer> input) {
        int result = 0;
        for (Integer element in input) {
            this.combinator.combine(result, input);
        }
        return result;
    }
}
At last, classes MaximumAccumator and SumAccumulator can now implement interface Combinator. We also rename them to express their roles more precisely:
class MaximumCombinator implements Combinator {
    public int combine(int element1, int element2) {
        if (element1 < element2) return element2;
        return element1;
    }
}

class SumCombinator implements Combinator {
    public int combine(int element1, int element2) {
        return element1 + element2;
    }
}
As an option, we can also provide the following factory, so that code consumers have an easy way to instantiate different kinds of accumulators:
class AccumulatorFactory() {
    Accumulator createMaximumAccumulator() {
        return new Accumulator(new MaximumCombinator());
    }

    Accumulator createSumAccumulator() {
        return new Accumulator(new SumAccumulator());
    }
}
Note how, by removing class inheritance, the code has improved in several ways:
  • Class Accumulator and implementations of Combinator are not coupled anymore. Now the combinators can be reused outside of this particular context.
  • The responsibilities are more focused. So it is easier to have class names which faithfully express their implementation's intent.
  • Unit testing will be simpler. First, since Accumulator is not abstract anymore, it can now be tested in isolation. Tests of SumAccumulator and MaximumAccumulator would have been unnecessarily complex to write and redundant since they need to exercise the common inherited code twice. Now we can easily test the combinator's code.

By allowing the father's code to call some method defined in its children, the abstract keyword couples classes even more tightly than simple inheritance does. As a first conclusion of this example and the previous one, we can say the abstract keyword represents a strong code smell. As a coding rule, it seems safer to avoid it altogether.
Naval strategy (Yi Sun-sin turtle ship)


A side note on unit testing

The previous example briefly mentioned the topic of writing unit tests. So I am taking this opportunity to quickly discuss the impact of inheritance on tests.

I am a mockist. Writing unit tests the mockist style consists in replacing all dependencies by mock objects. Each mock object behaviour is described on a test by test basis by succinct propositions. The goal is to write tests as unitarily as possible, so that:
  • only few tests fail whenever a code change which breaks past behaviour occurs,
  • after each code change, it is easy to know which test suite should be run to detect potential regressions,
  • tests run faster,
  • and most importantly, the cost of writing and maintaining tests does not escalate with the program's size (or the classes' dependence depth).
So, from the mockist standpoint, inheritance is a problem. Inheritance allows to split code among separate units, but prevents from testing these units in isolation. Tests of the descendants in a class hierarchy are necessarily redundant since they exercise the code of the ancestor's classes multiple times.

Circus by Marc Chagall

7 comments:

  1. Unit testing is easy but a program on the whole is a monster to test.
    Manchester Parking

    ReplyDelete
  2. Naval strategies of that era is very incredible.
    concert tickets

    ReplyDelete
  3. Thanks for this informative article, I hope you will get most positive response specially for this post. . . .
    สูตรบาคาร่า
    โกเด้นสล็อต

    ReplyDelete
  4. เลเซอร์หน้าใส เป็นอีกหนึ่งของใหม่ทางความสวยที่ช่วยฟื้นฟูผิวหนังที่แห้งด้าน บริเวณใบหน้าหมองคล้ำให้กลับมาผ่องใสมองสดชื่น สดใสภายในช่วงเวลาอันรวดเร็วทันใจ นับว่าเป็นทางลัดความสวยที่กำลังเป็นที่นิยมสูง ช่วยปรนนิบัติวัตถากผิวให้ขาวกระจ่างขาวสวยใส จากการลดลางเลือนริ้วรอยจุดด่างดำได้อย่างมีคุณภาพ


    เลเซอร์หน้าใส
    เลเซอร์ลดริ้วรอย
    เลเซอร์รอยสิว

    ReplyDelete