Lesson 3 of 6 · 11 min · 2 hands-on steps
Inheritance vs Composition
Share behavior the flexible way. One of the most-asked OOP design questions.
Inheritance: "is a"
A Dog is an Animal. With extends, the child class gets the parent's methods and can override them:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound`;
}
}
class Dog extends Animal {
speak() {
return `${this.name} barks`;
}
}
const pets = [new Animal("Generic"), new Dog("Rex")];
for (const p of pets) console.log(p.speak());Calling the same method on different types and getting the right behavior is called polymorphism.
The problem with deep inheritance
Imagine Animal → Bird → FlyingBird → Duck. Now you need a robot duck that flies and quacks but isn't alive. Where does it go? Deep inheritance trees get rigid fast. Changing a parent breaks every child.
Composition: "has a"
Instead, build objects from small pieces of behavior:
const canFly = (self) => ({ fly: () => `${self.name} flies` });
const canQuack = (self) => ({ quack: () => `${self.name} quacks` });
function makeDuck(name) {
const self = { name };
return Object.assign(self, canFly(self), canQuack(self));
}
function makeRobotDuck(name) {
const self = { name, battery: 100 };
return Object.assign(self, canFly(self), canQuack(self));
}
console.log(makeRobotDuck("RoboDuck").quack());The rule of thumb
- Use inheritance for a true, stable "is a" relationship with a shallow tree (1–2 levels)
- Use composition for mixing behaviors, which is most of the time
"Favor composition over inheritance" is a classic line from the Design Patterns book, and interviewers love asking why.
🧪 Practice: 2 steps
0 / 2 doneStep 1 of 2 · Inheritance basics
Shape has an area() that returns 0. Create Rectangle and Circle classes that extend Shape and override area().
Then write totalArea(shapes) that returns the sum of all areas (round to 2 decimals).
class Shape {
area() {
return 0;
}
}
class Rectangle extends Shape {
constructor(w, h) {
super();
this.w = w;
this.h = h;
}
}
class Circle extends Shape {
constructor(r) {
super();
this.r = r;
}
}
function totalArea(shapes) {
}
Output appears here. Press Run or Ctrl/⌘ + Enter.
Step 2 of 2 · Compose behaviors
Instead of a deep class tree, build objects by combining small behaviors.
Write makeRobot(name) that returns an object with name, plus the methods from both canWalk and canTalk. The robot's talk() should return "Beep, I am Robo" for makeRobot("Robo").
const canWalk = (self) => ({
walk: () => `${self.name} walks`,
});
const canTalk = (self) => ({
talk: () => `Beep, I am ${self.name}`,
});
function makeRobot(name) {
const self = { name };
// combine self with both behaviors
return self;
}
Output appears here. Press Run or Ctrl/⌘ + Enter.
✅ Check your understanding
"Favor composition over inheritance" means…
Inheritance models which relationship?
Create a free account to track your progress.