This article is going to look at how to implement a parameterizable algorithm so it can both conform generally to the open/closed principal and also most effectively be tested.
A common pattern for code reuse, implementation selection, or extension, is to use class inheritance and the template method pattern. This is an example of an implementation of the template method pattern with two different variations B
and C
:
abstract class A { public void doSomething() { doP(); doQ(); doR(); } protected abstract void doP(); protected abstract void doQ(); protected abstract void doR(); } class B extends A { @Override protected void doP() { /* do P the B way */} @Override protected void doQ() { /* do Q the B way */} @Override protected void doR() { /* do R the B way */} } class C extends A { @Override protected void doP() { /* do P the C way */} @Override protected void doQ() { /* do Q the C way */} @Override protected void doR() { /* do R the C way */} } |
Refactoring template method pattern to Strategy Pattern
We can always convert this template method pattern to a compositional pattern by performing a refactoring in the following steps (if you have member variable access there are a couple more steps, but I’ll cover them in a follow up article):
Step 1; encapsulate the construction of the B and C strategies:
abstract class A { public void doSomething() { doP(); doQ(); doR(); } protected abstract void doP(); protected abstract void doQ(); protected abstract void doR(); } class B extends A { public static A createB() { return new B(); } @Override protected void doP() { /* do P the B way */} @Override protected void doQ() { /* do Q the B way */} @Override protected void doR() { /* do R the B way */} } class C extends A { public static A createC() { return new C(); } @Override protected void doP() { /* do P the C way */} @Override protected void doQ() { /* do Q the C way */} @Override protected void doR() { /* do R the C way */} } |
Step 2; extract an interface for the strategy methods:
interface S { void doP(); void doQ(); void doR(); } abstract class A implements S { private final S s = this; public void doSomething() { s.doP(); s.doQ(); s.doR(); } } class B extends A { public static A createB() { return new B(); } @Override public void doP() { /* do P the B way */} @Override public void doQ() { /* do Q the B way */} @Override public void doR() { /* do R the B way */} } class C extends A { public static A createC() { return new C(); } @Override public void doP() { /* do P the C way */} @Override public void doQ() { /* do Q the C way */} @Override public void doR() { /* do R the C way */} } |
Step 3; pass the strategies into the superclass instead of using this
:
interface S { void doP(); void doQ(); void doR(); } final class A { private final S s; public A(final S s) { this.s = s; } public void doSomething() { s.doP(); s.doQ(); s.doR(); } } class B implements S { public static A createB() { return new A(new B()); } public void doP() { /* do P the B way */} public void doQ() { /* do Q the B way */} public void doR() { /* do R the B way */} } class C implements S { public static A createC() { return new A(new C()); } public void doP() { /* do P the C way */} public void doQ() { /* do Q the C way */} public void doR() { /* do R the C way */} } |
Advantage of the compositional style
Less fragile
Changes to A
such as new methods are much less likely to break the strategies in the compositional style.
Easier to Test
In the compositional style, class A
can be tested by itself using Mocks. As can class B
and class C
. In the inheritance style class B
and class C
cannot be tested without also testing class A
. This leads to duplication in the tests, as features of class A are re-tested for every subclass.
Easier to Reuse
In the compositional style class B
and class C
can be reused in other contexts where A is not relevant. In the inheritance style this type of reuse is not possible.
Emergent Model/Domain concepts
If class B
or class C
are reused in a different context, it may turn out that during subsequent refactoring they develop a new role within the system. I may even discover an important new domain concept.
This is related to the Expression Problem