Demystifying Composition
Pulling the curtain back on a common software design pattern (with squirrels)
The phrase “favor object composition over class inheritance”1 has permeated design discussion meetings since the conception of object-oriented approaches to creating software. If it’s favored enough to be featured in the first chapter of Design Patterns, I think it’s worth fully understanding what it means and how it’s used.
In a literal sense, the dictionary definition of “Composition” is: “arrangement into specific proportion or relation and especially into artistic form.”2 This appears to advocate for code organization for the sake of reuse — an admiral design philosophy. Design Patterns states that composition aims to assemble objects to attain more complex functionality. This has encapsulation merits, too, as one object will have a reference to multiple other objects.
Let’s try creating a squirrel with our current understanding of composition. The squirrel is a creature, so it should compose scurry and climb movements, right?
const creature = (state) => {
return {
startScurrying: () => {
if (!state.isClimbing) {
state.isScurrying = true;
}
},
startClimbing: () => {
if (!state.isScurrying) {
state.isClimbing = true;
}
},
};
};
const createSquirrel = () => {
let state = {
isScurrying: false,
isClimbing: false,
};
return Object.assign(state, creature(state));
};
const squirrel = createSquirrel();
console.log(squirrel.isClimbing); // false
squirrel.startClimbing();
console.log(squirrel.isClimbing); // true
Here, we’re composing the scurry and climb behaviors by combining them together. However, this is not a composition by way of delegation! Copying objects with Object.assign
leads to fragile design and unintended effects. While this is an attempt to combine or “compose,” it’s actually an expression of inheritance.
Despite the dictionary definition, in the context of programming, composition means delegating functionality to dependent objects. Composition is often compared to inheritance to define relationships between classes. This should serve as a rule for when to use either:
Inheritance: “is a” e.g., a squirrel is an animal
Composition: “has a” e.g., a squirrel has [a] movement pattern
Let’s try to fix our original example to compose the squirrel. A squirrel “has a” movement strategy, which we instantiate and pass as an argument to the new squirrel.
class Squirrel {
constructor(movementStrategy) {
this.movementStrategy = movementStrategy;
this.state = {
isClimbing: false,
isScurrying: false,
};
}
scurry() {
this.movementStrategy.scurry(this);
}
climb() {
this.movementStrategy.climb(this);
}
}
class Movement {
scurry(squirrel) {
if (!squirrel.state.isClimbing) {
squirrel.state.isScurrying = true;
}
}
climb(squirrel) {
if (!squirrel.state.isScurrying) {
squirrel.state.isClimbing = true;
}
}
}
const squirrel = new Squirrel(new Movement());
console.log(squirrel.state.isClimbing); // false
squirrel.climb();
console.log(squirrel.state.isClimbing); // true
Notice now how squirrel.climb();
delegates functionality to the underlying Movement class. Through delegation, we can compose behaviors at runtime so we can change the movement strategy based on—for instance—behavior based on the terrain the squirrel is scurrying/climbing on.
Composition is an important pattern to grasp so we may design for change.
Another critical improvement is that this gives us loose coupling, which makes testing easier! Through dependency injection, we can pass a mock or stub implementation Movement
to isolate logic in Squirrel
.
In summary, the benefits of composition include:
Loose coupling
Design flexibility
Easier unit testing
Read more about composition vs. inheritance.
Universal Set is never written with generative AI. All merits and shortcomings of ideas posed in these articles are mine alone.
Cover image source: Craiyon
Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994), 20.