Fragile Base Class Problem: Composition over Inheritance
Why do we always hear "Composition Over Inheritance"? Let's discover some hidden pitfalls associated with Inheritance and why you should prefer a HAS-A relationship over IS-A for resilient designs.
If you’re coming from the Object Oriented(OOP) world, you’d have often heard - “Composition Over Inheritance”. But have you wondered, why?
In this blog, we dive into one of the challenges that come with Inheritance in OOP languages, the "Fragile Base Class Problem” and share how unsuspecting changes in your base class can impact your child classes.
Fragile Base Class Problem
Imagine you've designed a Storage
base class that provides some core data persistence logic. Several other classes, like FileStorage
, DatabaseStorage
, and CloudStorage
, might inherit from it, each extending and specialising its behaviour. Let's say one such specialised class is EncryptedStorage
, which aims to encrypt data before saving.
The Fragile Base Class Problem arises when a modification is made to the Storage
base class. The modification does not break the contract of the base class but it does change the functionality. If the base class author changes an internal method that subclasses were implicitly relying on for their extended behaviour, or alters the sequence in which methods are called, subclasses can start misbehaving or failing entirely.
Let’s try to understand this with an example:
In this above version, EncryptedStorage
overrides prepareStorageLocation
for its custom path and writeData
to encrypt the content before the base Storage
class performs the actual write.
Our EncryptedStorage
is now broken! Its encryption logic and custom location logic within the overridden writeData
and prepareStorageLocation
methods are completely bypassed because the base class's storeDocument
method changed its internal implementation. The contract of storeDocument
didn't change, but its behaviour regarding its own overridable methods did.
This example clearly shows how inheritance creates tight coupling, where subclasses often rely not just on the what (the interface) of the base class, but also the how (the internal implementation details). This is the Fragile Base Class Problem in action.
Impact of Fragile Base Class Problem:
Unexpected Bugs & Security Vulnerabilities: Subclasses can start failing or performing in mysterious ways, as in our example, critical functionality like encryption can silently fail.
Increased Maintenance Overhead: Modifying base classes becomes a high-risk activity. Developers must be hyper-aware of all potential subclasses and their internal dependencies, which is often impractical in large systems or when dealing with third-party extensions.
Violation of Liskov Substitution Principle (LSP): A core tenet of OOP, LSP states that objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program. The Fragile Base Class Problem directly violates this.
Preventing Fragile Base Class Problem:
Think through your Contracts: In Java, declare classes or methods
final
if they are not designed for extension or overriding. This explicitly prevents inheritance or modification of specific behaviours.Be extremely cautious about non-abstract methods calling overridable ones – this is a prime spot for fragility. Reduce these.
Minimise
protected
Members:protected
members are part of the subclass contract. The more implementation details you expose, the larger the fragile surface.Prefer Composition Over Inheritance: Instead of inheriting a class, classes can contain instances of other classes and delegate tasks to them. This promotes looser coupling, and you just rely on the what(the interface/contract) of the composed class and not the how(implementation).
Go's Approach: No Inheritance, Only Composition
Go strongly encourages composition as the primary means of code reuse and building complex types. It achieves this through:
Struct Embedding: Go allows embedding a struct within another. This provides a form of composition where the methods of the embedded struct are "promoted" to the containing struct if not implemented by the outer struct. However, it's not inheritance; the containing struct has an instance of the embedded struct.
Interfaces: Go has interfaces, but they are satisfied implicitly. Any type that implements all the methods of an interface automatically satisfies that interface. This allows for polymorphism and decoupling without inheritance hierarchies.
Below is an example of how we can solve the above problem in Golang:
This concludes this article on the Fragile Base Class problem! Hopefully, from now you're about to type extends
, take a moment to consider if defining a clear collaboration between objects (has-a
) might serve you better and lead to a more resilient design than a rigid is-a
relationship!
👉 Connect with me here: Pratik Pandey on LinkedIn
I apologise for being away for a long-time. Thank you for everyone who checked in to ensure if everything was fine! I’ll take some more time to get back in the flow, but I’ve some good ideas on what I want to share and I hope I can get to them soon!