Mixins & Composition over Inheritance

advanced mixins composition delegation Object.assign fragile-base-class

We’ve all heard the advice: “favor composition over inheritance.” But what does that actually mean? And why do experienced developers keep saying it? Let’s break it down.

The Problem with Deep Inheritance

Inheritance creates a tight parent-child coupling. Change something in the parent and every child feels it. This is called the fragile base class problem.

class Animal {
  constructor(name) { this.name = name; }
  move() { console.log(`${this.name} moves`); }
}

class Bird extends Animal {
  fly() { console.log(`${this.name} flies`); }
}

class Penguin extends Bird {
  // Penguins can't fly! But they inherit fly() from Bird.
  // Now we have to override it to throw or do nothing. Awkward.
  fly() { throw new Error("Penguins can't fly!"); }
}

The hierarchy says “a Penguin IS a Bird” and “a Bird CAN fly.” But that’s not true for all birds. This is where inheritance breaks down — real-world things don’t fit neatly into tree structures.

Inheritance (rigid tree)
Animal
↓ extends
Bird (has fly)
↓ extends
Penguin inherits fly()
Composition (flexible mix)
canWalk
canFly
canSwim
↓ pick what you need
Eagle = walk + fly
Penguin = walk + swim

Mixins with Object.assign

A mixin is simply an object whose methods we copy onto another object or class. Think of it like adding abilities to a character in a game — pick and choose what fits.

const canFly = {
  fly() { console.log(`${this.name} is flying`); }
};

const canSwim = {
  swim() { console.log(`${this.name} is swimming`); }
};

class Duck {
  constructor(name) { this.name = name; }
}

Object.assign(Duck.prototype, canFly, canSwim); // mix in both abilities

const duck = new Duck("Donald");
duck.fly();  // Donald is flying
duck.swim(); // Donald is swimming

Object.assign copies the methods from the mixin objects onto the class prototype. Every Duck instance now has fly() and swim().

Functional Mixins

A more powerful approach is to use functions that take a class and return a new class with extra behavior. This lets the mixin have its own state and constructor logic.

const Serializable = (Base) => class extends Base {
  serialize() {
    return JSON.stringify(this);
  }
  static deserialize(json) {
    return Object.assign(new this(), JSON.parse(json));
  }
};

const Timestamped = (Base) => class extends Base {
  constructor(...args) {
    super(...args);
    this.createdAt = new Date();
  }
};

class User {
  constructor(name) { this.name = name; }
}

class TrackedUser extends Timestamped(Serializable(User)) {}

const user = new TrackedUser("Manish");
console.log(user.serialize());  // {"name":"Manish","createdAt":"..."}
console.log(user.createdAt);    // Date object

We stack the mixins by nesting them. Each one adds a layer. The order matters — Timestamped runs its constructor after Serializable, which runs after User.

The Diamond Problem

In languages with multiple inheritance (like C++), we can get the diamond problem: class D inherits from B and C, both of which inherit from A. Which version of A’s methods does D get? JavaScript avoids this entirely because it only has single inheritance with extends. Mixins are just method copying, not real inheritance chains — so there’s no diamond.

If two mixins define the same method name, the last one wins (since Object.assign overwrites).

Delegation Pattern

Delegation means forwarding method calls to another object instead of inheriting from it. The delegating object says “I don’t know how to do this — but I know someone who does.”

class Engine {
  start() { console.log("Engine started"); }
  stop() { console.log("Engine stopped"); }
}

class Car {
  constructor() {
    this.engine = new Engine(); // Car HAS an engine (not IS an engine)
  }
  start() { this.engine.start(); } // delegate to engine
  stop() { this.engine.stop(); }
}

const car = new Car();
car.start(); // Engine started

The car doesn’t inherit from the engine. It owns an engine and delegates work to it. This is composition — building objects from parts rather than building class hierarchies.

Composition with Closures

We can compose behavior using plain functions and closures, no classes needed:

function createPlayer(name) {
  const state = { name, hp: 100 };

  return {
    getName: () => state.name,
    getHP: () => state.hp,
    takeDamage: (dmg) => { state.hp = Math.max(0, state.hp - dmg); },
    heal: (amount) => { state.hp = Math.min(100, state.hp + amount); }
  };
}

const player = createPlayer("Manish");
player.takeDamage(30);
console.log(player.getHP()); // 70

No this, no prototype, no class — just functions and data. The state is completely private thanks to the closure.

Real-World Example: Event Emitter Mixin

Here’s a practical mixin that adds event emitter behavior to any class:

const EventEmitter = (Base) => class extends Base {
  #listeners = {};

  on(event, fn) {
    (this.#listeners[event] ??= []).push(fn);
  }
  off(event, fn) {
    this.#listeners[event] = (this.#listeners[event] || []).filter(cb => cb !== fn);
  }
  emit(event, ...args) {
    (this.#listeners[event] || []).forEach(fn => fn(...args));
  }
};

class Store extends EventEmitter(class {}) {
  #data = {};
  set(key, value) {
    this.#data[key] = value;
    this.emit("change", { key, value });
  }
}

const store = new Store();
store.on("change", (e) => console.log(`${e.key} changed to ${e.value}`));
store.set("theme", "dark"); // "theme changed to dark"

Any class can gain event emitter powers by mixing in EventEmitter. No inheritance hierarchy needed.

In simple language, inheritance says “this thing IS a type of that thing” while composition says “this thing HAS these abilities.” Composition is more flexible because we can mix and match behaviors without getting locked into a rigid class tree. Use inheritance for true “is-a” relationships and composition for everything else.