How many times did you ask yourself, in order to avoid duplicated code, if you should use inheritance or composition? Did you ask yourself about the differences between subtype and subclass? Do you always follows the Liskov Substitution Principle? This post will try to help to answer these questions. And try to give you more clarity to how and when subclassing is appropriate to be used.

The content of this post is a combination of paragraphs extracted from two great books: An Introduction to Object Oriented Programming, by Timothy Budd and Program Development in Java, by Barbara Liskov and John Guttag.

I would say that subclassing is used correctly if the subclass is also a subtype of the parent class. Let me try to explain this sentence, starting with what type and subtype means.

Subclass and Subtype

When we say that the number 5 is of type integer, we are stating that 5 belongs to a set of possible values (as an example, see the possible values for the Java primitive types). We are also stating that there is a valid set of methods I can perform on the value like addition and subtraction. And finally we are stating that there are a set of properties that are always satisfied, for example, if I add the values 3 and 5, I will get 8 as a result.

To give another example, think about the abstract data types, Set of integers and List of integers, the values they can hold are restricted to integers. They both support a set of methods, like add(newValue) and size(). And they both have different properties (class invariant), Sets does not allow duplicates while List does allow duplicates (of course there are other properties that they both satisfy).

Subtype is also a type, which has a relation to another type, called parent type (or supertype). The subtype must satisfy the features (values, methods and properties) of the parent type. The relation means that in any context where the supertype is expected, it can be substitutable by a subtype, without affecting the behaviour of the execution. Let’s go to see some code to exemplify what I’m saying. Suppose I write a List of integers (in some sort of pseudo language):

class List {
  data = new Array();

  Integer size() {
    return data.length;
  }

  add(Integer anInteger) {
    data[data.length] = anInteger;
  }
}

Then, I write the Set of integers as a subclass of the List of integers:

class Set, inheriting from: List {
  add(Integer anInteger) {
    if (data.notContains(anInteger)) {
      super.add(anInteger);
    }
  }
}

Our Set of integers class is a subclass of List of Integers, but is not a subtype, due to it is not satisfying all the features of the List class. The values, and the signature of the methods are satisfied but the properties are not. The behaviour of the add(Integer) method has been changed, not preserving the properties of the parent type.

How bad is this?

Well, this allows you to use Sets where Lists are expected. For example, a method in any other class might operate on a List, receiving it as a parameter, but at runtime you might receive a Set, which in that case it will change the behaviour that you are expecting, which is the behavior of the parent type. That is not goodYou have to think from the perspective of the client of your abstractions (a class and subclass in this case). This change in the behaviour will be a big surprise for your clients.

The following test case will pass if we use a List, but it will fail if we use a Set which was defined as a subclass of List.

test_subtype_relation() {
  list = new List(); # using Set() will break this test, not good.
  list.add(10);
  list.add(10);
  assertEquals(2, list.size());
}

The tests you have for your parent classes should still pass if you use instead any of their subclasses.

The example presented above is a classic use of inheritance only for code reuse, the size() method fits perfect with my Set of integers. In these cases, composition is your choice.

I hope this is not a surprise for you, but here is another consequence of this bad use of inheritance: we are violating the Liskov Substitution Principle which has been made popular by the SOLID principles. Remember, if you are subclassing but violating the subtyping relation, you are not following the Liskov Substitution Principle.

Is Overriding Always Bad?

From the example above, you might have gotten the feeling that overriding is always a bad choice if you want your subclass also be a subtype. That is not the case. Think about, for example, what would happen for the Object class in languages like Smalltalk or Java. They both implement a default behaviour for the equals(anObject) (in the case of Java) method, and the =anObject (in the case of smalltalk) method. Your subclasses, most of the time, needs to override this behaviour, and we still need to do it in the proper way.

So, to do it in the right way, we need to reason about the specification, the preconditions and the postconditions, of the method that we want to override. See the specification of the method add(anInteger) below from our previous example:

# pre:  -
# post: adds anInteger to this
List.add(anInteger);
# pre:  -
# post: if anInteger is not in this, then added it to this
Set.add(anInteger);

When you override a method, the precondition and postcondition of your subclass can differ from those on the parent class, but only taking into account the following. The method in the subclass can:

  • weaken the precondition and,
  • strengthen the postcondition.

Both conditions must be satisfied. Weakening the precondition means that the subclass method requires less (or the same) from the client (the caller) than what the supertype method does. This ensures that the client, written in terms of the supertype precondition, will continue be able to execute the method, if a subclass is used instead. Strengthening the postcondition means that when the method returns, the postcondition of the parent class is satisfied and also additional conditions might be added. In the case of our previous example, the precondition on the add(anInteger) is the same for both the subclass and the parent class, but the postcondition has been modified by the subclass method, not preserving the one on the parent class.

The next example, presents a CounterPairList class, which is a subclass of List, and also a subtype. See how the postcondition of the overriden add(anInteger) method preserves the parent type postcondition while also add an additional condition.

class CounterPairList, inheriting from: List {
  counter = 0;

  # pre:  -
  # post: adds anInteger to this, verify if a pair number was added to keep track
  add(Integer anInteger) {
    super.add(anInteger);
    if (anInteger % 2 == 0) {
      counter++;
    }
  }

  # pre:  -
  # post: returns the size of pair intergers in the List
  Integer sizeOfPairNumbers() {
    return counter;
  }
}

As an exercise, think about how you should override the Java Object#equals(anObject) method in order to preserve the subtype properties. Tell me…

Ways of Inheritance

There are several ways in which inheritance can be used, here is a list of some of them which I have used and I have seen other using them too:

  • For code reuse: Like we did in our first example, creating a Set of Integers, inheriting from List of Integers. Of course, avoid the duplication of code is a desirable thing, but there are other ways to do it, like composition. This is not good if you cannot preserve the features of the parent class.
  • For specialization: Like we did for our CounterPairList example, occurs when the subclass is a specialized form of the parent class, preserving the features of the parent class. Other typical example is when we have Cow as a specialized form of Mammal. This is considered a good way of inheritance as subclasses are subtypes.
  • For specification: This is where the subclasses implements behavior specified in the parent. Like the Java interface syntactical construction (or C++ pure virtual functions) or the abstract syntactical construction where some behaviour is implemented in the parent and there are some other bits deferred to the subclasses. This is considered also a good way of inheritance, considering that the subclasses will be also subtypes. I’m making this last comment as you can always define, for instance, the specification (in a Java interface) of the Stack#push and Stack#pop methods, and then implement them like they were a simple Set.
  • For limitation: This is where the behaviour of the subclass is smaller than the behaviour of the parent class. I have seen this way several times. When some methods of a class fits for you, so, you inherit from it. Other methods that you are inheriting should not be used, so you override those methods in order to inform the client that those are not available (throwing an exception with a message: “Not available for instances of this subclass…”). Don’t do this.
  • For naming a thing: Sometimes you just creates a subclass as a way to name or model something you see on your real world, but that subclass does not have behaviour at all. Just a constructor. Don’t do this. A class without behaviour is not really an object. You can find a great example of this on the book Test-Driven Development By Example, from Kent Beck. There you will see how and why the classes Dollar and Franc which are subclasses of Money end up disappearing.

Few Additional Comments on Inheritance

  • Inheritance is highly (and improperly) used for code reuse instead of composition in languages like Java because you have to write much less code. Composition requires you to write more code, for inject the collaborator in the constructor, and delegate on those methods you want to reuse. However, this is a lack of the language. For instance, C++ provides a syntactical construction called private inheritance where you can inherit all the facilities of the parent class, but those are not available for clients nor the subclass can be substituted when the parent class is expected.
  • Even when your subclass is a subtype of the parent class, the parent class might change their pre/postcondition in a subsequent release, which might break the subtype relation. Or the parent class might get an additional method and accidentally there is a method in the subclass with the same signature, then you end up overriding the method without knowing it. Subclasses and their parent classes need to evolve together.
  • Inheritance is a “white box” code reuse style, as you need to know how some methods are implemented to safely create subclasses. The implementation of methodA might call methodB and methodC from the same class, to fulfill an action. In the subclass, you might override methodB and get unexpected results when calling methodA. Inheritance breaks encapsulation.

Use inheritance properly, when subclasses are also subtypes. If you cannot guarantee that, use composition. And even when using inheritance properly keep an eye on how the inheritance hierarchy evolves to avoid surprises.