Skip to main content

03 - Inheritance and Composition: Code Reuse in OOP

Inheritance vs Composition: Two Approaches to Code Reuse Inheritance and composition are two fundamental mechanisms for code reuse in object-oriented programming. Understanding when to use each approach is crucial for writing maintainable, flexible, and scalable software.

What do you think about this article?

At a Glance

Inheritance vs Composition: Two Approaches to Code Reuse Inheritance and composition are two fundamental mechanisms for code reuse in object-oriented programming. Understanding when to use each approach is crucial for writing maintainable, flexible, and scalable software.

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.

TypeScript - Basic Inheritance
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
TypeScript - Abstract Classes
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"
TypeScript - Multiple Interfaces
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.

TypeScript - Composition
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
Good Use of Inheritance
// 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
Composition for Flexibility
// 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.

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

Sources and further reading

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects