Thursday, June 20, 2019

Modern Programming: never use Inheritance; use Composition instead

Inheritance vs Composition is an age old debate. The world has evolved enough that it's time to put this discussion to rest. There is no good reason to ever use inheritance in new code. Composition is functionally identical to Inheritance, produces superior outcomes, flatter class hierarchies and more flexible code than inheritance. Composition also does not violate encapsulation and avoids classes of issues produced by unexpected polymorphic dispatch to implementations. We get better class design for free as well. 

In short, never use inheritance - always express the same code re-use through composition and be happy. Let's go through each of the points one by one:

1. Composition is functionally identical to inheritance.
This one is easy to work with. When inheriting implementations, all subclass methods have an implicit parameter (an instance of the superclass). Composition just makes this parameter explicit as a constructor argument and takes away the superclass. Code reuse through calling the superclass method can be done by just calling the same method on the composed object.

2. Composition produces superior outcomes
When operating in an inheritance class hierarchy, a code dependency and a type dependency become coupled. Very often, this is not necessary. For example, a 2DSurface class may have a computeArea method that you want to re-use, however your class is not a 2DSurface. With inheritance, the 2DSurface type and the code reuse are coupled. With composition, the two are separate. This is advantageous when you want to introduce code between the levels of the hierarchy. Eg. You want to add a "TimingClass" that wraps calls into to the base class. Without composition, the inheritance hierarchy deepens and a TimingClass also ends up being a 2DSurface.

3. Composition produces flatter class hierarchies
When operating within an inheritance hierarchy, the more variants or ways the code gets reused, the levels of the hierarchy grow deeper with "leaf" level code overriding the base class code. Code with more than 2 layers of hierarchy is very difficult to manage since you have to trace up and down layers of code that are mutually calling each other. Composition forces a single, clean extension point in the code and adding in classes like the "TimingClass" above do not increase the depth of the inheritance hierarchy.

4. Composition does not violate encapsulation a.k.a. friendly refactoring
Changing a base class method's access and mutations of a base class member is virtually impossible in a non trivial class hierarchy (because subclasses may rely on protected methods or direct access to state which violates encapsulation). The solution is to solely depend on the public api methods of a class (so that internal state may be refactored without affecting behavior). Composition forces this in a direct manner (enforced by the compiler).

5. Composition prevents polymorphic dispatch bugs
A common (bad) design pattern is to have an abstract base class call the abstract method that must be implemented by subclasses. This produces bugs when code is called across different levels of the hierarchy. Check out the first example on this page.

6. Polymorphic dispatch can be implemented better through generics and type bounds
The common process of dispatching through a single "super-class" hierarchy is antiquated. Generics with type bounds provide a much better substitute. Eg. Shape.getArea() doesn't need to be implemented by writing all code against the abstract Shape class with the Shape class providing .getLength() and .getWidth() that get overridden. A much better way is to implement finer grained interfaces (traits) of the form: ShapeWithArea (.getArea()), RectilinearShape  (.getLength(), .getWidth()) . Composition can then fully express just the needed dependencies (eg. Class<T extends ShapeWithArea> if it just needs a ShapeWithArea but doesn't need a RectilinearShape and if it needs both, the type parameter becomes Class<T extends ShapeWithArea, RectilinearShape>). 

7. Dependency injection / Testability is much easier with composition
In composition, the superclass is just another dependency of the class and thus can be mocked and faked. This is much harder to do when trying to mock out the superclass itself in the type hierarchy if inheritance is used. Even if we tried to stub out the superclass methods, we wouldn't know if new ones got added.

Overall, all the advancements in our understanding of type theory, category theory point to composition being the "right" abstraction for composing code. Inheritance is a special case of composition (a trivial subset), has worse properties around unit-testing, dependency injection and code maintenance. Given the above, inheritance from concrete or abstract classes (especially those with any form of state) should be strongly avoided. Instead, implementation of interfaces + composition should be the strongly preferred approach. 

No comments:

Post a Comment