Last updated: 2026-09-23
Inheritance & Composition
Every example on this page uses class-based inheritance — a subclass extending a class fixed at compile time. That's the dominant model, and the one this page (and most of this site) assumes by default, but it isn't the only mechanism for sharing behaviour between objects. See Prototype-Based Programming for delegation, the alternative used by Self and, underneath its class syntax, by JavaScript: instead of a fixed class hierarchy, an object forwards whatever it doesn't define itself to another object it's linked to at runtime — more flexible than class-based inheritance in the ways the "avoid deep hierarchies" and "fixed at compile time" rows below are complaining about.
Inheritance: "Is-A" Relationship
class Vehicle:
def __init__(self, make: str, model: str):
self.make = make
self.model = model
class Car(Vehicle):
def __init__(self, make: str, model: str, doors: int):
super().__init__(make, model)
self.doors = doors
class Vehicle {
protected:
std::string make_;
std::string model_;
public:
Vehicle(std::string make, std::string model)
: make_(std::move(make)), model_(std::move(model)) {}
};
class Car : public Vehicle {
int doors_;
public:
Car(std::string make, std::string model, int doors)
: Vehicle(std::move(make), std::move(model)), doors_(doors) {}
};
public class Vehicle {
protected final String make;
protected final String model;
public Vehicle(String make, String model) {
this.make = make;
this.model = model;
}
}
public final class Car extends Vehicle {
private final int doors;
public Car(String make, String model, int doors) {
super(make, model);
this.doors = doors;
}
}
public class Vehicle {
protected string Make { get; }
protected string Model { get; }
public Vehicle(string make, string model) {
Make = make;
Model = model;
}
}
public sealed class Car : Vehicle {
public int Doors { get; }
public Car(string make, string model, int doors)
: base(make, model) {
Doors = doors;
}
}
class Vehicle
attr_reader :make, :model
def initialize(make, model)
@make = make
@model = model
end
end
class Car < Vehicle
attr_reader :doors
def initialize(make, model, doors)
super(make, model)
@doors = doors
end
end
When to Use Inheritance
✅ Good fits: - Clear "is-a" relationship (Dog is an Animal) - Shared behaviour + specialised extensions - Framework extension points
❌ Avoid: - Code reuse only (use composition) - "Has-a" relationships - Deep hierarchies (>3 levels)
Composition: "Has-A" Relationship
class Engine:
def start(self) -> None:
print("Engine started")
class Car:
def __init__(self):
self._engine = Engine() # Composition
def start(self) -> None:
self._engine.start()
class Engine {
public:
void start() { std::cout << "Engine started\n"; }
};
class Car {
Engine engine_; // Composition
public:
void start() { engine_.start(); }
};
public final class Engine {
public void start() { System.out.println("Engine started"); }
}
public final class Car {
private final Engine engine = new Engine();
public void start() { engine.start(); }
}
public sealed class Engine {
public void Start() => Console.WriteLine("Engine started");
}
public sealed class Car {
private readonly Engine _engine = new Engine();
public void Start() => _engine.Start();
}
class Engine
def start
puts "Engine started"
end
end
class Car
def initialize
@engine = Engine.new # Composition
end
def start
@engine.start
end
end
Composition over Inheritance
| Aspect | Inheritance | Composition |
|---|---|---|
| Coupling | Tight (parent changes break child) | Loose |
| Flexibility | Fixed at compile time | Runtime swappable |
| Reusability | Library-specific | Any object with interface |
| Testing | Harder to mock | Easy to mock/inject |
The "Favour Composition" Rule
Favor object composition over class inheritance."1
# Instead of inheritance:
class LoggingDatabase(Database):
def query(self, sql: str) -> None:
log(sql)
super().query(sql)
# Prefer composition:
class Database:
def __init__(self, logger=None):
self.logger = logger
def query(self, sql: str) -> None:
if self.logger:
self.logger.log(sql)
# execute query
// Instead of inheritance:
class LoggingDatabase : public Database {
public:
void query(const std::string& sql) override {
log(sql);
Database::query(sql);
}
};
// Prefer composition:
class Database {
Logger* logger_;
public:
explicit Database(Logger* logger = nullptr) : logger_(logger) {}
void query(const std::string& sql) {
if (logger_) logger_->log(sql);
// execute query
}
};
// Instead of inheritance:
class LoggingDatabase extends Database {
@Override
public void query(String sql) {
log(sql);
super.query(sql);
}
}
// Prefer composition:
public final class Database {
private final Logger logger;
public Database(Logger logger) { this.logger = logger; }
public void query(String sql) {
if (logger != null) logger.log(sql);
// execute query
}
}
// Instead of inheritance:
class LoggingDatabase : Database {
public override void Query(string sql) {
Log(sql);
base.Query(sql);
}
}
// Prefer composition:
public sealed class Database {
private readonly ILogger _logger;
public Database(ILogger logger) => _logger = logger;
public void Query(string sql) {
_logger?.Log(sql);
// execute query
}
}
# Instead of inheritance:
class LoggingDatabase < Database
def query(sql)
log(sql)
super
end
end
# Prefer composition:
class Database
def initialize(logger: nil)
@logger = logger
end
def query(sql)
@logger&.log(sql)
# execute query
end
end
The Liskov Substitution Principle (LSP)
If
Sis a subtype ofT, objects ofTmust be replaceable with objects ofSwithout breaking the program.2
Classic Violation: Square/Rectangle
class Rectangle:
def __init__(self, width: int, height: int):
self.width = width
self.height = height
def area(self) -> int:
return self.width * self.height
class Square(Rectangle):
def __init__(self, size: int):
super().__init__(size, size)
@property
def width(self) -> int:
return self._width
@width.setter
def width(self, value: int) -> None:
self._width = self._height = value
@property
def height(self) -> int:
return self._height
@height.setter
def height(self, value: int) -> None:
self._width = self._height = value
class Rectangle {
protected:
int width_;
int height_;
public:
Rectangle(int w, int h) : width_(w), height_(h) {}
virtual int width() const { return width_; }
virtual int height() const { return height_; }
virtual void width(int w) { width_ = w; }
virtual void height(int h) { height_ = h; }
int area() const { return width_ * height_; }
};
class Square : public Rectangle {
public:
Square(int size) : Rectangle(size, size) {}
void width(int w) override { width_ = height_ = w; }
void height(int h) override { width_ = height_ = h; }
};
public class Rectangle {
protected int width, height;
public Rectangle(int w, int h) { width = w; height = h; }
public int getWidth() { return width; }
public int getHeight() { return height; }
public void setWidth(int w) { width = w; }
public void setHeight(int h) { height = h; }
public int area() { return width * height; }
}
public class Square extends Rectangle {
public Square(int size) { super(size, size); }
@Override public void setWidth(int w) { width = height = w; }
@Override public void setHeight(int h) { width = height = h; }
}
public class Rectangle {
protected int Width { get; set; }
protected int Height { get; set; }
public Rectangle(int w, int h) { Width = w; Height = h; }
public virtual int Area() => Width * Height;
}
public class Square : Rectangle {
public Square(int size) : base(size, size) {}
public override int Width { set => Width = Height = value; }
public override int Height { set => Width = Height = value; }
}
class Rectangle
attr_accessor :width, :height
def initialize(width, height)
@width = width
@height = height
end
def area
width * height
end
end
class Square < Rectangle
def initialize(size)
super(size, size)
end
def width=(value)
@width = @height = value
end
def height=(value)
@width = @height = value
end
end
def resize(rect: Rectangle) -> None:
rect.width = 10
rect.height = 5
assert rect.area() == 50 # FAILS for Square! 10×10=100
void resize(Rectangle& rect) {
rect.width(10);
rect.height(5);
assert(rect.area() == 50); // FAILS for Square! 10×10=100
}
void resize(Rectangle rect) {
rect.setWidth(10);
rect.setHeight(5);
assert rect.area() == 50; // FAILS for Square! 10×10=100
}
void Resize(Rectangle rect) {
rect.Width = 10;
rect.Height = 5;
Debug.Assert(rect.Area() == 50); // FAILS for Square! 10×10=100
}
def resize(rect)
rect.width = 10
rect.height = 5
raise "LSP violated!" unless rect.area == 50 # FAILS for Square! 10×10=100
end
Fix: Don't inherit Square from Rectangle. Use composition or make Square a factory function.
Square from Rectangle — a setter that forces both dimensions together breaks any code that assumes independent width and height. Use composition or a factory function instead.The Strategy Pattern (Composition in Action)
from abc import ABC, abstractmethod
class CompressionStrategy(ABC):
@abstractmethod
def compress(self, data: str) -> str:
pass
class ZipStrategy(CompressionStrategy):
def compress(self, data: str) -> str:
return f"zip:{data}"
class GzipStrategy(CompressionStrategy):
def compress(self, data: str) -> str:
return f"gzip:{data}"
class Compressor:
def __init__(self, strategy: CompressionStrategy):
self.strategy = strategy
def compress(self, data: str) -> str:
return self.strategy.compress(data)
def set_strategy(self, strategy: CompressionStrategy) -> None:
self.strategy = strategy
# Usage — swap at runtime!
c = Compressor(ZipStrategy())
print(c.compress("data")) # "zip:data"
c.set_strategy(GzipStrategy())
print(c.compress("data")) # "gzip:data"
#include <memory>
#include <string>
class CompressionStrategy {
public:
virtual std::string compress(const std::string& data) = 0;
virtual ~CompressionStrategy() = default;
};
class ZipStrategy : public CompressionStrategy {
std::string compress(const std::string& data) override { return "zip:" + data; }
};
class GzipStrategy : public CompressionStrategy {
std::string compress(const std::string& data) override { return "gzip:" + data; }
};
class Compressor {
std::unique_ptr<CompressionStrategy> strategy_;
public:
explicit Compressor(std::unique_ptr<CompressionStrategy> s) : strategy_(std::move(s)) {}
std::string compress(const std::string& data) { return strategy_->compress(data); }
void setStrategy(std::unique_ptr<CompressionStrategy> s) { strategy_ = std::move(s); }
};
// Usage — swap at runtime!
Compressor c(std::make_unique<ZipStrategy>());
c.compress("data"); // "zip:data"
c.setStrategy(std::make_unique<GzipStrategy>());
c.compress("data"); // "gzip:data"
interface CompressionStrategy {
String compress(String data);
}
class ZipStrategy implements CompressionStrategy {
public String compress(String data) { return "zip:" + data; }
}
class GzipStrategy implements CompressionStrategy {
public String compress(String data) { return "gzip:" + data; }
}
public final class Compressor {
private CompressionStrategy strategy;
public Compressor(CompressionStrategy s) { this.strategy = s; }
public String compress(String data) { return strategy.compress(data); }
public void setStrategy(CompressionStrategy s) { this.strategy = s; }
}
// Usage — swap at runtime!
Compressor c = new Compressor(new ZipStrategy());
c.compress("data"); // "zip:data"
c.setStrategy(new GzipStrategy());
c.compress("data"); // "gzip:data"
public interface ICompressionStrategy {
string Compress(string data);
}
public sealed class ZipStrategy : ICompressionStrategy {
public string Compress(string data) => "zip:" + data;
}
public sealed class GzipStrategy : ICompressionStrategy {
public string Compress(string data) => "gzip:" + data;
}
public sealed class Compressor {
private ICompressionStrategy _strategy;
public Compressor(ICompressionStrategy s) => _strategy = s;
public string Compress(string data) => _strategy.Compress(data);
public void SetStrategy(ICompressionStrategy s) => _strategy = s;
}
// Usage — swap at runtime!
var c = new Compressor(new ZipStrategy());
c.Compress("data"); // "zip:data"
c.SetStrategy(new GzipStrategy());
c.Compress("data"); // "gzip:data"
class ZipStrategy
def compress(data)
"zip:#{data}"
end
end
class GzipStrategy
def compress(data)
"gzip:#{data}"
end
end
class Compressor
attr_writer :strategy
def initialize(strategy)
@strategy = strategy
end
def compress(data)
@strategy.compress(data)
end
end
# Usage — swap at runtime!
c = Compressor.new(ZipStrategy.new)
puts c.compress("data") # "zip:data"
c.strategy = GzipStrategy.new
puts c.compress("data") # "gzip:data"
Related Topics
- OOP Fundamentals — introduces inheritance as one of the four pillars; this page is the deep dive into when it's the wrong tool.
- Prototype-Based Programming: Objects Without Classes — the delegation-based alternative to class inheritance, more flexible in exactly the ways this page's "avoid deep hierarchies" advice is guarding against.
- SOLID Principles — the Liskov Substitution Principle covered here is one of the five, alongside the others that also constrain how inheritance should be used.
- Design Patterns — the Strategy pattern demonstrated here as "composition in action" is one of the Gang of Four's original 23.
References
Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. Held by the University of Reading Library. ↩
Liskov, B., & Wing, J. (1994). A behavioral notion of subtyping. ACM Transactions on Programming Languages and Systems, 16(6), 1811–1841. https://doi.org/10.1145/197320.197383 ↩