Inheritance: A case study on easy versus simple
Rich Hickey, creator of Clojure, delivered a great talk where he discusses the meanings of easy vs simple from a software development point of view. I urge you to watch it before reading this, but just in case you decide to do it later I’ll try to introduce the concepts.
Rich presents easy as something that is within reach, available, familiar. Where simple is defined as not complex, that is, something that is not entwined with different concepts.
Coming from an Object Oriented world, I find inheritance and subclassing to be very easy. Inheritance is advocated as one of the core constructs of the Object Oriented paradigm. It is how we learn to share implementations of behavior and provide type information to satisfy Polymorphism. However, I’ve come to realize that is also not simple.
Please bear with me as I fabricate an example to illustrate why I think this is true. I’m focusing on one bad aspect of inheritance and won’t even touch the “is a” problem (read up on the Liskov Substitution Principle if you’re interested).
Let’s imagine you are implementing an email sending module. Our first iteration (using Ruby) might look something like this:
Great, huh? This is simple. This class does one thing and one thing only.
Soon enough we have a new requirement. Marketing wants us to use different email servers when sending different types of emails. The code that uses our modules must be able to specify which implementation it wants to use. One possible solution to this problem:
This looks OK. Inheritance allowed us to share in the behavior of the parent class. The DRY gods are pleased.
Guess what? A new requirement comes along. Now, for our Transactional mails we’d also like to block email sending if the user’s subscription has expired. Oh, and operations wants us to log every time an email gets sent. Easy enough right?
This is starting to smell funny. Why does the blocking feature needs to know about the existence of the logging code? It could have been the other way around or we could have introduced them as separate concepts. But then, how do you combine them again?
Because of GDPR pressure, we need to add a data export feature that emails the user their data. This is true whether their subscription is valid or not.
…
Well, we could introduce a branch in the hierarchy that does not include the block checking feature, but can you see where that leads us? We’ll end up with an explosion of n² classes where n is the number of features. We’ll have a `BlockingLoggingTransactionalMailer` and `LoggingTransactionalMailer` and `BlockingTransactionalMailer`. Imagine keeping track and unit testing all the different combinations. Imagine trying to introduce a change in the base implementation, that is, changing the actual method we use to send emails. That would have a ripple effect on the entire chain.
So what’s the problem here? Inheritance made our lives initially easy but because it complects code reuse with hierarchy we eventually reached that very familiar point where the code hampers future developments and slows us down.
Now, let’s look at an alternative:
This alternative used composition to achieve the same result without any of the aforementioned problems. Note I’m not saying this is the best code ever. For example, this chain model is very frail. If any non essential middleware raises an exception it would break the entire chain.
But as I can tell by the way you’re twisting your nose at it, it’s not easy. I can hear you saying “Really? Do I need to instantiate all those classes every time I need to send an email?”. Yes, we could work around that but the fact is that there are more things now to deal with. The cardinality of the system has increased. But as Rich puts it, simplicity does not relate to cardinality. We have more things now but they are no longer entwined.
Simple but definitely not easy.
Does that imply a trade-off? I don’t think so. Go decided to eschew inheritance but introduced embedding to facilitate (make easy) the reuse of code. Together with interfaces I think it solves the problem really well and I like the song goes “I don’t remember why I ever once subclassed”.
In short:
Inheritance is easy but not simple.
Composition can be simple, but often it is not easy.
We as developers have the power to make simple easier.
Thanks for reading this far, please leave a comment with your thoughts on the subject.