In this series, I will highlight some areas where you can improve the quality of your code during the development process. After presenting some architectural concepts like SOLID principles in previous articles, I will elucidate the ‘Inheritance over Composition Principle’ in this article. I will show how composition is implemented in Java and Python using examples.
1. Key Concepts
Inheritance and composition are object-oriented concepts used to set up relationships between classes or objects. You should use these two principles soundly because of their major effect on your software architecture. Let’s define these two mechanisms and compare them.
1.1. Inheritance
Inheritance is a fundamental concept in object-oriented programming (OOP). It is a relationship between classes, in which one class (called a sub-class) inherits properties and behaviors (fields and methods) from another class (called a super-class). Inheritance implements the ‘is-a’ relationship and draws a hierarchical relationship between classes. This concept is easy to understand and promotes code reuse.
Benefits of Inheritance
Reusability: Sub-classes reuse the fields and methods of existing super-classes.
Method Overriding: A method inherited from a super-class may not satisfy the specific behavior in a sub-class. Therefore, a sub-class can override this method and provide its own implementation.
Extensibility: Sub-classes can add new functionality to extend their super-classes.
Polymorphism: Objects are treated as instances of their super-class.
1.2. Composition
Composition is an object-oriented programming concept that defines a relationship between objects. Composition means that an object of one class (called a composite) is composed of objects of other classes (called components).
Composition implements the ‘has-a’ relationship and allows the combination of simple objects to form complex objects. Composite object methods can delegate tasks to contained object methods. This contributes to functionality reuse without inheritance.
Benefits of Composition
We’ll talk about the benefits of composition in the next section when we compare it to inheritance.
2. Inheritance over Composition Principle
The principle of composition over inheritance (or composite reuse) recommends fostering composition over inheritance for the reasons below. This doesn’t mean you never use the inheritance concept. But do it in a limited way. Overusing inheritance can lead to tight coupling and inflexible designs.
Inheritance is a powerful mechanism when you respect in your design the nature of the real system that the software represents. When you recognize an intrinsic ‘is-a’ relationship, such as a car is a vehicle, use inheritance between classes.
Preferring composition over inheritance is due to the problems induced by inheritance, such as:
Broken encapsulation: As a subclass inherits from a super-class, the sub-class can see all the details of its parent class. That’s why the concept of encapsulation is considered broken.
Large hierarchies: If you have a deep inheritance hierarchy, this will result in growing complexity and reduced clarity. When you make some changes to super-classes, it would be hard to estimate their impact on the sub-classes.
Tight coupling: Any change in the top-level super-class will lead to many changes at the sub-classes level.
On the other hand, the composition has multiple advantages like:
Flexibility: Since the component object is injected into its composite object using its interface, it is easy to override its implementation.
Reusability: Different classes can reuse the same component classes.
Loose coupling: Objects are independent of each other. Modifying one has no impact on the other. This makes the code base easier to maintain and extend.
Testability: It is better when you use composition.
3. Examples
3.1. Example in Java
In this example, we show how to implement composition in Java. We define a class ‘Vehicle’, its sub-class ‘Car’, and another class ‘Steeringwheel’. An object of the ‘Steeringwheel’ class is a component of an object of the ‘Car’ class. To fulfill such a composition, we create an interface ‘ISteeringwheel’ and make the class ‘Steeringwheel’ implement this interface.
We add a ‘steeringwheel’ attribute to the ‘Car’ class but of type ‘Isteeringwheel’ (interface) and not of type ‘Steeringwheel’ (class). The component object (‘steeringwheel’) is injected using its interface when instantiating the ‘Car’ class.
public class Vehicle { }
public interface ISteeringwheel { }
public class Steeringwheel implements ISteeringwheel{ }
public class Car extends Vehicle{
ISteeringwheel steeringwheel;
public Car(ISteeringwheel steeringwheel) {
this.steeringwheel = steeringwheel;
}
}
public class CompositionDemo {
public static void main(String[] args) {
ISteeringwheel steeringwheel = new Steeringwheel();
Car car = new Car(steeringwheel);
}
}
The advantages of using composition are obvious here. The ‘Car’ and ‘Steeringwheel’ classes are independent. Any change in the ‘Steeringwheel’ class is not visible in the ‘Car’ class and vice versa. In addition, it is easy to replace the implementation of one object of the ‘Steeringwheel’ class with another, which makes it flexible.
3.2. Example in Python
Python supports the principle of composition to create complex objects based on simpler objects. Let’s take a well-known example of a system implementing a book library to illustrate how to implement composition in Python.
This system consists of three classes ‘Library’, ‘Book’, and ‘Author’. An object of the ‘Library’ class is made up of several objects of the ‘Book’ class and each book has an author.
Author Class: Represents an author with a first name, last name, and country. The constructor of the ‘Author’ class (__init__) initializes an instance of this class with the ‘firstname’, ‘lastname’, and ‘country’ attributes. The ‘__str__’ method displays a string representation of an object of the ‘Author’ class. This string is the result of the concatenation of its three attributes.
class Author:
def __init__(self, firstname, lastname, country):
self.firstname = firstname
self.lastname = lastname
self.country = country
def __str__(self):
return f'{self.firstname} {self.lastname} ({self.country})'
Book Class: Represents a book with a title, an author (instance of the ‘Author’ class), the year of publication, and the language in which the book is published.
class Book:
def __init__(self, title, author, year, language="English"):
self.title = title
self.author = author
self.year = year
self.language = language
def __str__(self):
return f'"{self.title}" by {self.author} in {self.language} (published in {self.year})'
Library Class: This class has an attribute ‘books’ that stores a list of ‘Book’ objects. This class provides ‘add_book’, ‘remove_book’, and ‘show_books’ methods. The ‘add_book’ method adds a book to the library. The ‘remove_book’ method removes a book from the library. The ‘show_books’ method lists the available books in the library.
class Library:
def __init__(self):
self.books = []
def add_book(self, book):
self.books.append(book)
def remove_book(self, book):
self.books.remove(book)
def show_books(self):
for idx, book in enumerate(self.books):
print(f'{idx + 1}. {book})')
To use this code, we can create some objects of the ‘Library’, ‘Book’, and ‘Author’ as follows:
# Creating instances of the Author class
author1 = Author("Stephen", "King", "USA")
author2 = Author("Albert", "Camus", "France")
# Creating instances of the Book class
book1 = Book("The Shining", author1, 1977, "English")
book2 = Book("The Green Mile", author1, 1996, "English")
book3 = Book("La Peste", author2, 1947, "French")
# Creating an instance of the Library class and adding books
library = Library()
library.add_book(book1)
library.add_book(book2)
library.add_book(book3)
print('''
List of books in the library after adding three books:
''')
# Listing all books in the library
library.show_books()
print('''
List of books in the library after removing one book:
''')
# Removing a book from the library
library.remove_book(book3)
# Listing all books in the library
library.show_books()
The execution of these examples gives us the following output:
List of books in the library after adding three books:
1. "The Shining" by Stephen King (USA) in English (published in 1977))
2. "The Green Mile" by Stephen King (USA) in English (published in 1996))
3. "La Peste" by Albert Camus (France) in French (published in 1947))
List of books in the library after removing one book:
1. "The Shining" by Stephen King (USA) in English (published in 1977))
2. "The Green Mile" by Stephen King (USA) in English (published in 1996))