Composition Over Inheritance in Object Oriented Design

Composition Over Inheritance banner

As you grow in your career as a Software Engineer, there will come a time when you are interested in more than just writing codes that solve problems. You are also worried about the way your code is structured, how easy it will be to make modifications to that complex codebase in the future without breaking existing features, and how easy a different programmer/software engineer will understand your thought process by the mere fact that they looked at the code you have written (you don’t want another engineer cursing you out every 2 minutes as they go through the shitty piece of spaghetti you wrote six months ago that they now have the unfortunate privilege to maintain).

According to the book Dive into Design Patterns by Alexander Shvets one of the elements of good software design principles is: “Favour Object Composition over Inheritance”.

To understand this principle in-depth, we have to define some terms and how they relate to this principle.

Object-Oriented Programming: Object-Oriented Programming or OOP for short is a programming paradigm based on using Objects to represent data and behaviours related to the data. These objects are constructed from a set of “blueprints” called classes.

Objects are representations of real-world attributes called properties or members and behaviours called methods. It is typically created from a set of “blueprints” called classes in other words objects are instances of classes.

In order to understand classes and objects beyond technical definition let’s look at an analogy of the terms involved.

Think of a building plan. In a typical building plan, it is expected that certain components be present, some of these components could include: a list of materials like sand, cement, plaster of Paris, etc. It would also include the cost of these materials. A building plan would also include the floor plan, site plan, land size, etc.

Building plans could contain generic information regarding houses that would be constructed, they could also be extended to contain information for specialised houses or custom-designed houses.

The building plans are the blueprints through which one or more houses would be constructed. This is what classes are to objects. Objects, on the other hand, are the different houses that would be constructed from the blueprint (building plan).

Inheritance: is one of the four pillars of object-oriented programming. It describes the ability of classes to be built on top of existing classes. One of the major benefits of inheritance is code reuse.

Inheritance allows developers to create slightly different classes from existing ones by “extending” the functionality of an existing class. The Inheritance relationship is considered an “is-a” relationship. For Example, an Admin user is a User, a Guest User is a User and more of such relationship.

Let’s say you have an existing User class with methods such as login and signup that handle registration and login for regular kinds of users. A new feature request was made available by the product manager to provide a mechanism for different types of users to register and log in. These types of users are tagged as administrators, and they would need to provide an extra set of credentials before they could be authenticated.

You could modify the existing User class to accommodate this set of users, but what happens when you are asked to make provision for three (3) more categories of users? You could be tempted to modify the existing user class until you can no longer make modifications. That is not all; based on all the modifications you made, there is a high chance that the logic for handling a certain category of users would have broken or had a bug introduced.

A way to solve this problem is by using Inheritance. You can extend the existing User class for each category of users that is required, and by doing so, you would adhere to the Single Responsibility Principle, which is the first requirement of the SOLID principles. Classes built this way are also easier to maintain in the long run.

class User {
  constructor(
    public firstName: string,
    public lastName: string,
    public userName: string,
    public email: string,
    public password: string,
  ) {}
  signUp() {
    // Sign Up Logic goes here
    console.log(`User ${this.firstName} - ${this.lastName} registered successfully.`);
  }
  login() {
    // Login logic goes here
    console.log(`User ${this.userName} logged in successfully.`);
  }
}

// User class after modidication to  accomodate Admin User

class User {
  constructor(
    public firstName: string,
    public lastName: string,
    public userName: string,
    public email: string,
    public password: string,
    public adminKey?: string,
    public type?: "Regular" | "Super",
  ) {}
  signUp() {
    // Sign Up Logic goes here
    if (this.type) {
      // do something admin related
      console.log(
        `Admin User ${this.firstName} - ${this.lastName} role ${this.type} registered successfully.`,
      );
    } else {
      console.log(`User ${this.firstName} - ${this.lastName} registered successfully.`);
    }
  }
  login() {
    // Login logic goes here
    if (this.adminKey) {
      console.log(`Admin User ${this.userName} logged in successfully.`);
    } else {
      console.log(`User ${this.userName} logged in successfully.`);
    }
  }
}

// Modification using Inheritance

class Administrator extends User {
  constructor(
    public firstName: string,
    public lastName: string,
    public userName: string,
    public email: string,
    public password: string,
    public adminKey: string,
    public type?: "Regular" | "Super",
  ) {
    super(firstName, lastName, userName, email, password);
  }

  login(): void {
    // Additional logic for administrator login
    if (!this.adminKey || !this.type) {
      console.log("Invalid administrator key. Login failed.");
    } else {
      console.log(`Administrator ${this.userName} logged in successfully.`);
    }
  }
}

Composition: is a type of relationship that exists between objects where one or more objects, say Object1, Object2, Object3 to ObjectN exist as part of another object, say ObjectA. We typically say that ObjectA is composed of multiple objects, which are Object1, Object2, Object3 to ObjectN. The type of relationship that exists in Object composition is called a “Has-A” relationship.

An example of this relationship includes: A Faculty Object consisting of multiple Departments A University Object Has many Faculties. A Car Has an Engine

At this point, there is a high chance that you have become more confused about why you should “Favour Composition over Inheritance”.

In the next section, I am going to demystify this concept.

One of the similarities that exist between Inheritance and Composition is how easy it is to build new classes and objects from existing ones this is without paying much attention to the relationship that exists between these objects. However, there are lots of differences that also exist between them. There are also problems with Inheritance as a concept and the practical application of it. These problems form the basis for “Favouring Composition over Inheritance”.

Problem 1: Subclasses (classes created by extending Superclasses) are tightly coupled to superclasses (classes that other classes extend from also called base classes). Any change in a superclass may break the functionality of subclasses.

For example, if we had a Person class that contains data like name, age, skin colour, and a few behaviours that can be performed by a person, like talking, walking, etc and a new requirement was provided where we need to create an Employee class, the first solution that comes to mind would be to create the Employee class by extending the Person class since an Employee “Is-a” Person.

We quickly see how this would not be a great idea due to the tight coupling that would exist between the two classes. if we make changes to the Person class say we change the signature of one of its methods or change the type of one of the member variables, we need to reflect these changes in the Employee class and every other subclass of the Person class otherwise, we risk breaking all existing code. We also break the encapsulation principle since the Employee class has access to the methods and variables of the Person class.

Solution: The Employee class includes a property of type Person named person. This is where Object Composition comes into play. Instead of inheriting from the Person class, the Employee class contains an instance of Person. This establishes a “has-a” relationship – an employee has a person. By doing this, the Employee class utilises the properties and methods of the Person class without inheriting from it. This prevents the tight coupling that occurs in inheritance, where changes to the superclass can affect the subclasses.

The internal details of the Person class remain encapsulated. Changes to the Person class, such as adding new methods or modifying existing ones, do not directly impact the Employee class. The Employee class only exposes a well-defined interface to interact with the Person object, promoting encapsulation and reducing the risk of unintended consequences when making changes.


class Person {
  private name: string;
  private age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  public getName(): string {
    return this.name;
  }

  public getAge(): number {
    return this.age;
  }

  public sayHello(): void {
    console.log(`Hello, my name is ${this.name}.`);
  }
}

class Employee {
  private person: Person; // Object Composition here
  private employeeId: number;

  constructor(person: Person, employeeId: number) {
    this.person = person;
    this.employeeId = employeeId;
  }

  public getEmployeeId(): number {
    return this.employeeId;
  }

  public getEmployeeName(): string {
    return this.person.getName();
  }

  public getEmployeeAge(): number {
    return this.person.getAge();
  }

  public introduceYourself(): void {
    console.log(`Hello, I'm employee ${this.employeeId}.`);
    this.person.sayHello();
  }
}

// Example usage:
const person = new Person("John Doe", 30);
const employee = new Employee(person, 101);

console.log(employee.getEmployeeId());       // Output: 101
console.log(employee.getEmployeeName());     // Output: John Doe
console.log(employee.getEmployeeAge());      // Output: 30

employee.introduceYourself();
// Output:
// Hello, I'm employee 101.
// Hello, my name is John Doe.


Problem 2: builds on problem one. A subclass cannot reduce the interface of the superclass. This means that you have to provide an implementation of all the “abstract methods” of the parent class, even if you will not be using them.

Inheritance creates problems when it comes to Abstract Classes and Methods. In object-oriented programming, an abstract class is a class that cannot be instantiated on its own and is typically meant to be subclassed or extended by other classes. It serves as a blueprint for other classes and may contain abstract methods, concrete methods and properties. Abstract classes can have both abstract and non-abstract (concrete) members. In programming languages where there is no Interface keyword, Abstract classes can be used in instances where an Interface is needed.

Abstract Classes and Methods in action
abstract class Animal {
  // Concrete property
  private _name: string;

  constructor(name: string) {
    this._name = name;
  }

  // Concrete method
  eat(): void {
    console.log(`${this._name} is eating.`);
  }

  // Abstract method - to be implemented by subclasses
  abstract makeSound(): void;
}

class Dog extends Animal {
  // Concrete implementation of the abstract method
  makeSound(): void {
    console.log("Woof! Woof!");
  }

  // Additional method specific to Dog
  fetch(): void {
    console.log("Fetching the ball.");
  }
}

// Creating an instance of the Dog class
const myDog = new Dog("Buddy");

// Using concrete methods from the abstract class
myDog.eat();

// Using the implemented abstract method
myDog.makeSound();

// Using the additional method specific to Dog
myDog.fetch();

When a class extends or inherits from an abstract superclass, the subclass is forced to provide an implementation for all abstract methods of the abstract class when the subclass does not need all the methods present in the abstract superclass. This is typically not ideal, especially when you think of the number of subclasses that can be created from the abstract superclass.

abstract class Animal {
  // Concrete property
  private _name: string;

  constructor(name: string) {
    this._name = name;
  }

  // Concrete method
  eat(): void {
    console.log(`${this._name} is eating.`);
  }

  // Abstract method - to be implemented by subclasses
  abstract makeSound(): void;

  abstract fly(): void;
  abstract walk(): void;
}

class Dog extends Animal {
  // Concrete implementation of the abstract method
  makeSound(): void {
    console.log("Woof! Woof!");
  }

  // Additional method specific to Dog
  fetch(): void {
    console.log("Fetching the ball.");
  }
  walk(): void {
    console.log("I have 4 legs and can walk");
  }

  fly(): void {
    console.log(
      "This has no use for me cause i have no wings but i have been forced to provide and implementation for it",
    );
  }
}

class Bird extends Animal {
  // Concrete implementation of the abstract method
  makeSound(): void {
    console.log("Chrip! Chirp!");
  }

  // Additional method specific to Dog
  fetch(): void {
    console.log("Fetching the ball.");
  }
  fly(): void {
    console.log("I have wings and i can fly");
  }

  walk(): void {
    console.log(
      "This has no use for me cause I have no legs but I have been forced to provide an implementation for it",
    );
  }
}

In the code example above, Dog and Bird are animals, the Bird primary mode of movement is to fly but cannot walk (not its primary mode of movement) while the Dog’s primary mode of movement is walking but it has no wings to fly. The abstract superclass Animal provides abstract methods, walk and fly which every subclass of the Animal superclass would be forced to provide an implementation for.

How does Object Composition help solve this problem you ask? Instead of creating methods that only a few subclasses would need, we would treat those methods as two separate interfaces and create concrete classes that would each implement each interface.

interface IWalk {
  walk(): void;
}

interface IFly {
  fly(): void;
}


class Walk implements IWalk {
  walk(): void {
    console.log("I have 4 legs and can walk");
  }
}

class Fly implements IFly {
  fly(): void {
    console.log("I have wings and I can fly");
  }
}

Concrete classes would extend the superclass but use Object composition to add additional functionality, like type of movement.

abstract class Animal {
  // Concrete property
  private _name: string;

  constructor(name: string) {
    this._name = name;
  }

  // Concrete method
  eat(): void {
    console.log(`${this._name} is eating.`);
  }

  // Abstract method - to be implemented by subclasses
  abstract makeSound(): void;
}

class Dog extends Animal {
  private movementMechanism: IWalk;

  constructor(name: string, movementMechanism: IWalk) {
    super(name);
    this.movementMechanism = movementMechanism;
  }
  // Concrete implementation of the abstract method
  makeSound(): void {
    console.log("Woof! Woof!");
  }

  // Additional method specific to Dog
  fetch(): void {
    console.log("Fetching the ball.");
  }

  move() {
    this.movementMechanism.walk();
  }
}

const dogMovementMechanism = new Walk();
const dog = new Dog("Buddy", dogMovementMechanism);
dog.move(); // I have 4 legs and can walk

const birdMovementMechanism = new Fly();
const bird = new Bird("Brody", birdMovementMechanism);
bird.move(); // I have wings and I can fly

The relationship that would be built becomes: “A Dog “is a” type of Animal and “has a” Walk movement behaviour” “A Bird “is a” type of Animal and “has a” Fly movement behaviour”

Here is the full code:

interface IWalk {
  walk(): void;
}

interface IFly {
  fly(): void;
}

abstract class Animal {
  // Concrete property
  private _name: string;

  constructor(name: string) {
    this._name = name;
  }

  // Concrete method
  eat(): void {
    console.log(`${this._name} is eating.`);
  }

  // Abstract method - to be implemented by subclasses
  abstract makeSound(): void;
}

class Dog extends Animal {
  private movementMechanism: IWalk;

  constructor(name: string, movementMechanism: IWalk) {
    super(name);
    this.movementMechanism = movementMechanism;
  }
  // Concrete implementation of the abstract method
  makeSound(): void {
    console.log("Woof! Woof!");
  }

  // Additional method specific to Dog
  fetch(): void {
    console.log("Fetching the ball.");
  }

  move() {
    this.movementMechanism.walk();
  }
}

class Bird extends Animal {
  private movementMechanism: IFly;
  constructor(name: string, movementMechanism: IFly) {
    super(name);
    this.movementMechanism = movementMechanism;
  }
  // Concrete implementation of the abstract method
  makeSound(): void {
    console.log("Woof! Woof!");
  }

  // Additional method specific to Dog
  fetch(): void {
    console.log("Fetching the ball.");
  }

  move() {
    this.movementMechanism.fly();
  }
}

class Walk implements IWalk {
  walk(): void {
    console.log("I have 4 legs and can walk");
  }
}

class Fly implements IFly {
  fly(): void {
    console.log("I have wings and i can fly");
  }
}

const dogMovementMechanism = new Walk();
const dog = new Dog("Buddy", dogMovementMechanism);
dog.move(); // I have 4 legs and can walk

const birdMovementMechanism = new Fly();
const bird = new Bird("Brody", birdMovementMechanism);
bird.move(); // I have wings and I can fly

Problem 3: builds on problem 2. When providing an implementation of the “abstract methods” of the parent class, you have to pay great attention to the method signature because you have to make sure that the new behaviour is compatible with the one present in the base class. This is very important to adhere to the Liskov Substitution Principle, which is the third requirement of the SOLID principles.

The Liskov Substitution Principle (LSP) is one of the five SOLID principles of object-oriented programming. It was introduced by Barbara Liskov in 1987 and states that objects of a superclass should be replaceable with objects of its subclass without affecting the correctness of the program. In other words, a derived class should be substitutable for its base class without altering the desirable properties of the program.

The LSP ensures that inheritance is used in a way that maintains the integrity of the class hierarchy and does not introduce unexpected behaviours when substituting objects of the derived class for objects of the base class.

When inheriting from a superclass, we have to pay attention to the function signature and implementation so that weird behaviours do not occur.

Let’s take a look at this code:

class Rectangle {
  protected width: number;
  protected height: number;

  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }

  getWidth(): number {
    return this.width;
  }

  getHeight(): number {
    return this.height;
  }

  setWidth(width: number): void {
    this.width = width;
  }

  setHeight(height: number): void {
    this.height = height;
  }

  area(): number {
    return this.width * this.height;
  }
}

class Square extends Rectangle {
  constructor(side: number) {
    super(side, side);
  }

  // Overriding the setWidth and setHeight methods
  setWidth(width: number): void {
    super.setWidth(width);
    super.setHeight(width);
  }

  setHeight(height: number): void {
    super.setWidth(height);
    super.setHeight(height);
  }
}

// Function using the Rectangle class
function printArea(rectangle: Rectangle): void {
  console.log(`Area: ${rectangle.area()}`);
}

// Example violating the Liskov Substitution Principle
const rectangle = new Rectangle(5, 10);
rectangle.setHeight(20);
printArea(rectangle);

// This should work the same, but it doesn't due to the violation
const square = new Square(5);
square.setWidth(20);
printArea(square);

Going over this code, everything looks great, the code runs without issue until we start taking it apart. The setHeight and setWidth method of the Square class assumes that since this is a square class, whenever we call each of those methods, it should set the value of its counterpart (height and width). This makes sense for the Square class because a square has equal sides, and we can easily calculate the area by knowing the length of one side and multiplying it by itself to get the area. This assumption quickly becomes a problem because, given that the Square is a subclass of the Rectangle and it does not have equal sides, passing the Square in places where the Rectangle could also be passed into gives a different result that is difficult to track.

We can avoid hiccups like this when we use Object Composition instead of Inheritance. We no longer necessarily have to worry about maintaining function signatures and providing the right implementation for methods inherited from superclasses, which we probably did not need in the first place.

Trying to maintain these similarities in implementation when extending superclasses can be very challenging, especially with superclasses that have a long list of methods that need to be implemented.

Conclusion:

Favouring composition over inheritance has several more advantages than the ones mentioned in this article. I implore you to do further research on how Object Composition helps you write better code. One very clear advantage is when it comes to Dependency Injection, Unit Testing and respecting one of the SOLID principles, which is Inversion of Control.

I have an article on Dependency Injection in the works, I hope it helps you understand how Dependency Injection and Inversion of control work.