Why composition over inheritance isn’t so clear cut
Composition patterns have been made easier through increasing IoC containers & dependency injections. Inheritance has been described as bad practice. But is that true? Here is my take.
Recently, I discussed the topic of composition over inheritance in my team at Queue-it. If you Google “inheritance versus composition” you will find many blogs and Stack Overflow discussions on the topic.
People’s opinions differ on which strategy is best. Comparing these two techniques from different viewpoints means you can find radical ideas supporting one and discouraging the other. Many have particularly favored composition over inheritance and some go even further, calling inheritance bad practice (Google “Inheritance is Evil”). For a detailed and balanced discussion, Steven Lowe has a nice blog post on the topic.
For me, it has been hard to accept inheritance as an “evil”
If I look back at how I learned object-oriented programming, all the sources I read always referred to inheritance and polymorphism as main concepts of OOP, alongside object, class, and encapsulation. In addition, I took part in several large, successful projects that were primarily written and based on inheritance and the template method design pattern (you could easily find more than five levels of inheritance in some classes).
Recently, though, I have been working on projects more reliant on IoC containers and defining loosely coupled small classes.
Now I can admit that using composition is easier and faster than inheritance in some cases.
So, it made me think about this topic more in-depth.
Here are some problems with using inheritance:
Coupling and implicit dependency between a superclass and its subclasses
Inheritance causes subclasses and superclasses to become coupled to each other, and it introduces implicit dependency between them. As a result, changing a superclass might result in changes in its subclasses. This coupling practically means that when you are coding in a subclass, you might have to get some detailed knowledge of the superclass. Inheritance is, to some extent, breaking information hiding, and encapsulation.
Costly refactors are needed to comply with the Liskov substitution principle
Many classes might inherit from a base class, but with time, you might find more tasks are shared in some subclasses, but not all. If you want to add the shared code in the base class, you will break the Liskov principle. So, you likely need to redesign your hierarchy structure, which is costly.
Costly refactors are needed to comply with the Single responsibility principle
In the same way, when you have many shared responsibilities and you put them in your base class but it gets too big, you will have to break your base class and do some refactoring. Again, it might be costly because subclasses depend on the base class, so it might cause changes in subclasses as well.
Limitation in subclasses
Programmers who want to inherit their new classes from a base class (that might be written by somebody else) have to live with the base class limitation.
Although there are ways to write unit tests for classes in hierarchy, it seems that unit testing or mocking a class, which is part of inheritance chain, is harder than unit testing or mocking a simple class without any level of inheritance.
The dependency between a subclass and its superclass is compile time, and you cannot change this relationship at runtime. It means you cannot change the parent of a subclass at runtime, in contrast to composition dependency, in which you can easily inject a new implementation of an interface at runtime.
Therefore, if we just think of inheritance as a way to reuse and share code between classes, it looks like overkill. Often, inheritances could be replaced with composition to gain this reusability in a cleaner and easier way.
So, it seems inheritance is a useless concept, right?
Actually, this conclusion is wrong. We should notice that sharing code is just one side of inheritance, and the semantic behind inheritance (the concept of a generic/specialized relationship between subclasses and superclasses) is not just for sharing code — it can provide good code structure.
Here are some benefits with “IS a relation” between classes:
Cohesion and code structure
Inheritance forces dependency between a superclass and its subclasses. But if this dependency is well designed, not only it is harmless, it could actually be useful and make code conceptually cohesive. For example, when you see an instance of subclass, you can assume you can treat that instance like its parent. For instance, in a UI reporting module, there could be different report items, like ShapeItem, LabelItem, FiledItem, etc. Because all these report items are subclasses of ReportItemBase, the report class can deal with all items in the same way (report item subclasses are conceptually coherent).
Grouping and modularizing classes (big picture, a.k.a. architecture)
Like the previous topic, by using inheritance correctly you can modularize your code, classify, and arrange concepts by generalization / specialization. This makes your code structured and much easier to understand and follow. A well-designed structure lets you explain the whole system at different levels of abstraction. At a high level, you just talk about base classes and how they connect to each other. At lower levels, you will go into the details and implementation of each subclass.
Applying policies (for example, cross cutting codes or domain specific logic) in different levels of hierarchy
Limiting programmers while they are extending a parent class might look like a drawback of inheritance. But it is actually a powerful feature if used correctly. In template method design pattern, superclasses let subclasses override some part of an algorithm. This means the parent class can do some custom actions before and after the subclass code (applying policies) without distracting programmers who are designing the subclasses. For example, in your business class, you can do custom logic, log, encrypt or decrypt data, data compression, security validation, etc., at different levels based on the type of a domain object and before sending queries to the repository.
Framework and plugins
Imagine that you are developing a big monolithic application and have some programmers with differing degrees of technical abilities. They want to add new features to the project for the purpose of costs, integrity, reusing, applying policies, etc. You have developed a framework in which programmers can add their custom code into the project just by extending base classes.
A good example of this is creating a new custom UI control while doing Windows Form programming. Form controls will inherit from each other and it is easy to extend a base control class and make a new UI control. In this case, the correct class as superclass should be selected, and then the programmer will extend the base and implement just some specific functions without going into the details of how Windows controls work. This makes for an easier and cheaper programming style.
Above, I’ve referred to this static dependency as a weak point for inheritance, but personally, I sometimes see it as an advantage of inheritance. I can follow my code statically in my favorite IDE (Visual Studio) and see how the base class is used and implemented. I can also use it as documentation of my code.
I think both techniques have their own purpose. There’s no need to prefer composition over inheritance outright.
On reflection, I can partially agree that depending too much on inheritance might be unwise for small applications. Still, I think the benefits and power of inheritance in big applications, libraries and frameworks outweigh the drawbacks.
Conversely, composition is easier to use, it keeps encapsulation, defines more explicit dependencies between classes (just dependency on interfaces), and totally fits to be used with IoC, and is also easier to be unit tested. However, a big drawback of too many small classes connected by composition is that the code base would lose its cohesion, it can be hard to follow code structure, and it might be hard to apply policies if you do not have a base class.
Written by: Mojtaba Sarooghi, Queue-it Software Developer