Skip to main content
1 of 2
Theraot
  • 9.2k
  • 2
  • 28
  • 35

"Prefer composition over inheritance" is just a good heuristic

You should consider the context, as this is not a universal rule. Do not take it to mean never use inheritance when you can do composition. If that were the case, you would fix it by banning inheritance.

I hope to make this point cleared along this post.

I will not try to defend the merits of composition by itself. Which I consider off topic. Instead, I will talk about some situations when developer may consider inheritance that would be better using composition. On that note, inheritance has its own merits, which I also consider off topic.


Stream example

I will take the example of a Stream class. This class allows you to read and write byte arrays from it.

If you want to provide the ability to read and write custom struct without losing your ability to read and write byte arrays, you may consider inheriting from it. If every developer does this, it will lead to different classes to do different operations on the Stream, and no way to reconcile them… if a developer wants to do operation X that is provided by StreamX and operation Y that is provided by StreamY that developer will start looking for multiple inheritance. When that happens, things has already gone wrong.

Therefore, inheritance is not a good way to extend behavior. It would be better to provide some form of utility methods. If we plan to use it to extend behavior and state, we should consider composition.

Let us say you did use inheritance anyway. Now you have no way to decouple your code from the Stream implementation. Disregarding the case where Stream changes, this can still cause trouble. In particular, if you want to test your code without creating an actual Stream, you cannot mock it, because it is in your inheritance chain.

Sometimes when people say "Prefer composition over inheritance", they actually mean to use composition and other things… For example, if I create class that uses a Stream member, I have lost the ability to replace a Steam object with one of mine. Given the situation, it can be better to use an IStream member and implement IStream. Where IStream is an interface※. This way, if necessary, multiple inheritance of interfaces is possible. In addition, you are not coupling to a particular implementation. Increasing reusability and testability of your code.

※: This also allows having multiple types of IStream. For example, one could read from a network socket, another from file system, and another could just store in memory. I am aware this could also work with an abstract class. As I said earlier, inheritance has its own merits. This is about situations that are better with composition.

Remember that inheritance is a tool for polymorphism. In addition, polymorphism is a tool for reusability. If you can increase the reusability of your code by using composition instead of inheritance, then do so. If you are not sure whatever or not composition provides better reusability, "Prefer composition over inheritance" is a good heuristic.


Vehicule example

Let us go for a variant of the classical examples that some OOP courses use… We have a Vehicle class, then we derive Car, Airplane, Balloon and Ship. Then Car and Airplane may have some common code, because they both can roll on wheel on land. We may consider creating an intermediary class in the inheritance chain for that. Yet, actually there is also some shared code between Airplane and Balloon.

Note: If you need to ground this example, pretend these are kinds of objects in a video game.

It is better to model this behavior as interfaces and composition, so we can reuse it without having to run into multiple class inheritance. If we, for example, create the FlyingVehicule class. We would be saying that Airplane is a FlyingVehicule (class inheritance), but we could also say that Airplane has a Flying component (composition) and Airplane is a IFlyingVehicule (interface inheritance).

All that without mentioning Amphibious.


Yo-Yo problem

Another case where composition helps is the Yo-Yo problem. This is a quote from Wikipedia:

In software development, the yo-yo problem is an anti-pattern that occurs when a programmer has to read and understand a program whose inheritance graph is so long and complicated that the programmer has to keep flipping between many different class definitions in order to follow the control flow of the program.

You resolve this by using a form of the adapter pattern. Which is again the same situation I have been talking about: Your class C will not inherit from class B. Instead your class C will have a member of type A, which may or may not be (or have) an object of type B. This way you will not be programming against the implementation detail of B, but against the contract that the interface (of) A offers.


Counter examples

Many frameworks favor inheritance over composition (which is the opposite of what we have been arguing). Developer may do this because they put a lot of work into their base class that having it implemented with composition would have increase the size of the client code. Sometimes this is due to limitations of the language.

For example, a PHP ORM framework may create a base class that uses magic methods to allow writing code as if the object had real properties. Instead the code handled by the magic methods will be going to the database, query for the particular requested field (perhaps cache it for future request) and return it. Doing it with composition would require the client to either create properties for each field or write some version of the magic methods code.

Addendum: There some other ways one allow extending ORM objects. Thus, I do not think inheritance is necessary on this case.

For another example, a video game engine may create a base class that uses native code depending on the target platform to do 3D rendering and event handling. This code is complex and platform specific. It would be expensive and error prone for the developer user of the engine to deal with this code, in fact, that is part of the reason of using the engine.

Furthermore, without the 3D rendering part, this is how many widget frameworks work. This releases you from worrying about handling OS messages… in fact, in many languages you cannot write such code without some form of native biding. Moreover, if you were to do it, it would tight your portability. Instead, with inheritance, provided the developer do not break compatibility (too much); you will be able to easily port your code to any new platforms they support in the future.

In addition of what I said above, consider that many times we only want to override a few methods and leave everything else with the default implementations. Had we been using composition we would have to create all those methods, even if only to delegate to the wrapped object.

By this argument, there is a point where composition can be worst for maintainability than inheritance (when the base class is too complex). Yet, remember that maintainability of inheritance can be worse than that of composition (when the inheritance tree is too complex), which is what I refer to in the yo-yo problem.

In the presented examples, the developers rarely intend to reuse the code generated via inheritance in other projects. That mitigates the diminished reusability of using inheritance instead of composition. In addition, by using inheritance the framework developers can provide a lot of easy to use and easy to discover code.


Final thoughts

As you can see, composition has some advantage over inheritance in some situations, not always. It is important to consider the context and the different factors involved (such as reusability, maintainability, testability, etc…) to make the decision. Back to the first point: "Prefer composition over inheritance" is a just good heuristic.

You may also have notices that many of the situation I describe can be resolved with Traits or Mixins to some degree. Sadly, these are not common features in the great list of languages, and they usually come with some performance cost. Thankfully, their popular cousin Extension Methods and Default Implementations mitigate some situations.

I have a recent post where I talk about some of the advantages of interfaces in why do we require interfaces between UI,Business and Data access in C#. It helps decoupling and eases reusability and testability, you might be interested.

Theraot
  • 9.2k
  • 2
  • 28
  • 35