Back to blog

Inheritance and Composition: Code Reuse Strategies in OOP

oopinheritancecompositiondesign-patternstypescriptpythonjava
Inheritance and Composition: Code Reuse Strategies in OOP

Introduction

Code reuse is one of the fundamental goals of object-oriented programming. Nobody wants to write the same code twice. But how you achieve code reuse matters enormously for the maintainability and flexibility of your software.

In this deep dive, we'll explore the two primary mechanisms for code reuse in OOP: inheritance and composition. You'll learn when each approach is appropriate, understand the famous principle "favor composition over inheritance," and see how to apply these concepts in real-world scenarios.

What You'll Learn

✅ Understand inheritance and the "is-a" relationship
✅ Master method overriding and the super keyword
✅ Learn composition and the "has-a" relationship
✅ Understand why to favor composition over inheritance
✅ Recognize inheritance pitfalls and how to avoid them
✅ Apply mixins and multiple inheritance patterns

Prerequisites


Understanding Inheritance

Inheritance is a mechanism where a new class (child/subclass/derived class) is created from an existing class (parent/superclass/base class), inheriting its properties and methods.

The "Is-A" Relationship

Inheritance models an "is-a" relationship:

  • A Dog is-a Animal
  • A Manager is-a Employee
  • A Car is-a Vehicle

When you use inheritance, you're saying that the child class is a specialized version of the parent class.

// TypeScript
class Animal {
  name: string;
 
  constructor(name: string) {
    this.name = name;
  }
 
  speak(): void {
    console.log(`${this.name} makes a sound`);
  }
}
 
// Dog IS-A Animal
class Dog extends Animal {
  breed: string;
 
  constructor(name: string, breed: string) {
    super(name); // Call parent constructor
    this.breed = breed;
  }
}
 
const dog = new Dog("Rex", "German Shepherd");
dog.speak(); // "Rex makes a sound" (inherited method)
console.log(dog.breed); // "German Shepherd"
# Python
class Animal:
    def __init__(self, name: str):
        self.name = name
 
    def speak(self) -> None:
        print(f"{self.name} makes a sound")
 
# Dog IS-A Animal
class Dog(Animal):
    def __init__(self, name: str, breed: str):
        super().__init__(name)  # Call parent constructor
        self.breed = breed
 
dog = Dog("Rex", "German Shepherd")
dog.speak()  # "Rex makes a sound"
print(dog.breed)  # "German Shepherd"
// Java
public class Animal {
    protected String name;
 
    public Animal(String name) {
        this.name = name;
    }
 
    public void speak() {
        System.out.println(name + " makes a sound");
    }
}
 
// Dog IS-A Animal
public class Dog extends Animal {
    private String breed;
 
    public Dog(String name, String breed) {
        super(name); // Call parent constructor
        this.breed = breed;
    }
 
    public String getBreed() {
        return breed;
    }
}

Method Overriding

Child classes can override parent methods to provide specialized behavior. This is fundamental to polymorphism.

Basic Method Overriding

// TypeScript
class Animal {
  name: string;
 
  constructor(name: string) {
    this.name = name;
  }
 
  speak(): void {
    console.log(`${this.name} makes a sound`);
  }
}
 
class Dog extends Animal {
  speak(): void {
    console.log(`${this.name} barks: Woof!`);
  }
}
 
class Cat extends Animal {
  speak(): void {
    console.log(`${this.name} meows: Meow!`);
  }
}
 
// Polymorphism in action
const animals: Animal[] = [
  new Dog("Rex"),
  new Cat("Whiskers"),
  new Animal("Unknown")
];
 
animals.forEach(animal => animal.speak());
// Output:
// Rex barks: Woof!
// Whiskers meows: Meow!
// Unknown makes a sound
# Python
class Animal:
    def __init__(self, name: str):
        self.name = name
 
    def speak(self) -> None:
        print(f"{self.name} makes a sound")
 
class Dog(Animal):
    def speak(self) -> None:
        print(f"{self.name} barks: Woof!")
 
class Cat(Animal):
    def speak(self) -> None:
        print(f"{self.name} meows: Meow!")
 
# Polymorphism in action
animals = [Dog("Rex"), Cat("Whiskers"), Animal("Unknown")]
 
for animal in animals:
    animal.speak()
# Output:
# Rex barks: Woof!
# Whiskers meows: Meow!
# Unknown makes a sound
// Java
public class Animal {
    protected String name;
 
    public Animal(String name) {
        this.name = name;
    }
 
    public void speak() {
        System.out.println(name + " makes a sound");
    }
}
 
public class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
 
    @Override  // Optional but recommended annotation
    public void speak() {
        System.out.println(name + " barks: Woof!");
    }
}

The super Keyword

The super keyword allows you to access parent class members from a child class.

Calling Parent Constructor

// TypeScript
class Employee {
  name: string;
  salary: number;
 
  constructor(name: string, salary: number) {
    this.name = name;
    this.salary = salary;
  }
 
  calculatePay(): number {
    return this.salary;
  }
}
 
class Manager extends Employee {
  private bonus: number;
  private teamSize: number;
 
  constructor(name: string, salary: number, bonus: number, teamSize: number) {
    super(name, salary); // MUST call super() first
    this.bonus = bonus;
    this.teamSize = teamSize;
  }
 
  calculatePay(): number {
    return super.calculatePay() + this.bonus; // Call parent method
  }
 
  getTeamSize(): number {
    return this.teamSize;
  }
}
 
const manager = new Manager("Alice", 80000, 10000, 5);
console.log(manager.calculatePay()); // 90000
# Python
class Employee:
    def __init__(self, name: str, salary: float):
        self.name = name
        self.salary = salary
 
    def calculate_pay(self) -> float:
        return self.salary
 
class Manager(Employee):
    def __init__(self, name: str, salary: float, bonus: float, team_size: int):
        super().__init__(name, salary)  # Call parent constructor
        self.bonus = bonus
        self.team_size = team_size
 
    def calculate_pay(self) -> float:
        return super().calculate_pay() + self.bonus  # Call parent method
 
manager = Manager("Alice", 80000, 10000, 5)
print(manager.calculate_pay())  # 90000

Calling Parent Methods

Use super when you want to extend parent behavior rather than completely replace it:

// TypeScript
class Logger {
  log(message: string): void {
    console.log(`[LOG] ${message}`);
  }
}
 
class TimestampedLogger extends Logger {
  log(message: string): void {
    const timestamp = new Date().toISOString();
    super.log(`[${timestamp}] ${message}`); // Extend parent behavior
  }
}
 
const logger = new TimestampedLogger();
logger.log("Application started");
// [LOG] [2026-02-06T10:30:00.000Z] Application started

Multi-Level Inheritance

Inheritance can chain across multiple levels, but deep hierarchies often become problematic.

// TypeScript - Multi-level inheritance
class Animal {
  name: string;
 
  constructor(name: string) {
    this.name = name;
  }
 
  eat(): void {
    console.log(`${this.name} is eating`);
  }
}
 
class Mammal extends Animal {
  warmBlooded = true;
 
  nurse(): void {
    console.log(`${this.name} is nursing`);
  }
}
 
class Dog extends Mammal {
  bark(): void {
    console.log(`${this.name} barks!`);
  }
}
 
const dog = new Dog("Rex");
dog.eat();    // From Animal
dog.nurse();  // From Mammal
dog.bark();   // From Dog
console.log(dog.warmBlooded); // true - From Mammal

The Problem with Deep Hierarchies

Deep inheritance hierarchies (3+ levels) often indicate design problems:

// Problematic deep hierarchy
class Entity { }
class LivingEntity extends Entity { }
class Animal extends LivingEntity { }
class Mammal extends Animal { }
class Canine extends Mammal { }
class Dog extends Canine { }
class GermanShepherd extends Dog { }
class PoliceGermanShepherd extends GermanShepherd { }
 
// Problems:
// 1. Hard to understand - where does behavior come from?
// 2. Fragile - changes to any parent affect all descendants
// 3. Rigid - hard to reuse parts independently
// 4. Tight coupling - classes depend on entire hierarchy

Rule of thumb: If your hierarchy exceeds 2-3 levels, consider using composition instead.


Single vs Multiple Inheritance

Single Inheritance (Java, TypeScript)

Most languages support only single inheritance — a class can only extend one parent:

// Java - Single inheritance only
public class Dog extends Animal {
    // Can only extend ONE class
}
 
// This is NOT allowed in Java:
// public class Dog extends Animal, Pet { }  // Error!

Multiple Inheritance (Python)

Python supports multiple inheritance, but it comes with complexity:

# Python - Multiple inheritance
class Flyable:
    def fly(self):
        print("Flying...")
 
class Swimmable:
    def swim(self):
        print("Swimming...")
 
class Duck(Flyable, Swimmable):
    def quack(self):
        print("Quack!")
 
duck = Duck()
duck.fly()   # From Flyable
duck.swim()  # From Swimmable
duck.quack() # From Duck

The Diamond Problem

Multiple inheritance creates the diamond problem when two parents have the same method:

# Python - Diamond problem
class Animal:
    def speak(self):
        print("Animal speaks")
 
class Dog(Animal):
    def speak(self):
        print("Woof!")
 
class Cat(Animal):
    def speak(self):
        print("Meow!")
 
# Which speak() method does DogCat inherit?
class DogCat(Dog, Cat):
    pass
 
dc = DogCat()
dc.speak()  # "Woof!" - Python uses Method Resolution Order (MRO)
 
# Check the MRO
print(DogCat.__mro__)
# (<class 'DogCat'>, <class 'Dog'>, <class 'Cat'>, <class 'Animal'>, <class 'object'>)

Python resolves this with MRO (Method Resolution Order), using the C3 linearization algorithm. The order of parent classes in the declaration matters.


Understanding Composition

Composition is a design principle where complex objects are built from simpler objects. Instead of inheriting behavior, you include (compose) objects that provide that behavior.

The "Has-A" Relationship

Composition models a "has-a" relationship:

  • A Car has-a Engine
  • A Computer has-a Processor
  • A Team has-a list of Players
// TypeScript - Composition example
class Engine {
  private horsepower: number;
 
  constructor(horsepower: number) {
    this.horsepower = horsepower;
  }
 
  start(): void {
    console.log(`Engine with ${this.horsepower}hp started`);
  }
 
  stop(): void {
    console.log("Engine stopped");
  }
}
 
class Transmission {
  private gears: number;
  private currentGear: number = 0;
 
  constructor(gears: number) {
    this.gears = gears;
  }
 
  shiftUp(): void {
    if (this.currentGear < this.gears) {
      this.currentGear++;
      console.log(`Shifted to gear ${this.currentGear}`);
    }
  }
 
  shiftDown(): void {
    if (this.currentGear > 0) {
      this.currentGear--;
      console.log(`Shifted to gear ${this.currentGear}`);
    }
  }
}
 
// Car HAS-A Engine and HAS-A Transmission
class Car {
  private engine: Engine;
  private transmission: Transmission;
  private brand: string;
 
  constructor(brand: string, horsepower: number, gears: number) {
    this.brand = brand;
    this.engine = new Engine(horsepower);
    this.transmission = new Transmission(gears);
  }
 
  start(): void {
    console.log(`Starting ${this.brand}...`);
    this.engine.start();
  }
 
  drive(): void {
    this.transmission.shiftUp();
  }
 
  stop(): void {
    this.transmission.shiftDown();
    this.engine.stop();
  }
}
 
const car = new Car("Toyota", 200, 6);
car.start();  // Starting Toyota... Engine with 200hp started
car.drive();  // Shifted to gear 1
car.stop();   // Shifted to gear 0, Engine stopped
# Python - Composition example
class Engine:
    def __init__(self, horsepower: int):
        self.horsepower = horsepower
 
    def start(self) -> None:
        print(f"Engine with {self.horsepower}hp started")
 
    def stop(self) -> None:
        print("Engine stopped")
 
class Transmission:
    def __init__(self, gears: int):
        self.gears = gears
        self.current_gear = 0
 
    def shift_up(self) -> None:
        if self.current_gear < self.gears:
            self.current_gear += 1
            print(f"Shifted to gear {self.current_gear}")
 
    def shift_down(self) -> None:
        if self.current_gear > 0:
            self.current_gear -= 1
            print(f"Shifted to gear {self.current_gear}")
 
# Car HAS-A Engine and HAS-A Transmission
class Car:
    def __init__(self, brand: str, horsepower: int, gears: int):
        self.brand = brand
        self.engine = Engine(horsepower)
        self.transmission = Transmission(gears)
 
    def start(self) -> None:
        print(f"Starting {self.brand}...")
        self.engine.start()
 
    def drive(self) -> None:
        self.transmission.shift_up()
 
    def stop(self) -> None:
        self.transmission.shift_down()
        self.engine.stop()
 
car = Car("Toyota", 200, 6)
car.start()  # Starting Toyota... Engine with 200hp started

Delegation Pattern

Composition often uses delegation — forwarding method calls to composed objects:

// TypeScript - Delegation pattern
interface INotifier {
  send(message: string): void;
}
 
class EmailNotifier implements INotifier {
  send(message: string): void {
    console.log(`Sending email: ${message}`);
  }
}
 
class SMSNotifier implements INotifier {
  send(message: string): void {
    console.log(`Sending SMS: ${message}`);
  }
}
 
class PushNotifier implements INotifier {
  send(message: string): void {
    console.log(`Sending push notification: ${message}`);
  }
}
 
// NotificationService delegates to various notifiers
class NotificationService {
  private notifiers: INotifier[] = [];
 
  addNotifier(notifier: INotifier): void {
    this.notifiers.push(notifier);
  }
 
  notify(message: string): void {
    // Delegate to all notifiers
    this.notifiers.forEach(notifier => notifier.send(message));
  }
}
 
// Usage
const service = new NotificationService();
service.addNotifier(new EmailNotifier());
service.addNotifier(new SMSNotifier());
service.addNotifier(new PushNotifier());
 
service.notify("Your order has shipped!");
// Sending email: Your order has shipped!
// Sending SMS: Your order has shipped!
// Sending push notification: Your order has shipped!

This is far more flexible than inheriting from a single notification class!


Aggregation vs Composition

Both are "has-a" relationships, but they differ in ownership:

Composition (Strong Ownership)

  • Part cannot exist without the whole
  • Whole is responsible for creating/destroying parts
  • Example: Body has Heart — heart doesn't exist without body
class Heart {
  beat(): void {
    console.log("Beating...");
  }
}
 
class Body {
  private heart: Heart;
 
  constructor() {
    this.heart = new Heart(); // Body creates Heart
  }
 
  // When Body is destroyed, Heart is destroyed too
}

Aggregation (Weak Ownership)

  • Part can exist independently of the whole
  • Part is passed in, not created by the whole
  • Example: Team has Players — players exist without the team
class Player {
  constructor(public name: string) {}
}
 
class Team {
  private players: Player[] = [];
 
  addPlayer(player: Player): void {
    this.players.push(player); // Player already exists
  }
 
  removePlayer(player: Player): void {
    const index = this.players.indexOf(player);
    if (index > -1) {
      this.players.splice(index, 1);
    }
  }
}
 
// Players exist independently
const player1 = new Player("Alice");
const player2 = new Player("Bob");
 
const team = new Team();
team.addPlayer(player1);
team.addPlayer(player2);
 
// If team is deleted, players still exist

Inheritance vs Composition: When to Use Each

This is one of the most important design decisions in OOP. Here's a comprehensive comparison:

Comparison Table

AspectInheritanceComposition
Relationship"Is-a""Has-a"
CouplingTightLoose
FlexibilityLow (set at compile time)High (can change at runtime)
Code ReuseAll or nothingPick what you need
TestingHarder (dependent on parent)Easier (mock components)
EncapsulationPotentially brokenPreserved
Behavior ChangeRequires new subclassChange component

Use Inheritance When:

  1. There's a clear "is-a" relationship that's unlikely to change
  2. You need polymorphism — treating subclasses as parent type
  3. The parent class is stable and won't change frequently
  4. You want to extend behavior, not just reuse it
// Good use of inheritance - clear is-a, need polymorphism
abstract class Shape {
  abstract getArea(): number;
  abstract getPerimeter(): number;
}
 
class Circle extends Shape {
  constructor(private radius: number) {
    super();
  }
 
  getArea(): number {
    return Math.PI * this.radius ** 2;
  }
 
  getPerimeter(): number {
    return 2 * Math.PI * this.radius;
  }
}
 
class Rectangle extends Shape {
  constructor(private width: number, private height: number) {
    super();
  }
 
  getArea(): number {
    return this.width * this.height;
  }
 
  getPerimeter(): number {
    return 2 * (this.width + this.height);
  }
}
 
// Polymorphism - can treat all shapes uniformly
function calculateTotalArea(shapes: Shape[]): number {
  return shapes.reduce((total, shape) => total + shape.getArea(), 0);
}

Use Composition When:

  1. There's a "has-a" relationship
  2. You need flexibility — swap behaviors at runtime
  3. You want to combine multiple behaviors without multiple inheritance
  4. The relationship might change over time
  5. You want to preserve encapsulation
// Good use of composition - behavior can change at runtime
interface IAttackBehavior {
  attack(): void;
}
 
interface IMoveBehavior {
  move(): void;
}
 
class SwordAttack implements IAttackBehavior {
  attack(): void {
    console.log("Swinging sword!");
  }
}
 
class BowAttack implements IAttackBehavior {
  attack(): void {
    console.log("Shooting arrow!");
  }
}
 
class WalkMove implements IMoveBehavior {
  move(): void {
    console.log("Walking...");
  }
}
 
class FlyMove implements IMoveBehavior {
  move(): void {
    console.log("Flying...");
  }
}
 
// Character composed of behaviors - can change at runtime
class Character {
  private attackBehavior: IAttackBehavior;
  private moveBehavior: IMoveBehavior;
 
  constructor(attack: IAttackBehavior, move: IMoveBehavior) {
    this.attackBehavior = attack;
    this.moveBehavior = move;
  }
 
  setAttackBehavior(attack: IAttackBehavior): void {
    this.attackBehavior = attack;
  }
 
  setMoveBehavior(move: IMoveBehavior): void {
    this.moveBehavior = move;
  }
 
  performAttack(): void {
    this.attackBehavior.attack();
  }
 
  performMove(): void {
    this.moveBehavior.move();
  }
}
 
// Usage - flexible!
const warrior = new Character(new SwordAttack(), new WalkMove());
warrior.performAttack(); // Swinging sword!
 
// Power-up! Warrior can now fly
warrior.setMoveBehavior(new FlyMove());
warrior.performMove(); // Flying...
 
// Found a bow!
warrior.setAttackBehavior(new BowAttack());
warrior.performAttack(); // Shooting arrow!

Favor Composition Over Inheritance

This famous principle from the Gang of Four's "Design Patterns" book is widely accepted in software engineering.

Why Favor Composition?

1. Avoids Tight Coupling

Inheritance creates strong dependencies. Changes to the parent class can break all subclasses:

// Problem: Tight coupling with inheritance
class Stack<T> extends Array<T> {
  push(item: T): number {
    return super.push(item);
  }
 
  pop(): T | undefined {
    return super.pop();
  }
}
 
// Problem: Stack inherits ALL Array methods
const stack = new Stack<number>();
stack.push(1);
stack.push(2);
stack.splice(0, 1); // Oops! This breaks stack invariant!
stack.reverse();    // This too!
// Stack now has methods that violate LIFO principle!
// Solution: Composition preserves encapsulation
class Stack<T> {
  private items: T[] = []; // Compose with Array
 
  push(item: T): void {
    this.items.push(item);
  }
 
  pop(): T | undefined {
    return this.items.pop();
  }
 
  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }
 
  isEmpty(): boolean {
    return this.items.length === 0;
  }
 
  // Only expose what Stack needs - no splice, reverse, etc.
}

2. Enables Runtime Flexibility

Inheritance is fixed at compile time. Composition allows changing behavior at runtime:

// Payment processor that can change strategy at runtime
interface PaymentProcessor {
  process(amount: number): void;
}
 
class CreditCardProcessor implements PaymentProcessor {
  process(amount: number): void {
    console.log(`Processing $${amount} via credit card`);
  }
}
 
class PayPalProcessor implements PaymentProcessor {
  process(amount: number): void {
    console.log(`Processing $${amount} via PayPal`);
  }
}
 
class CryptoProcessor implements PaymentProcessor {
  process(amount: number): void {
    console.log(`Processing $${amount} via cryptocurrency`);
  }
}
 
class PaymentService {
  private processor: PaymentProcessor;
 
  constructor(processor: PaymentProcessor) {
    this.processor = processor;
  }
 
  // Can change at runtime!
  setProcessor(processor: PaymentProcessor): void {
    this.processor = processor;
  }
 
  checkout(amount: number): void {
    this.processor.process(amount);
  }
}
 
// Usage
const service = new PaymentService(new CreditCardProcessor());
service.checkout(100); // Processing $100 via credit card
 
// User changes payment method
service.setProcessor(new PayPalProcessor());
service.checkout(50); // Processing $50 via PayPal

3. Easier Testing

Composition allows mocking dependencies easily:

// With composition - easy to test
interface IEmailService {
  send(to: string, subject: string, body: string): Promise<void>;
}
 
class OrderService {
  constructor(private emailService: IEmailService) {}
 
  async placeOrder(order: Order): Promise<void> {
    // Process order...
    await this.emailService.send(
      order.customerEmail,
      "Order Confirmation",
      `Your order ${order.id} has been placed`
    );
  }
}
 
// In tests - easy to mock
class MockEmailService implements IEmailService {
  sentEmails: Array<{to: string, subject: string, body: string}> = [];
 
  async send(to: string, subject: string, body: string): Promise<void> {
    this.sentEmails.push({ to, subject, body });
  }
}
 
// Test
const mockEmail = new MockEmailService();
const orderService = new OrderService(mockEmail);
await orderService.placeOrder(testOrder);
 
expect(mockEmail.sentEmails).toHaveLength(1);
expect(mockEmail.sentEmails[0].to).toBe(testOrder.customerEmail);

Common Inheritance Pitfalls

1. The Fragile Base Class Problem

Changes to a parent class can unexpectedly break child classes:

// Version 1 - Parent class
class Counter {
  protected count: number = 0;
 
  add(value: number): void {
    this.count += value;
  }
 
  addMultiple(values: number[]): void {
    values.forEach(v => this.count += v);
  }
}
 
// Child class depends on parent implementation
class LoggingCounter extends Counter {
  private addCalls = 0;
 
  add(value: number): void {
    this.addCalls++;
    super.add(value);
  }
 
  getAddCalls(): number {
    return this.addCalls;
  }
}
 
// Works as expected
const counter = new LoggingCounter();
counter.add(5);
console.log(counter.getAddCalls()); // 1
 
// Version 2 - Parent class changes implementation
class Counter {
  protected count: number = 0;
 
  add(value: number): void {
    this.count += value;
  }
 
  addMultiple(values: number[]): void {
    values.forEach(v => this.add(v)); // Changed to use add()!
  }
}
 
// Now LoggingCounter breaks!
const counter2 = new LoggingCounter();
counter2.addMultiple([1, 2, 3]);
console.log(counter2.getAddCalls()); // 3 - Unexpected! Was 0 before

2. The Square-Rectangle Problem (LSP Violation)

A classic example of misusing inheritance:

// Problematic: Square extends Rectangle
class Rectangle {
  protected width: number;
  protected height: number;
 
  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }
 
  setWidth(width: number): void {
    this.width = width;
  }
 
  setHeight(height: number): void {
    this.height = height;
  }
 
  getArea(): number {
    return this.width * this.height;
  }
}
 
class Square extends Rectangle {
  constructor(size: number) {
    super(size, size);
  }
 
  // Must override to maintain square invariant
  setWidth(width: number): void {
    this.width = width;
    this.height = width; // Keep it square
  }
 
  setHeight(height: number): void {
    this.height = height;
    this.width = height; // Keep it square
  }
}
 
// This code works for Rectangle but breaks for Square
function stretch(rect: Rectangle): void {
  rect.setWidth(rect.getArea() / 2);
  // Expectation: height unchanged, width halved
  // Reality with Square: both dimensions change!
}
 
const rect = new Rectangle(4, 4);
stretch(rect);
console.log(rect.getArea()); // 8 (works as expected)
 
const square = new Square(4);
stretch(square);
console.log(square.getArea()); // 4 (not 8! Broken!)

Solution: Use composition or separate hierarchies:

// Better approach with composition
interface Shape {
  getArea(): number;
}
 
class Rectangle implements Shape {
  constructor(
    private width: number,
    private height: number
  ) {}
 
  setWidth(width: number): void {
    this.width = width;
  }
 
  setHeight(height: number): void {
    this.height = height;
  }
 
  getArea(): number {
    return this.width * this.height;
  }
}
 
class Square implements Shape {
  constructor(private size: number) {}
 
  setSize(size: number): void {
    this.size = size;
  }
 
  getArea(): number {
    return this.size * this.size;
  }
}
 
// No inheritance relationship - no problem!

3. Breaking Encapsulation

Inheritance can expose internal details to subclasses:

// Parent exposes protected member
class BankAccount {
  protected balance: number = 0;
 
  deposit(amount: number): void {
    if (amount > 0) {
      this.balance += amount;
    }
  }
 
  withdraw(amount: number): boolean {
    if (amount <= this.balance) {
      this.balance -= amount;
      return true;
    }
    return false;
  }
}
 
// Child can break invariants
class HackerAccount extends BankAccount {
  stealMoney(): void {
    this.balance = 1000000; // Direct access bypasses validation!
  }
}

Mixins: Combining Inheritance and Composition

Mixins are a way to add functionality to classes without strict inheritance.

TypeScript Mixins

// TypeScript - Mixin pattern
type Constructor<T = {}> = new (...args: any[]) => T;
 
// Mixin: Timestampable
function Timestampable<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    createdAt: Date = new Date();
    updatedAt: Date = new Date();
 
    touch(): void {
      this.updatedAt = new Date();
    }
  };
}
 
// Mixin: Serializable
function Serializable<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    toJSON(): string {
      return JSON.stringify(this);
    }
  };
}
 
// Base class
class Entity {
  id: string;
 
  constructor(id: string) {
    this.id = id;
  }
}
 
// Combine mixins
class User extends Serializable(Timestampable(Entity)) {
  email: string;
 
  constructor(id: string, email: string) {
    super(id);
    this.email = email;
  }
}
 
const user = new User("1", "alice@example.com");
console.log(user.createdAt); // Date object
user.touch();
console.log(user.toJSON()); // {"id":"1","email":"alice@example.com","createdAt":"...","updatedAt":"..."}

Python Mixins

# Python - Mixin classes
from datetime import datetime
import json
 
# Mixin classes (no __init__, provide specific functionality)
class TimestampMixin:
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.created_at = datetime.now()
        self.updated_at = datetime.now()
 
    def touch(self):
        self.updated_at = datetime.now()
 
class SerializableMixin:
    def to_json(self) -> str:
        return json.dumps(self.__dict__, default=str)
 
# Base class
class Entity:
    def __init__(self, id: str):
        self.id = id
 
# Combine mixins (mixin first, then base class)
class User(TimestampMixin, SerializableMixin, Entity):
    def __init__(self, id: str, email: str):
        super().__init__(id)
        self.email = email
 
user = User("1", "alice@example.com")
print(user.created_at)  # datetime object
user.touch()
print(user.to_json())  # {"id": "1", "email": "alice@example.com", ...}

Real-World Example: Refactoring Inheritance to Composition

Let's refactor a real-world example from inheritance to composition:

Before: Inheritance-based Design

// Problematic: Deep inheritance for notifications
class Notifier {
  send(message: string): void {
    console.log(`Sending: ${message}`);
  }
}
 
class EmailNotifier extends Notifier {
  send(message: string): void {
    console.log(`Sending email: ${message}`);
  }
}
 
class UrgentEmailNotifier extends EmailNotifier {
  send(message: string): void {
    super.send(`[URGENT] ${message}`);
  }
}
 
class EncryptedEmailNotifier extends EmailNotifier {
  send(message: string): void {
    const encrypted = this.encrypt(message);
    super.send(encrypted);
  }
 
  private encrypt(message: string): string {
    return Buffer.from(message).toString('base64');
  }
}
 
// What if we need UrgentEncryptedEmailNotifier?
// What about SMSNotifier with same features?
// This leads to class explosion!

After: Composition-based Design

// Better: Composition with decorator pattern
interface Notifier {
  send(message: string): void;
}
 
// Core notifiers
class EmailNotifier implements Notifier {
  send(message: string): void {
    console.log(`Sending email: ${message}`);
  }
}
 
class SMSNotifier implements Notifier {
  send(message: string): void {
    console.log(`Sending SMS: ${message}`);
  }
}
 
class PushNotifier implements Notifier {
  send(message: string): void {
    console.log(`Sending push: ${message}`);
  }
}
 
// Decorators (compose behaviors)
class UrgentNotifier implements Notifier {
  constructor(private wrapped: Notifier) {}
 
  send(message: string): void {
    this.wrapped.send(`[URGENT] ${message}`);
  }
}
 
class EncryptedNotifier implements Notifier {
  constructor(private wrapped: Notifier) {}
 
  send(message: string): void {
    const encrypted = Buffer.from(message).toString('base64');
    this.wrapped.send(encrypted);
  }
}
 
class LoggingNotifier implements Notifier {
  constructor(private wrapped: Notifier) {}
 
  send(message: string): void {
    console.log(`Logging: About to send message`);
    this.wrapped.send(message);
    console.log(`Logging: Message sent`);
  }
}
 
// Usage - compose behaviors as needed!
const emailNotifier = new EmailNotifier();
const urgentEmail = new UrgentNotifier(emailNotifier);
const encryptedUrgentEmail = new EncryptedNotifier(urgentEmail);
const loggedEncryptedUrgentEmail = new LoggingNotifier(encryptedUrgentEmail);
 
loggedEncryptedUrgentEmail.send("Secret message");
// Logging: About to send message
// Sending email: W1VSR0VOVF0gU2VjcmV0IG1lc3NhZ2U=
// Logging: Message sent
 
// Easy to create any combination!
const urgentSMS = new UrgentNotifier(new SMSNotifier());
const encryptedPush = new EncryptedNotifier(new PushNotifier());

Summary and Key Takeaways

Inheritance

  • Models "is-a" relationships
  • Enables polymorphism — treating subclasses as parent type
  • Use super to call parent methods and constructors
  • Creates tight coupling between parent and child
  • Best for stable hierarchies with clear specialization

Composition

  • Models "has-a" relationships
  • Creates loose coupling between objects
  • Uses delegation to reuse behavior
  • Enables runtime flexibility — swap behaviors dynamically
  • Easier to test — mock dependencies easily

Favor Composition Over Inheritance

  • Avoids fragile base class problem
  • Preserves encapsulation
  • Enables runtime behavior changes
  • Makes testing easier
  • More flexible and maintainable

When to Use Each

Use Inheritance WhenUse Composition When
Clear "is-a" relationship"Has-a" relationship
Need polymorphismNeed flexibility
Parent is stableBehaviors may change
Extending behaviorCombining behaviors
Shallow hierarchy (1-2 levels)Would create deep hierarchy

Practice Exercises

  1. Refactor to Composition: Take an inheritance hierarchy (like Vehicle → Car → ElectricCar → TeslaModelS) and refactor it using composition.

  2. Design a Game Character System: Create a flexible character system for a game where characters can have different movement abilities (walk, fly, swim) and attack abilities (sword, magic, archery). Use composition so abilities can be changed at runtime.

  3. Build a Document Formatter: Design a document formatting system where you can combine formatting options (bold, italic, underline, color) in any combination. Use the decorator pattern with composition.


What's Next?

Now that you understand inheritance and composition, you're ready to learn about Polymorphism and Interfaces in the next post. You'll discover:

  • Compile-time vs runtime polymorphism
  • Interface-based design
  • Duck typing
  • The Liskov Substitution Principle in depth

Continue your OOP journey: Polymorphism and Interfaces


Additional Resources

Previous Posts in This Series

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.