Last updated: 2026-09-23

U
Undergraduate level

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

This is the approved approach. Favour object composition over class inheritance — it keeps coupling loose, swaps behaviour at runtime, and stays easy to mock in tests.
# 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 S is a subtype of T, objects of T must be replaceable with objects of S without 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.

Don't do this! Don't inherit 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"
  • 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


  1. 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.

  2. 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