Inheritance
Inheritance in Low-Level Design, exploring code reuse, the Diamond Problem, and the composition vs. inheritance trade-off.
Inheritance is a mechanism in object-oriented programming that allows a class (subclass/child) to inherit state (fields) and behavior (methods) from another class (superclass/parent).
It models an IS-A relationship (e.g., a CheckingAccount IS-A BankAccount).
The Purpose: Reuse and Hierarchy
Inheritance is primarily used for two reasons:
- Code Reuse: Subclasses automatically inherit implemented methods and fields from the parent class, reducing duplicate code.
- Subtype Polymorphism: It establishes a shared type hierarchy, allowing a client to treat different subclasses as their common superclass type.
Multiple Inheritance & The Diamond Problem
Most modern object-oriented languages (like Java, C#, and Swift) do not support multiple class inheritance (inheriting from more than one class directly). This is to avoid the Diamond Problem.
The Conflict:
- Class
Band ClassCboth inherit from ClassAand override a methodexecute(). - Class
Dinherits from bothBandC(multiple inheritance). - When
D.execute()is called, the compiler does not know whether to runB's version orC's version ofexecute().
To prevent this ambiguity, these languages restrict class inheritance to single inheritance (one direct parent class) while allowing classes to implement multiple interfaces.
Composition vs. Inheritance
One of the most famous guidelines in low-level design is: "Favor Composition over Inheritance."
While inheritance is powerful, it creates a tight, compile-time coupling (often referred to as the Fragile Base Class problem). If the parent class changes, all child classes can break.
- Inheritance (IS-A): Tight coupling. Use only when the relationship is permanent and represents a true hierarchy.
- Composition (HAS-A): Loose coupling. Instead of inheriting behavior, a class references another class containing the behavior.
Simple Example: Car and Engine
Inheriting is incorrect when one object is simply a component of another:
// WRONG: A Car IS NOT an Engine
public class Car extends Engine {
// If Engine implementation details change, Car breaks.
// Also, we cannot easily swap Engine types at runtime.
}
// RIGHT: A Car HAS AN Engine (Composition)
public class Car {
private final Engine engine; // Can easily inject CombustionEngine or ElectricEngine
public Car(Engine engine) {
this.engine = engine;
}
}The Fragile Base Class Example
If we want a class that counts the number of mathematical operations performed, we might extend a base Calculator class:
// Base class (written by someone else)
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int addThree(int a, int b, int c) {
// Internally calls its own add() method
return add(a, b) + c;
}
}
// Subclass trying to count operations
public class CountingCalculator extends Calculator {
private int operationCount = 0;
@Override
public int add(int a, int b) {
operationCount++;
return super.add(a, b);
}
@Override
public int addThree(int a, int b, int c) {
operationCount++;
return super.addThree(a, b, c);
}
public int getOperationCount() {
return operationCount;
}
}Why it Breaks (Double Counting)
If you call addThree(1, 2, 3) on CountingCalculator, you expect operationCount to be 1 (since it is one logical operation from the client's perspective). However, the final count will be 2.
Here is why:
- Our subclass intercepts the
addThreecall and incrementsoperationCountto1. - It delegates to the parent class:
super.addThree(a, b, c). - Internally, the parent class's
addThree()method callsadd(a, b). - Because we overrode
add(), execution is redirected back to our overriddenadd()method, which incrementsoperationCountto2.
This subclass is fragile. We are forced to know the undocumented internal implementation details of Calculator. If a future developer updates Calculator.addThree() to not call add() internally, our subclass's counting behavior will silently change or break.
Other Limitations of Inheritance Coupling
Beyond fragile classes, tight inheritance coupling introduces other design issues:
- Violating Encapsulation: A subclass inherits every public method from its parent, which can expose operations that violate the child's invariants. For example, if a
ReadOnlyDatabaseclass extends a standardDatabaseclass, it inherits thewrite()method. A client can still callreadOnlyDb.write(), completely breaking the read-only safety of the subclass. - Compile-Time Rigidity: The relationship is locked at compile-time. You cannot dynamically swap or change parent behavior at runtime.
- Class Explosion: Combining different feature permutations (e.g., logging, caching, and encrypting) requires creating a separate subclass for every single combination, leading to a massive, hard-to-manage class hierarchy.
The Composition Alternative (HAS-A)
Instead of extending Calculator, we wrap it. This shields us from the internal logic of the calculator class:
public class CountingCalculatorWrapper {
private final Calculator calculator = new Calculator(); // Composition (HAS-A)
private int operationCount = 0;
public int add(int a, int b) {
operationCount++;
return calculator.add(a, b);
}
public int addThree(int a, int b, int c) {
operationCount++;
return calculator.addThree(a, b, c);
}
public int getOperationCount() {
return operationCount;
}
}Summary
- Inheritance (IS-A): Derives fields and behavior from a parent class. Useful for sharing code and building hierarchies.
- Diamond Problem: Ambiguity caused by multiple inheritance, which is why many languages only support single inheritance.
- Favor Composition (HAS-A): Keep systems flexible by composing behavior rather than inheriting it, reducing tight coupling and fragile hierarchies.