Section 12.4 Comparison of Extension mechanisms
Subsection 12.4.1 Basic comparison and tradeoffs
We have thus far seen the two main extension mechanisms: inheritance by means of subclasses, and object composition. Let’s compare the two:
- In both cases we have two classes that each express a behavior/responsibility. Both techniques attempt to make those two behaviors work together towards a common goal, but they differ in how they go about it.
- Inheritance results in one object with two classes, while composition consists of two objects with two different classes, but each object having one class. So composition is a tad more expensive in memory, as two objects take up more space than one.
- Continuing on the same theme, execution of the composition code is a tad slower as it requires making a method call to another class.
- In composition, the interaction between the two behaviors is more explicit, by the presence of the a field used from the one class to access the other. By contrast in inheritance, the interaction happens directly by a call to a method that happens to be in the superclass, but there is no clear indication in the code that this is the case. Composition makes it explicit which behavior comes from which class.
- The relation between the two behaviors is static in the case of inheritance, with a single object carrying both responsibilities and the class extension being specified at compile time. On the other hand, the relation in the case of composition is more dynamic, given by an object stored in a field and possibly settable at runtime. This provides the composition technique with immense flexibility, which we will explore later. For example, we could have a single
Course
concept that works with a number of differentGrade
systems, that are maybe expressed via aGrade
interface and a number of different implementations of that interface. - Building on this earlier point, composition relies on the stated interface and behavior of the second object/class, while inheritance relies on the specific implementation provided by the superclass. Inheritance in that sense is more rigid.
So to sum up, inheritance is more rigid but computationally slightly more efficient, while composition is more flexible and computationally slightly less efficient. In general we are often presented with this tradeoff between efficiency and flexibility, you have to sacrifice the one in order to gain the other.
Subsection 12.4.2 Has-a vs is-a relationships
The above two extension mechanisms are often phrased in terms of the kind of relationship between the two classes. Inheritance is likened to a is-a relationship. For example we could say that a student is a person, and that therefore it makes sense to have
Student
as a subclass of Person
. On the other hand for courses and grades it might make more sense to say that a "course" has a grade (or better yet that a student’s enrollment in a course should have a grade, we’ll probably replace Course with Enrollment in the future); this then corresponds to a has-a relationship, which lends to the composition approach where a Course
has in it a field of type Person
.This thought process can be helpful in deciding between the two approaches, but I would caution you to not rely on it too literally. An important rule to keep in mind is that the objects and classes we use in our programs may represent real life entities, but they are just that, representations of those entities; and these representations don’t have to have the same relationship that the real entities do.
The decision on which mechanism to use ultimately relates more to a question as to the rigid and strong coupling that inheritance provides, and whether this is appropriate for our use case.
A common example of this is the so-called template method pattern that is popular amongst frameworks. In effect a framework provides a strict and opinionated way around doing something, and a common way of enforcing that is by providing a method that does most of the work, but which contains some parts that are left up to the user. This is accomplished by the method calling on other methods of the class which are abstract, and you are expected to subclass the class and provide implementations of those methods. We’ll revisit this pattern later, but the gist of it is that there is a core implementation provided by the framework and living in the superclass, and we are implementing a subclass in order to use that framework. There is therefore a very tight coupling between the subclass and the framework-provided superclass; we would never want to use our class with a different superclass, it would make no sense.
This is in many ways a good deciding factor: Can you imagine your subclass/derived class doing its thing with many different superclasses? If yes, then you probably want a composition approach instead, so that you can change the class used. If no, then perhaps inheritance makes more sense.
So to sum up, use inheritance if:
- you are using a framework that requires you to interact with it by providing subclasses for its elements, or
- there is a tight coupling between the work that the subclass does and the work that the superclass does, and a different implementation of the superclass would not be meaningful.
Otherwise use composition.
Subsection 12.4.3 The problem of multiple dimensions
Another important reason to favor composition over inheritance in most situations is the so-called problem of multiple dimensions. By dimension here we simply mean "one kind of change/parameter". If we have an object/class that needs to somehow handle different kinds of changes, composition provides a more manageable way to do so.
As an example, consider what I called a
Course
before. Actually perhaps it is time to start calling it something else, for example let’s talk about a GradeRecord
: An entity that represents a grade present in the student’s transcript. What might that entry consists of?- It of course would need to have a grade. This may be a regular letter grade, or it may be an "in-progress" entry, or perhaps a "waived" entry. These might need to be represented by different classes, and present one dimension to the problem.
- It also needs to have a source for the entry. This might typically be a course, but it could also be some sort of transfer credit, placement test or requirement equivalence. These might need to be represented by different classes, and present a second dimension to the problem.
So a
GradeRecord
class needs to have a relation with two other classes. If we are using the compositional approach this is easy: Simply give the class two fields, one for the grade and one for the source. Then we may need different subclasses for the different kinds of Grade
and subclasses for the different kinds of Source
.Now what if we wanted to do inheritance? Since inheritance locks in the implementation of the superclass, we would need to create a new subclass for each combination of grade and source. For example if we had 3 different classes representing grades and 4 different classes representing sources, we would in effect need $3\times 4=12$ different subclasses. That’s quite a lot! And if we had a third dimension, we would have needed a product of three numbers there, and an even more out of control number of subclasses. Inheritance does not scale well with the number of dimensions.
So when dealing with different dimensions, the compositional approach is almost always a cleaner solution.