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
- Completed The Four Pillars of OOP
- Completed Classes, Objects, and Abstraction
- Completed Encapsulation and Information Hiding
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
Dogis-aAnimal - A
Manageris-aEmployee - A
Caris-aVehicle
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()) # 90000Calling 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 startedMulti-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 MammalThe 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 hierarchyRule 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 DuckThe 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
Carhas-aEngine - A
Computerhas-aProcessor - A
Teamhas-a list ofPlayers
// 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 startedDelegation 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:
BodyhasHeart— 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:
TeamhasPlayers— 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 existInheritance vs Composition: When to Use Each
This is one of the most important design decisions in OOP. Here's a comprehensive comparison:
Comparison Table
| Aspect | Inheritance | Composition |
|---|---|---|
| Relationship | "Is-a" | "Has-a" |
| Coupling | Tight | Loose |
| Flexibility | Low (set at compile time) | High (can change at runtime) |
| Code Reuse | All or nothing | Pick what you need |
| Testing | Harder (dependent on parent) | Easier (mock components) |
| Encapsulation | Potentially broken | Preserved |
| Behavior Change | Requires new subclass | Change component |
Use Inheritance When:
- There's a clear "is-a" relationship that's unlikely to change
- You need polymorphism — treating subclasses as parent type
- The parent class is stable and won't change frequently
- 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:
- There's a "has-a" relationship
- You need flexibility — swap behaviors at runtime
- You want to combine multiple behaviors without multiple inheritance
- The relationship might change over time
- 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 PayPal3. 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 before2. 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
superto 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 When | Use Composition When |
|---|---|
| Clear "is-a" relationship | "Has-a" relationship |
| Need polymorphism | Need flexibility |
| Parent is stable | Behaviors may change |
| Extending behavior | Combining behaviors |
| Shallow hierarchy (1-2 levels) | Would create deep hierarchy |
Practice Exercises
-
Refactor to Composition: Take an inheritance hierarchy (like
Vehicle → Car → ElectricCar → TeslaModelS) and refactor it using composition. -
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.
-
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
- OOP & Design Patterns Roadmap - Complete learning path
- The Four Pillars of OOP - OOP fundamentals
- Classes, Objects, and Abstraction - Building blocks of OOP
- Encapsulation and Information Hiding - Protecting data integrity
Related Posts
- Go: Favor Composition Over Inheritance - How Go implements composition without inheritance
📬 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.