A topic frequently written about in the context of code quality, Composition vs Inheritance ultimately impacts team productivity, so why has so much less been written about how it can impact a team’s productivity?
I’m going to go over the high level points about scrunch’s decision to move away from inheritance trees as much as possible and more towards modular composition based code.
What Was Wrong?
So what really prompted thought process that led down the composition path? There isn’t really a simple answer; it was more a combination of bad experiences onboarding staff into old code bases, some smells that kept popping up and ultimately the happiness of the staff working within our existing processes (well-tested code etc).
There’s plenty you can go and read about what composition vs inheritance actually means from a code level, however I’m going to go over my 5 points in the context of team happiness.
Easier To Read And Review Diffs/Pull Requests
Imagine a pull request with a series of changes across multiple files of existing code. A sprawling mess of clusters of red and green lines spread all over the place, with an equal matching sprawl of updated tests to match. It’s a reviewer’s nightmare. If your reviewer is a senior engineer they are not going to be happy about having to spend a significant amount of time going through the sprawl trying to make sure the changes don’t break depended upon (and probably live in production) behaviour already implemented in the code. A reviewer’s job is a lot easier if they have larger blocks of green and matching blocks of green tests; large changes in less files; less touch points with existing code, more sequential lines of code rather than tiny diffs.
We’ve noticed these ‘bitsy’ reviews tend to happen a lot more when dealing with adding new features to existing inheritance trees. With more composition style code you may modify some existing method to use a new dependency, then implement the dependency as a whole new, smaller class. This leads to an easier job for the reviewer, greater cohesion between team members and more time to focus on new features!
Junior And Midlevel Engineers Can Get Started With Less Fuss
Programmers new to a codebase find large inheritance trees very confusing and mindbending to deal with. It forces them to learn about a bunch of things they don't necessarily need to know about up front in order to be useful implementing some new feature.
By not having to grok a whole inheritance tree and associated dependencies juniors can get started implementing a feature with minimal fuss. They can implement the class and tests, possibly without even hooking it into the existing code if someone else will do that. A nice modular component that they can feel a sense of accomplishment about, while having semi-greenfield development to help them onboard into a team and product they still have a lot to learn about.
Greater Sense Of Ownership From The Team
Often when large inheritance trees have lots of people making peripheral changes, people become less precious about making bad changes to the codebase. Generally, it’s great when more people are familiar with the code, but not if that reduces the collective sense of responsibility for it.
By committing large contiguous blocks of code, people will take more ownership and pride in the work they are delivering, and it’ll be easier for other staff to find the person responsible for that block of code too. Obviously, sometimes there will be bug fixes and small changes, but they will ideally be small.
If Someone Borks An Implementation, It’s Mostly Easy To Swap It Out
This one kind of speaks for itself but generally, if mistakes ARE made it’s easy to switch out an implementation as long as the interface still makes sense (make sure you do code reviews!). Instead of finger pointing, it’s easier to create a new implementation and turn it into an educational process. If you’re using XP practices this can be a great thing for a junior and senior to pair on.
Optimisation Can Be Done At An Instance Branch Level, Not Subclass Level
This one is quite subtle but very powerful. As your codebase scales and situations that demand flexibility from your code arise, frequently one of the first things to address is performance. If you need to change the implementation of a class where the change would impact several levels of an inheritance tree the work involved in ensuring it doesn’t completely break existing code is extensive. The alternative is that a composed dependency of the class where the optimisation needs to be applied can be done in the depended upon class and any of its own dependencies.
You can think of it this way: instead of having to break some crazy inheritance structure, which is a tree of classes, you can break it at the depended upon instance level, a tree of instances.
Making refactoring and maintenance easy for developers will save them significant time and heartache. Who doesn’t want that?
Bonus Point: Inheritance Is Viral; Unchecked It Will Run Rampant
Given all of the above, it’s clear there are benefits to using more composition based structures over inheritance. One of the things that tends to happen with inheritance structures is that people start thinking about everything in that context. As new features are added, there tends to be new methods on existing classes or new subclasses overwriting single methods. Inheritance is viral, easy to catch and hard to notice until it’s too late!
So here you’ve seen a few points that we’ve found are the almost hidden and rarely discussed side effects of encouraging composition over inheritance. What started as a gut feeling amongst a few developers has turned into something that is considered at every code review and whenever questions of code structure are raised.