home / skills / yanko-belov / code-craft / composition-over-inheritance

composition-over-inheritance skill

/skills/composition-over-inheritance

This skill helps you replace inheritance with composition by guiding you to assemble behaviors via interfaces and injectables for flexible class design.

npx playbooks add skill yanko-belov/code-craft --skill composition-over-inheritance

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
5.5 KB
---
name: composition-over-inheritance
description: Use when tempted to use class inheritance. Use when creating class hierarchies. Use when subclass needs only some parent behavior.
---

# Composition Over Inheritance

## Overview

**Favor object composition over class inheritance.**

Inheritance creates tight coupling and rigid hierarchies. Composition creates flexible, reusable components that can be mixed and matched.

## When to Use

- Designing relationships between classes
- Tempted to use `extends`
- Class needs behavior from multiple sources
- Creating "is-a" relationships
- Building class hierarchies

## The Iron Rule

```
NEVER use inheritance when composition would work.
```

**No exceptions:**
- Not for "it's the OOP way"
- Not for "is-a relationship"
- Not for "code reuse via extends"
- Not for "polymorphism"

**Default to composition. Use inheritance only for true type hierarchies.**

## Detection: The Inheritance Smell

If inheritance feels awkward or forced, use composition:

```typescript
// ❌ VIOLATION: Inheritance hierarchy
class Animal {
  eat(): void { console.log('Eating'); }
}

class FlyingAnimal extends Animal {
  fly(): void { console.log('Flying'); }
}

class SwimmingAnimal extends Animal {
  swim(): void { console.log('Swimming'); }
}

// Duck needs both fly AND swim - inheritance can't do this cleanly
class Duck extends FlyingAnimal {
  swim(): void { console.log('Swimming'); } // Duplicated!
}
```

## Correct Pattern: Composition with Interfaces

Define capabilities as interfaces, compose them:

```typescript
// ✅ CORRECT: Composition
interface Flyable {
  fly(): void;
}

interface Swimmable {
  swim(): void;
}

interface Eatable {
  eat(): void;
}

// Reusable behaviors
const flyingBehavior: Flyable = {
  fly() { console.log('Flying'); }
};

const swimmingBehavior: Swimmable = {
  swim() { console.log('Swimming'); }
};

const eatingBehavior: Eatable = {
  eat() { console.log('Eating'); }
};

// Compose what you need
class Duck implements Flyable, Swimmable, Eatable {
  fly = flyingBehavior.fly;
  swim = swimmingBehavior.swim;
  eat = eatingBehavior.eat;
}

class Fish implements Swimmable, Eatable {
  swim = swimmingBehavior.swim;
  eat = eatingBehavior.eat;
}

class Bird implements Flyable, Eatable {
  fly = flyingBehavior.fly;
  eat = eatingBehavior.eat;
}
```

## Why Inheritance Fails

| Problem | Example |
|---------|---------|
| **Diamond problem** | Duck needs Flying AND Swimming |
| **Tight coupling** | Child knows parent internals |
| **Rigid hierarchy** | Can't change parent without breaking children |
| **Forced inheritance** | Gets methods it doesn't need |
| **Fragile base class** | Parent changes break all children |

## Why Composition Wins

| Benefit | Example |
|---------|---------|
| **Flexible** | Mix any behaviors together |
| **Loose coupling** | Components don't know each other |
| **Easy testing** | Mock individual behaviors |
| **Runtime changes** | Swap behaviors dynamically |
| **No hierarchy lock-in** | Add new combinations freely |

## Pressure Resistance Protocol

### 1. "It's the OOP Way"
**Pressure:** "Object-oriented programming uses inheritance"

**Response:** Modern OOP favors composition. Inheritance is overused.

**Action:** Use interfaces + composition. It's still OOP.

### 2. "It's an Is-A Relationship"
**Pressure:** "A Duck IS-A Bird, so it should extend Bird"

**Response:** "Is-a" often becomes "has-a" when requirements change. Composition handles both.

**Action:** Model as "has behaviors" not "is a type".

### 3. "Code Reuse via Extends"
**Pressure:** "I need the parent's methods"

**Response:** Composition provides better code reuse without coupling.

**Action:** Extract shared behavior into composable units.

### 4. "Polymorphism Requires Inheritance"
**Pressure:** "I need to treat different types uniformly"

**Response:** Interfaces provide polymorphism without inheritance.

**Action:** Define interface, have classes implement it.

## Red Flags - STOP and Reconsider

If you notice ANY of these, use composition instead:

- `extends` keyword in your code
- Class hierarchy deeper than 2 levels
- Child class overriding parent methods
- "Diamond problem" - needs multiple parents
- Subclass doesn't use all parent methods
- Changing parent breaks children
- Hard to test without instantiating parent

**All of these mean: Refactor to composition.**

## When Inheritance IS Appropriate

Use inheritance only when:
- True type hierarchy (rarely)
- Framework requires it (React class components, etc.)
- Extending library classes you don't control

Even then, keep hierarchy shallow (max 2 levels).

## Quick Reference

| Inheritance | Composition |
|-------------|-------------|
| `class Dog extends Animal` | `class Dog implements Animal` + behavior injection |
| Rigid hierarchy | Flexible composition |
| Single parent only | Multiple behaviors |
| Tight coupling | Loose coupling |
| Changes cascade | Changes isolated |

## Common Rationalizations (All Invalid)

| Excuse | Reality |
|--------|---------|
| "It's the OOP way" | Modern OOP prefers composition. |
| "It's an is-a relationship" | "Has behavior" is more flexible. |
| "Need parent's methods" | Compose the behavior instead. |
| "Polymorphism needs it" | Interfaces provide polymorphism. |
| "Less code with extends" | More flexibility with composition. |
| "I noted it's problematic" | Don't do it if it's problematic. |

## The Bottom Line

**Compose behaviors. Don't inherit them.**

When designing classes: define interfaces for capabilities, create composable behaviors, inject what each class needs. Use `extends` only as last resort.

Overview

This skill recommends favoring composition over class inheritance when designing TypeScript classes. It explains why inheritance creates tight coupling and rigid hierarchies and shows how to build flexible, testable components by composing behaviors via interfaces and injected implementations. The guidance emphasizes defaulting to composition and reserving inheritance for rare, true type hierarchies or framework-imposed cases.

How this skill works

The skill inspects design decisions where classes extend other classes, looking for inheritance smells like deep hierarchies, overridden parent methods, or duplicated behavior. It guides you to extract capabilities as interfaces and reusable behavior objects, then compose those behaviors into concrete classes. It also provides patterns, detection heuristics, and short responses to common rationalizations for using inheritance.

When to use it

  • Designing relationships between classes and weighing extends vs. implements
  • When a class needs behavior from multiple sources or multiple parents
  • When you find class hierarchies deeper than two levels
  • When a subclass overrides many parent methods or inherits unused methods
  • When changes in a parent class cascade and break children

Best practices

  • Default to composition: model capabilities as interfaces and inject behaviors
  • Keep inheritance shallow (max two levels) and only for true type hierarchies
  • Extract shared methods into composable behavior objects instead of extending
  • Prefer loose coupling so behaviors can be mocked and swapped at runtime
  • Watch for the diamond problem and replace multiple inheritance attempts with composition

Example use cases

  • Modeling animals where one entity needs flying and swimming without duplicating code
  • Refactoring a deep class hierarchy that becomes fragile after parent changes
  • Implementing reusable capabilities (e.g., logging, persistence, validation) across unrelated classes
  • Replacing extends-based code reuse with interfaces and injected behavior objects
  • When frameworks force inheritance reluctantly, keep classes small and behavior composable

FAQ

When is inheritance acceptable?

Use inheritance only for true type hierarchies, framework requirements, or extending library classes you don't control, and keep hierarchies shallow.

How do I get polymorphism without inheritance?

Define interfaces for the required behavior and have classes implement them; you can treat instances uniformly through interface types.