Inherit Wisely

Inheritance is often considered at the heart of object-oriented design. We've learnt in time how wrong we were, and how to use it wisely.

Grady Booch in his book Object-Oriented Analysis and Design defines Object-Oriented Programming as follows.

Object-oriented programming is a method of implementation in which programs are organized as cooperative collections of objects, each of which represents an instance of some class, and whose classes are all members of a hierarchy of classes united via inheritance relationships.

He then says :

Inheritance is the most important “is a” hierarchy, and as we noted earlier, it is an essential element of object-oriented systems. Basically, inheritance defines a relationship among classes, wherein one class shares the structure or behavior defined in one or more classes (denoting single inheritance and multiple inheritance, respectively).

Inheritance thus represents a hierarchy of abstractions, in which a subclass inherits from one or more superclasses. Typically, a subclass augments or redefines the existing structure and behavior of its superclasses.

With all due respect to the father of UML, I genuinely believe he was wrong to this regard.

Inheritance breaks encapsulation

We have a big problem here.

As we stated earlier, the most cherished value of object is encapsulation.

Objects have the very deep and profound desire to keep their treasure for themselves and protect it from the world.

But what can happen when a class inherits another ?

Let's take a simple example.

class Queue {
    private array: number[] = [];
    function add(value: number) {
        this.array.push(value);
    }
    
    function addAll(values: number[]) {
        values.forEach(value => {
            this.array.push(value);
        }
    }
}       

The Queue class represents... a queue. Nothing of importance.

Let's say we suddenly want to count the number of elements added. Consider the class out of reach and that the only way for us to add this feature is to extend it.

class CountingQueue extends Queue {
    private count: number = 0;
    
    function add(value: number) {
        this.count++;
        super.add(value);
    }
    
    function addAll(values: number[]) {
        this.count += values.length;
        super.addAll(values);
    }
}

It works.

But one day the implementor of Queue decides to make a subtle change.

class Queue {
    private array: number[] = [];
    function add(value: number) {
        this.array.push(value);
    }
    
    function addAll(values: number[]) {
        values.forEach(value => {
            this.add(value); // <---- line changed !
        }
    }
}       

Suddenly, the parent class changes the implementation of addAll to uses add.

And now the CountingQueue class is broken and counts values twice when calling addAll.

How did we come to this ? We inherited from the Queue class, decorated its methods but our logic was dependant on the internal implementation of the Queue class. In other words, we knew exactly what Queue was doing behind the curtains and considered safe to take this behavior for granted.

Now, Queue is tied to this implementation. It isn't free to move as it wants, because someone learnt their secret and threatened to tell everyone about it. So Queue now has to comply.

That's not how we behave in a society.

So indeed, as Grady Booch says, inheritance is the tightest of coupling. You can't have more coupling than that.

This coupling is not symmetrical. When a subclass inherits a superclass, the subclass becomes dependant on the superclass and must conform to its superclass. It has no choice but to carry the burden.

But in this case, we rebalanced the coupling in a terrible way. Not only the Queue superclass knows naught of the CountingQueue subclass, but it must refrain from changing the way it operates because some unknown subclass is counting on that specific implementation.

That's not poor design.

That's the bottom of the pit. We can't go further down.

But this problem is well known and is referred to as Inheritance Breaks Encapsulation.

Inheritance creates a tight coupling between base and derived classes. Whether or not that is good or bad depends on the nature of what you are trying to do; there are cases where delegation might be more appropriate, and there are cases where inheritance is GoodEnough.

So how can we fix the code above ?

Short answer : we really can't. Queue wasn't designed to allow anyone to peek into how it adds elements. That's a design consideration, and one way we could have done it using inheritance is as follows.

class Queue {
    private array: number[] = [];
    
    function add(value: number) {
        this.array.push(value);
        this.onAdd(value);
    }
    
    function addAll(values: number[]) {
        values.forEach(value => {
            this.array.push(value);
            this.onAdd(value);
        }
    }
    
    protected onAdd(value: number) {
        // To be used by subclasses
    }
}       

class CountingQueue extends Queue {
    private count: number = 0;
    
    // @override
    protected onAdd(value: number) {
        this.count++;
    }
}

Our Queue class was designed with Observability in mind. It opens a hook and declare itself ready to notify anyone interested in knowing when a new value is added onto the queue. It's actually an ObservableQueue.

Delegation

Well, this design is a little bit fishy.

Our Queue class declares an onAdd method with no body. We can add a comment stating the method exists for subclasses to use it, but that's weird.

Or we could declare Queue abstract.

abstract class Queue {
    private array: number[] = [];
    
    // methods...
    
    abstract onAdd(value: number);
}       

class CountingQueue extends Queue {
    private count: number = 0;
    
    // @override
    protected onAdd(value: number) {
        this.count++;
    }
}

It works, and may sometimes be the solution. However, that class-based observability is poor. It forces us to make Queue an abstract class. That's a problem, because it makes perfect sense to instantiate a queue by itself. Queue is a terrible candidate for an abstract class.

We can further enhance the design.

abstract class BaseQueue {
    private array: number[] = [];
    
    function add(value: number) {
        this.array.push(value);
        this.onAdd(value);
    }
    
    function addAll(values: number[]) {
        values.forEach(value => {
            this.array.push(value);
            this.onAdd(value);
        }
    }
    
    abstract onAdd(value: number);
}       

class Queue extends AbstractQueue {
    function onAdd(value: number) {
        // do nothing
    }
}

class CountingQueue extends Queue {
    private count: number = 0;
    
    // @override
    protected function onAdd(value: number) {
        this.count++;
    }
}

We're going around in circles.

Honestly, there's no way out of this nightmare.

So how can we elegantly fix this problem ?

You may have guessed : by using delegation instead.

interface IEventListener {
    onAdd(value: number)
}

class Queue {
    private listener: IEventListener;
    private array: number[] = [];
    
    function add(value: number) {
        this.array.push(value);
        this.listener?.onAdd(value);
    }
    
    function addAll(values: number[]) {
        values.forEach(value => {
            this.array.push(value);
            this.listener?.onAdd(value);
        }
    }

    function setEventListener(listener: IEventListener) {
        this.listener = listener;
    }
}     

class Counter implements IEventListener {
    private count: number = 0;
    
    onAdd(value: number) {
        this.count++;
    }
}

We moved from class-based observability to object-based observability. We removed the property from the class hierarchy and moved it into objects.

Some patterns can either be class-based or object-based.

For example, the Strategy Pattern is the object-based equivalent of the Template Method Pattern, which is a class-based solution.

The Adapter Pattern can either be in a class-based fashion or an object-based fashion.

The object-based solutions are always more flexible because they can change at runtime and are more in the object-oriented spirit of things. Class-based solutions are static : once a class inherits another, there's no way back.

Thus, any object can be notified of events as long as they implement the onAdd method. Any object.

Interface & Implementation Inheritance

We distinguish two forms of inheritance :

  • Implementation Inheritance : when a class inherits another class's implementation of some or all its methods.

  • Interface Inheritance : when a class inherits a completely abstract class filled with abstract methods, or uses a special construct called interfaces.

As we saw, implementation inheritance often leads to all sort of problems, and the solution often is delegation. That's not to reject implementation inheritance entirely, but one must use it very carefully and usually avoid it.

Yet, the other form of inheritance consist of inheriting an interface with no implementation. This interface serves as a contract and is imposed to the class implementing it.

In most language, two different constructs exist because a class can implement many interfaces, but can't inherit multiple classes. Inheriting multiple classes leads to many implementation problems, such as the Deadly Diamond of Death & the performance overhead of runtime checking for the correct implementation.

Inheriting a contract is a very powerful mechanism. In fact, we just saw an example.

interface IEventListener {
    onAdd(value: number)
}

class Queue {
    private listener: IEventListener;  
    // methods
}     

class Counter implements IEventListener {
    private count: number = 0;
    
    onAdd(value: number) {
        this.count++;
    }
}

The interface is an asymmetrical contract between two objects :

  • The dependant : the class using that interface. Here, it's Queue. The interface exists solely for the dependant to use it. We need to create an interface for Queue to call the onAdd method, and the onAdd methods obeys the semantic of the Queue object. In other words, that interface's sole purpose is to serve Queue, its master.

  • The implementor : the class implementing the interface. Here, it's Counter. The class Counter doesn't need that interface. As far as it is concerned, it just exists by itself and can well have a meaning of its own. But the interface is imposed on it. It has no other choice but to do its part of the bargain. If the implementor doesn't implement the method, the compiler will act as an executor and will severely punish the class for trespassing the law.

See how asymmetrical that relation is ? Queue needs it but has no responsibility toward that interface. Counter doesn't need it, but is forced to implement its method.

That's why interfaces are such a powerful construct : they force clients of the interface to respect the law. The contract cannot be violated or revoked. And every other class who wants to depend on the contract can rest assured that the contract will be honoured.

It's very powerful. It allows objects to communicate to one another with as little coupling as possible. An object can contain dozen of methods and implement an interface of a single method, and another object can depend on that interface and depends on only one of these twelve methods.

Moreover, interfaces enable polymorphism in a very safe way. Many objects can implement a specific interface. As far as the dependant is concerned, any object will do. They just need to implement that interface.

It's a terrific mechanism to reduce coupling.

When Concrete Inheritance makes sense

Favor composition over inheritance means favor composition over inheritance.

And not "inheritance is terrible" or "never ever use inheritance".

There's actually some cases where inheritance is fine, or should I say the best possible way to link two classes together.

Let's consider a Collection class.

class Collection<T> {
    public add(element: T): void;
    public remove(element: T): void;
    public get(index: number): T;
    public size(): number;
}

This very simple class represents a collection. It's very generic in nature and can handle any type of element. Hence, the Collection itself is parametrically polymorphic.

Now, let's say we want to have a collection of users and need a method to find all users of a specific age. Such code would look like this.

const users = new Collection<User>();
const legalUsers = [];

for (let i = 0; i < users.size(); i++) {
    if (users.get(i).isLegal()) {
        legalUsers.push(users.get(i));
    }
}       

This code is awful for two reasons :

  • It screams procedural thinking, it can't be more imperative than that

  • It exhibits logic that should be encapsulated into a class

The first problem can be fixed in many ways. The mixed OO/FP approach is to use a filter. The pure OO is to use an Iterator.

But let's put that problem aside for now, because the second is even worse.

This bit of code will probably be part of another object's code who deals with users. It's part of a bigger Procedural Object such as an Application Services. And it will pollute that code with low-level details.

class CommandHandler {
    public execute(command: Command) {
        const users = this.usersRepository.fetch();
        const legalUsers = [];

        for (let i = 0; i < users.size(); i++) {
            if (users.get(i).isLegal()) {
                legalUsers.push(users.get(i));
            }
        }
        
        // More code to handle the use case    
    }
}

Once again, the astute reader may wonder why we don't directly fetch legal users from the source instead.

That's a good point, reader. You're paying attention.

However, that remark will be discarded in the context of this example.

Thanks you very much.

Any Clean Coder out there would extract that bit of logic into a method of the class, and in fact that's what I used to do.

class CommandHandler {
    public execute(command: Command) {
        const users = this.usersRepository.fetch();
        const legalUsers = this.getLegalUsers(users);
        
        // More code to handle the use case    
    }
    
    // Extracted
    public getLegalUsers(users): User[] {
        const legalUsers = [];

        for (let i = 0; i < users.size(); i++) {
            if (users.get(i).isLegal()) {
                legalUsers.push(users.get(i));
            }
        }
        
        return legalUsers;
    }
}

It's clean, it works, most people would call it a day.

But I don't call it a day. It's awfully procedural. We only abstracted away some logic into a function. That's what the best procedural programmers would do.

So, what are we gonna do ? A solution would be to wrap that code into a Compute Object.

class CommandHandler {
    public execute(command: Command) {
        const users = this.usersRepository.fetch();
        const legalUsers = new LegalUsers(users).get()
        
        // More code to handle the use case    
    }
}

class LegalUsers {
    constructor(private readonly users: Collection<User>) {}
    
    get() {
        const legalUsers = [];
        for (let i = 0; i < this.users.size(); i++) {
            if (this.users.get(i).isLegal()) {
                legalUsers.push(this.users.get(i));
            }
        }
        
        return legalUsers;
    }
}

It's much better. Now, we have an object encapsulating the logic itself, abstracted away from our sight and from the command handler.

That code is more object. But it's still not perfectly object.

Because Compute Objectare the compensation of a deep flaw. Because we failed to write proper object code, we found a halfway solution between OO and procedural code. It's still dirty, but at least it tries very hard to be an object. And every effort is welcome.

Why, you say ? Because as the name suggests, it's a Computation, not a service. It's not alive, it just wraps some calculus.

Don't get me wrong, Compute Objects are perfectly fine solutions when there's nothing better at hand. But when there's a better alternative, always strive for the best.

Can we do better ? Of course we can. Inheritance's been waiting for this moment all its life.

class UsersCollection extends Collection<User> {
    legalUsers() {
        const legalUsers = [];
        for (let i = 0; i < this.size(); i++) {
            if (this.get(i).isLegal()) {
                legalUsers.push(this.get(i));
            }
        }
        
        return legalUsers;
    }
}

And now.

class CommandHandler {
    public execute(command: Command) {
        const users = this.usersRepository.fetch();
        const legalUsers = users.legalUsers();
        
        // More code to handle the use case    
    }
}

We achieved perfection while using concrete inheritance. Is that black magic ?

Let's analyze this code and see why it makes perfect sense.

The model is not the reality

Inheritance is defined as a is-a relationship. A Person is a Mammal, a Taxi is a Car, and a Politician is an Animal.

It's the strongest form of coupling, because there's no way denying that relation. It's deeply ingrained into the subclass.

However, it's sometimes difficult to make sense of inheritance, because our code is not the reality but simply a model of the reality.

Take a Person for example. They have a name, hair color, eye color, possess objects, have a family, an history, and know all the events that happened to them yesterday. That's a lot of informations, right ?

But in our code, we may only be interested in the Person's e-mail address and name. That's our model. It's not the reality, just a small (very, very small) part of it.

And if a Person is indeed a Mammal, a Person class may not necessarily be a subtype of a Mammal class. It depends on the model, not the reality.

For inheritance to make sense, the model of the Person must be considered a subtype of Mammal. As Bertrand Meyer says in his book Object-Oriented Software Construction, there's no hard rule : it's contextual, it depends.

Does it make sense, in the context of the code, to say that a Person is a Mammal ?

Maybe. Sometimes the answer is yes, and sometimes the answer is no.

In our code, does it make sense to say that a UsersCollection is a Collection ? I don't see any reasonable argument against it.

It's very hard not to convince yourself that a UsersCollection is not a Collection.

Heck, it's in the name !

Extensions Only

What's more, our code doesn't even assume anything from its parent class besides its public contract.

It simply assume the size to be correct and the get method to return a User at a specific index. That's part of Collection's bargain and there's nothing Collection can do about it.

Hence, we didn't violate any rule. The parent class is free to change its implementation details because our UserCollections class doesn't depend on these internals and assume nothing.

A big reason is that we didn't override and rewrite any method of the parent class. We just extended it with more features to fit our needs. After all, a UsersCollection isn't just a Collection of users. It's a Collection of users with behavior specific to users.

That's what extensibility is all about.

Composability

We just talked about how we can extend a class to add specific functionalities to it.

But sometimes, we'd also like to reuse that functionality.

Let's take our trusty Collections class and some Observabilityto it.

class Collection<T> {
    public add(element: T): void;
    public remove(element: T): void;
    public get(index: number): T;
    public size(): number;
    
    public addEventListener(listener: IListener);
    public removeEventListener(listener: IListener);
}

But Observability is a recurring capability. We may need that very same capability elsewhere.

class Dictionnary<K, T> {
    public set(key: K, value: T): void;
    public get(key: K): void;
    public remove(key: K): T;
    public size(): number;
    
    public addEventListener(listener: IListener);
    public removeEventListener(listener: IListener);
}

It would be good to extract that capability and recycle it for every class who need observability.

abstract class Observable {
    public addEventListener(listener: IListener);
    public removeEventListener(listener: IListener);
}

class Collection<T> extends Observable {
    public add(element: T): void;
    public remove(element: T): void;
    public get(index: number): T;
    public size(): number;
}

class Dictionnary<K, T> extends Observable {
    public set(key: K, value: T): void;
    public get(key: K): void;
    public remove(key: K): T;
    public size(): number;
}

Does it make sense ? Absolutely not.

Can you convince yourself that a Dictionnary is an Observable ? I can hardly. I can't imaging two Observable together raising their child, a Dictionnary. It doesn't hold water.

Because a Dictionnary is not an Observable, but a Dictionnary is Observable. It can be Observed. It's a property of the Dictionnary, not something inherent to the Dictionnary.

After all, can a non-observable Dictionnary exist ? Of course.

What's more problematic : since most languages allow for single inheritance only, we just used our single inheritance slot. Our Dictionnary cannot extends anything else.

What if Dictionnary is a subclass of AbstractDictionnary ? We may, for example, need some default behavior or implement a TemplateMethod somewhere along the hierarchy of classes.

Our only way is the following.

abstract class Observable {
    public addEventListener(listener: IListener);
    public removeEventListener(listener: IListener);
}

abstract class BaseDictionnary extends Observable {
    // some base methods
}

class Dictionnary<K, T> extends BaseDictionnary {
    public set(key: K, value: T): void;
    public get(key: K): void;
    public remove(key: K): T;
    public size(): number;
}

Is BaseDictionnary an Observable ? No. Of course not.

So we may remember the wise words of the gang of four : favor composition over inheritance.

class Observable {
    public addEventListener(listener: IListener);
    public removeEventListener(listener: IListener);
}

class Dictionnary<K, T> extends BaseDictionnary {
    private observable: Observable;
    
    public addEventListener(listener: IListener) {
        this.observable.addEventListener(listener);
    }
    
    public removeEventListener(listener: IListener) {
        this.observable.removeEventListener(listener);
    }
    
    public set(key: K, value: T): void;
    public get(key: K): void;
    public remove(key: K): T;
    public size(): number;
}

That's much better. We delegated the mechanism of observability to the Observable class.

It's called delegation through the means of method forwarding. That's a form a composition.

All we had to do was sacrifice our dignity and write the same methods twice.

I'm talking of these.

public addEventListener(listener: IListener) {
    this.observable.addEventListener(listener);
}

public removeEventListener(listener: IListener) {
    this.observable.removeEventListener(listener);
}

That's pure nonsense. No one in their right mind would find this elegant.

And that's the biggest problem with many OO languages : their only means of clean composability is through inheritance. But inheritance wasn't built with composability in mind.

In TypeScript, the language I'm using for this book, it's impossible. And it kills me to admit that PHP did something good when they introduced traits.

My only dream is for TypeScript to look like this one day.

export trait Observable {
    public addEventListener(listener: IListener);
    public removeEventListener(listener: IListener);
}

class Dictionnary<K, T> extends BaseDictionnary {
    use Observable;
    
    public set(key: K, value: T): void;
    public get(key: K): void;
    public remove(key: K): T;
    public size(): number;
}

This is the most elegant, reusable and clean form of OO composability. It's not ambiguous with classes since it's just a fragment of a class. A trait by itself cannot be instantiated.

And now, not only do we have proper composition, but we can insert a Dictionnary everywhere a method expects an Observable. Because the Dictionnary has the features of an Observable and, from the point of view of that method, implements the contract of an Observable.

And I believe that's the best solution to the interfaces considered harmful problem.

Turns out TypeScript has a mixin mechanism.

Well, it's not a mechanism. It's a trick that uses inheritance to perform a weak and awful form of mixin. I really don't like it. It makes me very uncomfortable.

Inheritance is a really bad tool for this. It should be a language construction.

Rules of Inheritance

We just saw how inheritance forces us into class-based patterns instead of object-based patterns, and tend to be less flexible.

We saw how harmful implementation inheritance can be, and how it breaks encapsulation, the most cherished value of our dear objects.

Thus, we derive the following rules.

  • Interfaces can extend Interfaces

  • Abstract classes can implement Interfaces

  • Abstract classes can extend Abstract classes

  • Concrete classes can implement Interfaces

  • Concrete classes can extend Abstract classes

  • Concrete classes may sometimes but rarely extend Concrete classes

In subclasses :

  • Do not ever rewrite an existing method

  • Do not ever cancel an existing method (by throwing or by returning immediately)

  • Only implement abstract methods, or methods made to be overwritten

  • Only add new methods (Extensions Only)

That's it.

Last updated