Inheritance vs Composition: Two Approaches to Code Reuse
Inheritance and composition are two fundamental mechanisms for code reuse in object-oriented programming. Both allow building complex software from simpler components, but with very different approaches and implications.
🎯 What You'll Learn
- When to use inheritance and when composition
- Differences between abstract classes and interfaces
- The "Composition over Inheritance" principle
- How to avoid common inheritance problems
Inheritance: The "IS-A" Relationship
Inheritance expresses an "is-a" relationship. A dog is an animal, a circle is a shape. The child class inherits all members of the parent class and can extend or override them.
class Vehicle {{ '{' }}
protected speed: number = 0;
constructor(protected brand: string) {{ '{' }}{{ '}' }}
accelerate(amount: number): void {{ '{' }}
this.speed += amount;
console.log(`${{ '{' }}this.brand{{ '}' }} accelerates to ${{ '{' }}this.speed{{ '}' }} km/h`);
{{ '}' }}
getInfo(): string {{ '{' }}
return `${{ '{' }}this.brand{{ '}' }} - Speed: ${{ '{' }}this.speed{{ '}' }} km/h`;
{{ '}' }}
{{ '}' }}
class Car extends Vehicle {{ '{' }}
private doors: number;
constructor(brand: string, doors: number) {{ '{' }}
super(brand); // Calls parent constructor
this.doors = doors;
{{ '}' }}
// Extends parent functionality
getInfo(): string {{ '{' }}
return `${{ '{' }}super.getInfo(){{ '}' }} - Doors: ${{ '{' }}this.doors{{ '}' }}`;
{{ '}' }}
openTrunk(): void {{ '{' }}
console.log("Trunk opened");
{{ '}' }}
{{ '}' }}
const myCar = new Car("Tesla", 4);
myCar.accelerate(50);
console.log(myCar.getInfo());
// Output: Tesla - Speed: 50 km/h - Doors: 4
Abstract Classes vs Interfaces
TypeScript offers both abstract classes and interfaces to define contracts and reusable structures.
Abstract Classes:
- Can have implementations
- Support modifiers (private, protected)
- Single inheritance (extends only one)
- Use them to share common code
Interfaces:
- Only signatures, no implementation
- All members are public
- Multiple implementation (implements multiple interfaces)
- Use them to define contracts
abstract class Shape {{ '{' }}
protected color: string;
constructor(color: string) {{ '{' }}
this.color = color;
{{ '}' }}
// Concrete method (with implementation)
getColor(): string {{ '{' }}
return this.color;
{{ '}' }}
// Abstract methods (to be implemented in subclasses)
abstract area(): number;
abstract perimeter(): number;
{{ '}' }}
class Circle extends Shape {{ '{' }}
constructor(private radius: number, color: string) {{ '{' }}
super(color);
{{ '}' }}
area(): number {{ '{' }}
return Math.PI * this.radius ** 2;
{{ '}' }}
perimeter(): number {{ '{' }}
return 2 * Math.PI * this.radius;
{{ '}' }}
{{ '}' }}
const circle = new Circle(5, "red");
console.log(circle.area()); // 78.54
console.log(circle.getColor()); // "red"
interface Flyable {{ '{' }}
fly(): void;
altitude: number;
{{ '}' }}
interface Swimmable {{ '{' }}
swim(): void;
depth: number;
{{ '}' }}
// A class can implement multiple interfaces
class Duck implements Flyable, Swimmable {{ '{' }}
altitude: number = 0;
depth: number = 0;
fly(): void {{ '{' }}
this.altitude = 100;
console.log(`Flying at ${{ '{' }}this.altitude{{ '}' }}m altitude`);
{{ '}' }}
swim(): void {{ '{' }}
this.depth = 2;
console.log(`Swimming at ${{ '{' }}this.depth{{ '}' }}m depth`);
{{ '}' }}
{{ '}' }}
const duck = new Duck();
duck.fly(); // "Flying at 100m altitude"
duck.swim(); // "Swimming at 2m depth"
Composition: The "HAS-A" Relationship
Composition expresses a "has-a" relationship. A car has an engine, a computer has a CPU. Instead of inheriting, you include instances of other classes as members.
class Engine {{ '{' }}
constructor(private horsepower: number) {{ '{' }}{{ '}' }}
start(): void {{ '{' }}
console.log(`${{ '{' }}this.horsepower{{ '}' }}HP engine started`);
{{ '}' }}
getHorsepower(): number {{ '{' }}
return this.horsepower;
{{ '}' }}
{{ '}' }}
class Transmission {{ '{' }}
constructor(private type: string) {{ '{' }}{{ '}' }}
shift(gear: number): void {{ '{' }}
console.log(`${{ '{' }}this.type{{ '}' }} transmission: gear ${{ '{' }}gear{{ '}' }}`);
{{ '}' }}
{{ '}' }}
// Car "has an" Engine and "has a" Transmission
class CarComposed {{ '{' }}
private engine: Engine;
private transmission: Transmission;
constructor(horsepower: number, transmissionType: string) {{ '{' }}
this.engine = new Engine(horsepower);
this.transmission = new Transmission(transmissionType);
{{ '}' }}
start(): void {{ '{' }}
this.engine.start();
{{ '}' }}
drive(): void {{ '{' }}
this.transmission.shift(1);
console.log("Car is moving");
{{ '}' }}
getSpecs(): string {{ '{' }}
return `${{ '{' }}this.engine.getHorsepower(){{ '}' }}HP`;
{{ '}' }}
{{ '}' }}
const car = new CarComposed(200, "automatic");
car.start(); // "200HP engine started"
car.drive(); // "automatic transmission: gear 1"
// "Car is moving"
Composition Over Inheritance
The "Composition over Inheritance" principle suggests preferring composition over inheritance when possible. Why?
✅ Advantages of Composition:
- Flexibility: Change behaviors at runtime
- Reusability: Combine components in different ways
- Testing: Test components independently
- Loose coupling: Safer modifications
⚠️ Problems with Inheritance:
- Rigid hierarchy difficult to modify
- Strong coupling between parent and child
- Fragile Base Class Problem
- Diamond Problem (multiple inheritance)
When to Use Inheritance
Inheritance is appropriate when:
- There's a clear "is-a" relationship (Dog is an Animal)
- The subclass is a true specialization of the superclass
- You need polymorphism (treat different objects with common interface)
- The hierarchy is stable and won't change frequently
// Clear and stable hierarchy
abstract class Employee {{ '{' }}
constructor(
protected name: string,
protected salary: number
) {{ '{' }}{{ '}' }}
abstract calculateBonus(): number;
getDetails(): string {{ '{' }}
return `${{ '{' }}this.name{{ '}' }} - Salary: €${{ '{' }}this.salary{{ '}' }}`;
{{ '}' }}
{{ '}' }}
class Manager extends Employee {{ '{' }}
calculateBonus(): number {{ '{' }}
return this.salary * 0.2; // 20% bonus
{{ '}' }}
{{ '}' }}
class Developer extends Employee {{ '{' }}
constructor(name: string, salary: number, private language: string) {{ '{' }}
super(name, salary);
{{ '}' }}
calculateBonus(): number {{ '{' }}
return this.salary * 0.15; // 15% bonus
{{ '}' }}
{{ '}' }}
When to Use Composition
Composition is preferable when:
- The relationship is "has-a" (Car has an Engine)
- You need flexible reuse of functionality
- Behaviors need to be interchangeable
- The structure can evolve over time
// Strategy pattern with composition
interface PaymentStrategy {{ '{' }}
pay(amount: number): void;
{{ '}' }}
class CreditCardPayment implements PaymentStrategy {{ '{' }}
pay(amount: number): void {{ '{' }}
console.log(`Payment of €${{ '{' }}amount{{ '}' }} with credit card`);
{{ '}' }}
{{ '}' }}
class PayPalPayment implements PaymentStrategy {{ '{' }}
pay(amount: number): void {{ '{' }}
console.log(`Payment of €${{ '{' }}amount{{ '}' }} with PayPal`);
{{ '}' }}
{{ '}' }}
class ShoppingCartComposed {{ '{' }}
private items: number[] = [];
private paymentStrategy!: PaymentStrategy;
addItem(price: number): void {{ '{' }}
this.items.push(price);
{{ '}' }}
// Change strategy at runtime!
setPaymentStrategy(strategy: PaymentStrategy): void {{ '{' }}
this.paymentStrategy = strategy;
{{ '}' }}
checkout(): void {{ '{' }}
const total = this.items.reduce((sum, price) => sum + price, 0);
this.paymentStrategy.pay(total);
{{ '}' }}
{{ '}' }}
const cart = new ShoppingCartComposed();
cart.addItem(50);
cart.addItem(30);
cart.setPaymentStrategy(new CreditCardPayment());
cart.checkout(); // "Payment of €80 with credit card"
cart.setPaymentStrategy(new PayPalPayment());
cart.checkout(); // "Payment of €80 with PayPal"
Conclusion
Inheritance and composition are not enemies but complementary tools. Inheritance is powerful for modeling stable hierarchies with polymorphism, while composition offers flexibility and modular reuse. The practical rule: start with composition, use inheritance only when the "is-a" relationship is clear and the benefits outweigh the costs.
Related deep dive: 01 - Introduzione alla Programmazione Orientata agli Oggetti.
Related deep dive: 02 - Incapsulamento in OOP: Proteggere e Nascondere i Dati.
🎯 Key Points
- Inheritance = "is-a", Composition = "has-a"
- Abstract classes for shared code, interfaces for contracts
- Composition over Inheritance for greater flexibility
- Use inheritance for stable hierarchies with clear polymorphism
Frequently asked questions
What is the main difference between inheritance and composition in object-oriented programming?
Inheritance expresses an "is-a" relationship, while composition expresses a "has-a" relationship.
When should you use inheritance instead of composition?
Use inheritance when the "is-a" relationship is clear and the benefits outweigh the costs.
What is the "Composition over Inheritance" principle in object-oriented programming?
The "Composition over Inheritance" principle suggests preferring composition over inheritance when possible.
Why might you start with composition instead of inheritance according to the article's practical rule?
You should start with composition because it offers flexibility and modular reuse.
Commenti
Caricamento commenti...
Sign in to comment