Composition vs. inheritance for OCP in programming languages
by Tsivinsky Petr, petsivinskii@edu.hse.ru
Introduction
Software design principles play an important role in creating maintainable and scalable systems. The Open-Closed Principle (OCP) states that software entities (classes, modules, functions, and so on) should be open for extension, but closed for modification. [1]. This essay explores the nuanced relationship between composition and inheritance in achieving the Open-Closed Principle, delving into examples and strategies to illustrate their differences.
1. Understanding the Open-Closed Principle
The Open-Closed Principle guides developers to design systems that allow for easy extension without modifying existing code. This principle aims to enhance code stability, maintainability, and adaptability, particularly in large-scale software projects.
2. Inheritance: The Traditional Approach
Inheritance, a fundamental concept in object-oriented programming, involves a class inheriting properties and behaviours from another class. While it facilitates code reuse, it can introduce a rigid hierarchy that may hinder extensibility, violating the Open-Closed Principle.
The example you can see on Picture 1.
Picture 1. Traditional inheritance in Python
In this example, if we want to add another type of shape (like a Triangle), we would have to modify the `total_area` method in the `AreaCalculator` class to include the new shape type. This violates the Open-Closed Principle because we're modifying existing code (the `AreaCalculator` class) to extend its functionality.
2.1 Polymorphism
To adhere to the Open-Closed Principle, we can refactor the code to use polymorphism. Each shape class will implement its own area method, and the `AreaCalculator` will call this method on each shape. This way, the `AreaCalculator` doesn't need to know the details of how to calculate the area for each shape, and we can add new shapes without modifying the `AreaCalculator`. See picture 2.
Picture 2. Adding Open-Closed Principle to Inheritance
3. Composition: A Flexible Approach
Composition, a design paradigm centered around building complex objects by combining simpler ones, provides a flexible alternative to rigid class hierarchies. It enables the creation of modular and adaptable systems through the aggregation of components.
See example on picture 3.
Picture 3. Composition in Python
4. Strategies to Achieve Open-Closed Principle
4.1 Dependency Injection (Composition)
Dependency injection is a composition technique that promotes loose coupling by injecting dependencies into a class rather than hardcoding them. In this case, we could use constructor injection to provide the `AreaCalculator` with the shapes it should calculate the area for. [2]
See picture 4.
Picture 4. Dependency Injection in Python
In this code, the `AreaCalculator` is not responsible for creating the shapes. Instead, they are injected into the `AreaCalculator` through the constructor. This way, the `AreaCalculator` is open for extension (you can pass any shape as long as it has an `area` method) but closed for modification (you don't need to change the `AreaCalculator` to add new shapes).
4.2 Container Class (Composition)
A container class is a class that is used to hold objects in memory or external storage. A container class acts as a repository in which objects are stored and retrieved. Container classes serve as the foundation of the Abstract Data Type (ADT) [3].
Using a container class can also adhere to the Open-Closed Principle. The container class would be responsible for managing the objects and theirs dependencies. See picture 5.
Picture 5. Container Class in Python
In this example, `ShapesContainer` is the container class that manages the shapes. The `AreaCalculator` receives the `ShapesContainer` and retrieves the shapes from it. This way, you can easily add new shapes to the `ShapesContainer` without having to modify the `AreaCalculator` class.
4.3 Abstract Base Classes (ABCs) (Inheritance)
Abstract Base Classes (ABCs) can be used to implement the Open-Closed Principle. An Abstract Base Class is a kind of class that cannot be instantiated and is meant to be subclassed by other classes. ABCs are designed to encapsulate common behavior that can be reused by multiple subclasses. [4] See picture 6.
Picture 6. Abstract Base Class in Python
The `AbstractShape` here is an abstract base class that defines a common interface for all shapes. `Rectangle` and `Circle` are concrete classes that implement this interface in their own way.
This design adheres to the Open-Closed Principle because you can add new types of shapes (by creating new subclasses of `AbstractShape`) without having to modify the existing shape classes or the code that uses them.
4.4 Interface-based Programming (Inheritance)
Modern software architectures heavily promote the use of interfaces. Originally conceived as a means to separate specification from implementation, popular programming languages toady accommodate interfaces as special kinds of types that can be used - in place of classes - in variable declarations. While it is clear that these interfaces offer polymorphism independent of the inheritance hierarchy, little has been said about the systematic use of interfaces, or how they are actually used in practice. [5]
For this example I switched to Java because Python doesn’t have interfaces. See picture 7.
Picture 7. Interface-based Programming in Java
In this example I provided code that remains open for extension (you can add more shapes by simply creating more classes that implement the Shape interface) but closed for modification (you don't need to change existing code when adding new shapes).
Conclusion
I hope that provided examples will help you understand the difference between inheritance and composition, and how to implement the Open-Closed Principle in both approaches.
In conclusion, achieving the Open-Closed Principle involves a careful balance between composition and inheritance. While composition offers flexibility, inheritance, when used strategically, can provide an intuitive structure. A combination of these paradigms, coupled with dependency injection, container classes, abstract base classes, and interface-based programming, enables the development of maintainable and extensible systems.
I started using object composition more often after studying design patterns. And this helped to understand how modern frameworks such as React and Angular work. Back in the days I practiced using patterns and I advise everyone to try it [6].
References
1. Object Oriented Software Construction, Bertrand Meyer, Prentice Hall, 1988, p 23 https://books.google.ru/books/about/Object_oriented_Software_Construction.html?id=xls_AQAAIAAJ&redir_esc=y
2. Dependency Injection Principles, Practices, and Patterns** by Mark Seemann, Steven van Deursen. 2019 Manning Publications. ISBN: 9781617294730 https://www.oreilly.com/library/view/dependency-injection-principles/9781617294730/
3. An Introduction to Object-Oriented Programming, 2e By Timothy Budd. Published by Addison-Wesley, 1997, ISBN: 0-201-82419-1 , chapter 15 https://web.engr.oregonstate.edu/~budd/Books/oopintro2e/ (last access 12/21/2023)
4. Mastering Object-Oriented Python: Build powerful applications with reusable code using OOP design patterns and Python 3.7, 2nd Edition. Steven F. Lott, Packt Publishing Ltd, Jun 14, 2019. Chapter 5. Page 151 https://books.google.ru/books/about/Mastering_Object_Oriented_Python.html?id=eLKdDwAAQBAJ&redir_esc=y
5. Patterns of Interface-Based Programming by Philip Mayer and Friedrich Steimann. July 2005 The Journal of Object Technology. 4(5):75-94 https://www.researchgate.net/publication/220299034_Patterns_of_Interface-Based_Programming