From inheritance to decorator
This is one of the advanced questions I sometimes ask in interviews:
Most candidates will struggle, yet the core idea is again to use a combination of interface and composition in replacement of inheritance. Through a gradual refactoring, we flatten the class hierarchy and introduce the decorator pattern.
First let us define interface I, which declares method f:
Thanks to the decorator pattern, getting any combination we could wish for is extremely easy:
At last, as was already shown in the previous recipe example, we could, if necessary, implement a factory that offers shortcuts to rapidly instantiate a variety of classes, all implementing interface I.
Suppose you have three classes A, B and C. Class A has a method f which performs some task. Classes B and C inherit A. Both classes override method f to add some extra behaviour to the base version. Class B outputs some log:
class B extends A { public override void f() { super.f(); log("f was called"); } }, while class C increments some counter:
class C extends A { private int counter; public C() { this.counter = 0; } override public override void f() { super.f(); this.counter++; } }Now, you would like to instantiate a class which mixes all behaviours together: the base behaviour, the log and the counter increment. How would you do it? I am expecting a clean design, which can easily evolve in the future if needed.
Most candidates will struggle, yet the core idea is again to use a combination of interface and composition in replacement of inheritance. Through a gradual refactoring, we flatten the class hierarchy and introduce the decorator pattern.
First let us define interface I, which declares method f:
interface I { void f(); }Then, we can have all classes implement interface I. Next, we can modify class B so that, instead of inheriting class A, it expects an object with interface I as argument of its constructor. In its implementation of method f, class B calls the base version of method f from this interface rather than from its super-class:
class B implements I { private I i; B(I i) { this.i = i; } void f() { this.i.f(); log("f was called"); } }We do the same with class C:
class C implements I { private I i; private int counter; public C(I i) { this.i = i; this.counter = 0; } public void f() { this.i.f(); this.counter++; } }
Classes B and C are now both decorators: if we view classes as API transformers, then we can see they both expect an interface I as input, and they both build instances which provide the same interface I, yet adding a layer of behaviour in the middle.
Peeling the onion |
Thanks to the decorator pattern, getting any combination we could wish for is extremely easy:
- new A() provides only f,
- new B(new A()) provides f and logging,
- new C(new A()) provides f and statistics,
- new B(new C(new A())) or new C(new B(new A())) provide f, logging and statistics.
At last, as was already shown in the previous recipe example, we could, if necessary, implement a factory that offers shortcuts to rapidly instantiate a variety of classes, all implementing interface I.
Avoiding override
As previously mentioned here, overridden methods are sometimes a form of dead code. Indeed, writing some code in the base class only to scratch it in the children, is not very elegant. Hopefully, keyword override can be replaced by the use of interfaces. Here is how.
Let us start again from our canonical hierarchy: class A father of classes B and C. Class A defines a method f, which is overridden in C (but not in B). All other things being equals, it is preferable to create an interface I which defines method f and have class A accept this interface as one of its constructor argument.
Two additional classes which both implement interface I are created out of the definitions of f present in A and C. By splitting our implementation into smaller components, we gained flexibility:
Let us start again from our canonical hierarchy: class A father of classes B and C. Class A defines a method f, which is overridden in C (but not in B). All other things being equals, it is preferable to create an interface I which defines method f and have class A accept this interface as one of its constructor argument.
Two additional classes which both implement interface I are created out of the definitions of f present in A and C. By splitting our implementation into smaller components, we gained flexibility:
- We can assemble a class B with the version of method f which was originally present in C, mixed in.
- Interface I (possibly after some renaming) can now be reused outside of its original context.
No comments:
Post a Comment