I was attempting to make some changes to some fairly old code in our codebase (something I probably wrote myself…) which hasn’t been touched on for a while.
Naturally, my first step is to understand what the code does, so I started by looking at the class where I need to make my changes.
Perhaps unsurprisingly, I couldn’t figure out how it works.. The class contains only a handful of override methods, but I have no idea how they fit together.
So I started digging deeper through several layers of abstract classes, each filling in parts of the puzzle, until I reached the base of this class hierarchy.
By this point, I’m staring at a control flow full of strategically placed gaps. Going back-and-forth along the class hierarchy several times, I ended up with a vague sense of how the various pieces of logic scattered across the hierarchy fit together.
What’s more, where we needed to deviate from the control flow dictated by the base class, we have had to reinvent a brand new control flow mid-hierarchy, making it even harder for me to understand what’s going on.
This is not where I want to be… I want to be able to reason about my code easily, and with confidence.
I suspect a great many of you have experienced similar pains in the past, but how do you go about applying Composition over Inheritance?
Wikipedia’s definition and example of Composition over Inheritance focuses only on domain modelling, and I’m generally not a fan of conclusions such as:
To favor composition over inheritance is a design principle that gives the design higher flexibility, giving business-domain classes and more stable business domain in the long term.
In other words, HAS-A can be better than an IS-A relationship.
What does this even mean?!? Are you able to back up these claims of “high flexibility” and “more stable business domain in the long term” with empirical evidence?
In my view, the real benefit of Composition over Inheritance is that it encourages better problem decomposition – if you don’t break up the problem into smaller pieces (that are easier to tackle) first you have nothing to compose with later on. Scott Wlaschin’s railway oriented programming approach is an excellent example of how to apply composition in a practical and elegant way.
And the challenge of problem decomposition is not limited to code organization. Microservices are all the rage right now, and the move from monolithic architectures to microservices is another example of problem decomposition, albeit one that happens at a higher level.
So I’ll be taking knife to the aforementioned class hierarchy and replacing them with small, composable units, using a language that is a great fit for the job – F#!
Will you follow my lead?