OOP Inheritance: What, when and why

0

Intro

Let’s start with some notes that set a baseline for the discussion. We have two types of inheritance. Interfaces and parent classes. An interface is similar to a parent class, but does not provide any implementation. The child class must override all methods. A middle ground is an abstract class where some methods are defined, and some (abstract) are left for future definition by the child class. An interface is like an abstract class in which all methods are abstract.

Interfaces are used when we want to say what our class is capable of (what behaviour is offering). Parent classes express what kind of concept the child class represents and provide some common implementation with it. Using interfaces allow us to say that “I need to use a object capable of doing this” instead of “I need to use an object of this class (because this class is capable of doing this)”.
Inheritance: The purpose of inheritance is twofold and includes both semantics and mechanics.

Inheritance captures mechanics by encoding the representation of the data and behavior of a class and making it available for reuse and extension in subclasses.

Inheritance captures semantics (meaning) in a classification hierarchy, arranging concepts from generalized to specialized. If the mechanics are reuse of code/behaviour, then the semantics are reuse of concept.

The semantics-related aspect of inheritance is very important. Ignorance of this aspect can lead to bad architectural decisions, bad design. A good example is the well-known “is-a” rule-of-thumb. It is one of the earliest-to-be-taught rules to amateur programmers that helps them decide whether a concept is an object-oriented child of another concept. But it’s a tricky rule that should not be taken lightly! Let’s try to think fast. Circle is an ellipse. Is it ? Of course! Anyone knows that. Can a circle be used in every case that an ellipse is expected? …hmm, no? Of course, no! The behaviour of a circle does not contain all of the behaviour an ellipse has. And it should not! The circle does not encapsulate all the semantics that are expressed through an ellipse. Only a part of that! We will come back to circle-ellipse relation, again, a bit later. That’s why when overriding a method, it is not only the mechanics that needs to be maintained (preserve the method signature) but also the semantics. The new method should be designated to implement the same type of work that was originally intended and for the same reason. Semantics!

Having said all these, we can also make the distinction between a subclass (refers to the mechanics – the subclass is defined through inheritance) and a subtype (refers to the semantics). A subtype, defined as such at a higher level of domain modeling, needs not to end up as a child class necessarily.

From the semantic purpose of inheritance, it becomes clear that a child class should belong to the same application-domain as the parent class and, more precise, be a specialization of the parent class. After all, the Liskov-Substitution principle tries to tell us the same thing: that we should preserve semantic interoperability of types in a hierarchy. In simple words, an object of the subclass should be able to substitute an object of the parent class and handle without a problem all cases that the later could handle.

So, to inject code that is not (in nature) heavily tied to class’ behaviour, use other means like traits, mixins or composition.

Guidelines

This is a set of guidelines on when inheritance should be used:

  • Both classes are in the same logical domain
  • The subclass is a proper subtype of the superclass
  • The superclass’s implementation is necessary or appropriate for the subclass
  • The enhancements made by the subclass are primarily additive.

The first two points deal more with the semantics of the class hierarchy and they heavily reflect the Liskov-Substitution principle. The semantics of the subclass should (at least) the same of the parent class. The former should be able to represent the later in all cases. The last two points are closely related to the Interface Segregation principle. The say that a subclass should no be forced to implement redundant behaviour. A sign for that is when the subclass tries to remove/hide/mask some of the behaviour of the parent class. Blindly focusing on the “code reuse” aspect of inheritance can lead to flawed designs.

You may now wonder why there is so much fuss about the problems of inheritance and what about this “favor composition over inheritance” mantra. The reason is that people who don’t understand inheritance tend to misuse or overuse it. Bad design is not a reason to blame inheritance. What is more, real use of concrete inheritance (let’s leave interfaces and abstract classes aside for a moment) is not so often in real life. The reason is mostly used in:

  • Higher-level domain modeling
  • Frameworks and framework extensions
  • Differential programming

If you’re not doing any of these things, you probably won’t need class inheritance very often. Higher-level domain modeling is about grouping related sets of concepts and in general organizing the names and concepts that describe the domain. Framework design is about writing code related the higher-level ans most basic functionality of our system. But be aware! As we go deeper into the implementation of our design, we may find that our original generalizations about the domain concepts, captured in our inheritance hierarchies, are beginning to shred. We should not be afraid to transform inheritance hierarchies into complementary cooperating interfaces and components when the code leads you in that direction.

One of the most common use of inheritance is for differential programming. We need a widget that is just like the existing Widget class, but with a few tweaks and enhancements. In this case, inherit away; it is appropriate because our subclass is still a widget, we want to reuse the entire interface and implementation from the superclass, and our changes are primarily additive. If you find that your subclass is removing things provided by the superclass, question inheriting from that superclass.

Bad use

Let’s have a look at a few examples that where some may say that point out the problematic nature of inheritance and see why, on the contrary, they are examples of bad design.

The Circle-Ellipse problem:

Say you’re implementing a graphical editor and you have an Ellipse class, which has methods to set the width and height. Now you want to add a circle class. Of course, geometrically, every circle is-a ellipse, so you make Circle a subclass of Ellipse. That means Circle also needs to implement or inherit methods to set the width and height. But if you separately set the width or height, it won’t be a circle anymore. So, we have methods that the child class is forced to implement and these method are not only redundant but also totally irrelevant to the semantics of the child class. The child class is not a proper subtype of the parent class. It cannot represent the parent class in all cases. This inheritance example violates both the Liskov Substitution Principle and the Interface Segregation Principle. Bad design!

The Stack-ArrayList design:

Let’s start with a simple and extremely common example of misusing inheritance:

class Stack extends ArrayList
{
    public void push(Object value){}
    
    public Object pop(){}
}

There is a number of problem with this design:

  • It violates the Liskov-Substitution principle. Implementing a Stack by inheriting from ArrayList is a cross-domain relationship: ArrayList is a randomly-accessible Collection; Stack is a queuing concept, with specifically restricted (non-random) access. These are different modeling domains.A stack cannot replace ArrayList and handle all the cases an ArrayList can handle.
  • Mechanically, inheriting from ArrayList violates encapsulation; using ArrayList to hold the stack’s object collection is an implementation choice that should be hidden from consumers.
  • In a way, it violates the Interface Segregation principle. The parent class forces the subclass to (publicly) provide methods that are not needed (are outside of the class’ purpose). This class will function as a Stack, but its interface is fatally bloated. The public interface of this class is not just push and pop, as one would expect from a class named Stack, it also includes get, set, add, remove, clear, and a bunch of other messages inherited from ArrayList that are inappropriate for a Stack.

The PriorityQueue – Simulator design:

This is another bad design story that I have heard from another developer. It’s about designing a simulator. The simulator needs a queue of events, each scheduled for some time, where new events can be scheduled for any future time, and at each step in the simulation you need to remove the event scheduled for the soonest time. A priority queue lokked like the perfect way to implement that, so the Simulation class derived from a PriorityQueue class. But when the Simulation class needed methods scheduleEvent and nextEvent, while the PriorityQueue needed enqueue and dequeue. The thing about inheritance is that every method of the base class is available in the derived class, whether appropriate or not. You can override it, but you can’t eliminate it. This is why the Liskov substitution principle is so important: it should be possible to use an instance of any derived class anywhere an instance of the base class could be used. It makes no sense to use a Simulation where a PriorityQueue is expected.

The “Fragile base class” problem

The fragile base class problem is a fundamental architectural problem of object-oriented programming systems where base classes (superclasses) are considered “fragile” because seemingly safe modifications to a base class, when inherited by the derived classes, may cause the derived classes to malfunction. The programmer cannot determine whether a base class change is safe simply by examining in isolation the methods of the base class.

Example:

The following trivial example is written in the Java programming language and shows how a seemingly safe modification of a base class can cause an inheriting subclass to malfunction by entering an infinite recursion which will result in a stack overflow.

class Super {

    private int counter = 0;
    
    void inc1() {
        counter++;
    }
    
    void inc2() {
        counter++;
    }

}

class Sub extends Super {

    @Override
        void inc2() {
        inc1();
    }

}

Calling the dynamically bound method inc2() on an instance of Sub will correctly increase the field counter by one. If however the code of the superclass is changed in the following way:

class Super {

    private int counter = 0;
    
    void inc1() {
        inc2();
    }
    
    void inc2() {
        counter++;
    }
}

a call to the dynamically bound method inc2() on an instance of Sub will cause an infinite recursion between itself and the method inc1() of the super-class and eventually cause a stack overflow.

The need for careful design: The Fragile Base class problem may be seen as an inherent weakness of inheritance or as a result of a bad design. I would prefer the second. Let’s see what is happening here. We are providing two public methods to the class user. On one hand, we are “telling” the user of the class that he can override them and, on the other hand, we are building the functionality of the one method to the implementation of the other. Just with the description someone can understand that we are doing something bad! There are multiple ways to fix this basd design:

From a superclass’ perspective:

(a) Don’t let the class user override the public methods (all of them or just the ones that are consist dependencies). For example, declare the methods we depend on as “final” (Java , C++), or “sealed” (C# , VB.NET).

(b) Don’t create interdependencies between inherited methods. That means, super-class methods should avoid making calls to dynamically-bound methods.

(c) Make sure that the dependent methods will make calls to the right method. For example, use the self:: in PHP.

From a child class’ perspective:

(d) Do not override methods from third-party libraries.

(e) Do not allow overriden methods to call inherited methods.

Abstract classes

Special attention should be paid here. Abstract methods are created in order to overriden, so (a) and (c) are not applicable by definition. (b) doesn’t make much sense, too, for abstract methods. (d) refers to a case that should not be expected under normal conditions.

So, our only resort is (e). If we think about it, it makes sense. The abstract method is part of some business logic that is being described on a high-level by the abstract class. Some of the details of this business logic are being delegated to abstract methods because they are going to diverge a bit from one derived class to another. These abstract methods are going to be called by these methods that contain the high-level description of the business logic. So, there is no meaning into making the implementation of the abstract method to call back the methods that calls it. Since being in the derived class we don’t know which method calls the abstract implementation, we’d better not call any of the inherited methods.

Someone may ask “and what about methods that are called by the abstract implementation ?”. These methods work on a lower level that the abstract implementation and their existence is tightly coupled with the business logic of the abstract implementation. Even if some of them are common for all the abstract implementations, they should remain in the same class as the abstract implementation. It not only a matter of easier maintenance and readability but also it enforces better the semantics of a OOP class hierarchy (going down the hierarchy, we are going from more generic concepts to less generic ones without mixing the abstraction levels contained within the same class).