Composing objects: building complex behavior from simple parts
Apply the composing objects principle to assemble behavior from small, focused collaborators rather than inheriting from deep class hierarchies.
Introduction
The composing objects principle builds complex behavior by assembling focused collaborators. Instead of making one class inherit every capability, give each capability a small object and let a coordinating object delegate to it.
This article uses a configurable Robot to show how composition handles independent variation, how it relates to Strategy, and where inheritance is still a good fit.
TL;DR / Mental Model
- Mental model: a composed object is a coordinator with collaborators, not a hierarchy of capabilities.
- Boundary: each collaborator owns one behavior; the outer object owns the workflow and delegates through a contract.
- Design payoff: independent variation can be combined at construction time or, when required, swapped at runtime.
- Trade-off: composition adds wiring, delegation, and more objects, so use it for genuine behavioral variation rather than turning every method into a class.
30-Second Explanation
Compose behavior by giving an object small collaborators such as Movement, Weapon, and Sensor. A Robot delegates to those interfaces, so walking can be replaced by flying without changing the robot's coordination logic. This avoids subclass combinations, but it still requires clear contracts and sensible boundaries.
5-Minute Explanation
Find the independent axes of variation, define a focused interface for each, and inject concrete implementations into the composed object. Keep construction-time collaborators final when runtime replacement is unnecessary; use a Strategy-style setter or a new immutable instance only when the domain needs reconfiguration. Composition is not a ban on inheritance: a true, substitutable IS-A relationship or a framework template can still justify a base class.
The Problem
A Robot needs to walk, shoot lasers, and detect obstacles. The tempting move is to build an inheritance chain: Robot extends MovingEntity extends ShootingEntity. Then the requirements change. A new robot flies instead of walking, and another detects heat instead of obstacles. A hierarchy now has to represent several independent choices at once.
The composing objects principle says: assemble complex behavior from small, focused collaborators rather than layering every capability through inheritance. Each collaborator does one job. The composed object wires them together. Swap a collaborator, and the behavior changes without rewriting the coordinating class.
What Is the Composing Objects Principle
The composing objects principle states: build complex behavior by combining simple, focused objects rather than layering behavior through inheritance. Each small object handles exactly one capability. The composed object holds references to these collaborators and delegates to them.
Think of it like LEGO. Each brick is simple and single-purpose (a 2x4 block, a wheel, a hinge). You build a car not by making a "CarBrick" that inherits from "WheelBrick," but by snapping independent bricks together. Want to change the wheels? Pop them off and snap on different ones. The chassis does not care.
On the left, adding a new capability means inserting a new layer into the hierarchy. On the right, adding a capability means plugging in one more collaborator. For independent axes of variation, the composed approach can grow roughly with the number of implementations, while a naive inheritance approach can grow with the number of combinations.
Composition Over Inheritance
This is not a theoretical debate. It is a practical observation: inheritance hierarchies break the moment requirements combine in unexpected ways.
With inheritance, you get one axis of variation. Robot extends MovingEntity locks in movement behavior for every robot. If you need a flying robot and a walking robot, you need two separate class trees or you resort to ugly workarounds (empty method overrides, boolean flags, multiple inheritance hacks).
With composition, you get independent axes of variation. Movement is one axis. Weapon is another. Sensor is a third. Mix and match freely. A robot with WalkingMovement, LaserWeapon, and RadarSensor is just three constructor arguments. Swap WalkingMovement for FlyingMovement and nothing else changes.
In this example, inheritance needs a leaf class per combination: 2 movements Γ 2 weapons Γ 2 sensors = 8 leaf classes, each with its own name and hierarchy position. Composition needs 6 small implementations plus one Robot class that accepts any combination. Add a third weapon type and inheritance needs 12 leaf classes; composition needs 7 small implementations plus the same Robot class.
The math alone makes the case. But the real payoff is testing. Each collaborator is testable in isolation. WalkingMovement does not need a weapon to be tested. LaserWeapon does not need a sensor. And Robot can be tested with mocks for all three.
Implementation
// Capability interface: how the robot moves.
// Each implementation is a small, focused class.
// Robot never knows whether it walks, flies, or rolls.
public interface Movement {
void move(double x, double y);
String type();
}Notice what the Robot class does not contain: no if (type == FLYING) branches, no @Override of parent methods, no casting. It holds three interfaces and calls them. Adding a MissileWeapon means writing one new class that implements Weapon; the coordinating class can remain unchanged. That is the open/closed principle in action, powered by composition.
Strategy + Composition: Pluggable Behavior at Runtime
Composing objects becomes even more powerful when you allow swapping collaborators after construction. This is where the Strategy pattern meets composition: the composed object holds a strategy that can be replaced at runtime.
To enable runtime swapping, make the collaborator field non-final and expose a setter:
public class Robot {
private final String name;
private Movement movement; // no longer final
private final Weapon weapon;
private final Sensor sensor;
// ... constructor as before ...
// Swap movement strategy at runtime.
// The robot starts walking and switches to flying mid-mission.
public void setMovement(Movement movement) {
this.movement = movement;
}
}
This is exactly what the Strategy pattern does: encapsulate a family of algorithms behind an interface and make them interchangeable. The difference in framing is that Strategy focuses on one swappable behavior, while the composing objects principle applies the same idea across all of a class's capabilities simultaneously.
Start with final fields for construction-time composition. Add setters only when the requirements genuinely need runtime swapping; otherwise, immutability keeps the object's configuration easier to reason about.
Pattern vocabulary
When one collaborator represents a swappable family of algorithms, this is the Strategy pattern. Naming the pattern is useful because it distinguishes one focused use of composition from the broader principle of assembling an object from collaborators.
When Inheritance Is Still Fine
Composition is a useful default when behavior varies, not the only tool. Inheritance is appropriate when all three conditions hold:
- True IS-A relationship. A
SavingsAccountgenuinely IS-ABankAccount. It shares identity, not just behavior. - Liskov substitutability. Every place that accepts a
BankAccountworks correctly with aSavingsAccount. No surprises, no exceptions thrown from overridden methods. - Shared state and behavior. The subclass needs the parent's fields and the parent's method implementations, not just its interface.
| Criterion | Inheritance fits | Composition fits |
|---|---|---|
| Relationship | True IS-A (Dog IS-A Animal) | HAS-A or USES-A (Robot HAS-A Weapon) |
| Substitutability | Subclass passes Liskov check | Not required |
| Variation axes | Single axis (one thing varies) | Multiple independent axes |
| Runtime flexibility | Fixed at compile time | Swappable at runtime |
| Framework requirement | extends AbstractList, extends HttpServlet | No framework constraint |
Concrete examples where inheritance is correct:
ArrayList extends AbstractList(genuine IS-A, shared state, framework contract)HttpServletsubclasses (servlet container expects this hierarchy)- Domain models with strict taxonomy (
CheckingAccount extends BankAccountwhen the bank truly models it this way)
The honest rule: if you are inheriting to reuse three lines of code, use composition. If you are inheriting because the domain relationship is genuinely hierarchical and substitutable, inheritance is fine. When in doubt, compose.
Inheritance is a one-way door
Once you publish an inheritance hierarchy, clients can depend on it. Changing a superclass contract may break subclasses and downstream callers. Composition, by contrast, lets you change a collaborator's internals without changing the composed class's API when its contract remains stable. Prefer the more reversible choice when the domain does not require inheritance.
Common Mistakes
1. Premature abstraction
You write Movement, Weapon, and Sensor interfaces before you have a concrete use case. Then you discover the abstraction is wrong, and you refactor the interface, the implementation, and every test. Do not add an interface automatically; introduce one when there is real variation, a test seam, or a boundary worth protecting. A second implementation often reveals the right abstraction, but it is not a requirement in every design.
2. Over-composition (too many collaborators)
A Robot with 12 injected collaborators is not necessarily well-composed. It may be a class with too many responsibilities masked by delegation. A constructor with more than several parameters is a signal to review cohesion, not a hard cutoff. Split the class first when the collaborators form separate responsibilities, then compose the pieces.
3. Composing what should be a method
Not every behavior needs its own class. If the "collaborator" is a single method with no state and no alternative implementations, it belongs as a private method on the composed class, not as a separate object with an interface. Composition is for genuine behavioral variation, not for turning every method into a class.
4. Forgetting to define clear interfaces
Composing concrete classes can make a relationship unnecessarily rigid. If Robot holds a WalkingMovement field instead of a Movement field, it cannot swap movement implementations through that field. When variation or substitution is required, compose through an interface or abstract type; concrete composition is fine when the collaborator is intentionally fixed.
| Mistake | Symptom | Fix |
|---|---|---|
| Premature abstraction | Interface with one implementation, changes frequently | Wait for 2+ implementations, then extract |
| Over-composition | Constructor with 8+ parameters | Split the class into smaller composed units |
| Trivial composition | Single-method stateless "strategy" class | Use a private method instead |
| Concrete composition | Robot holds WalkingMovement not Movement | Depend on the abstraction, not the implementation |
Test Your Understanding
Quick Recap
- The composing objects principle says: build complex behavior by assembling small, focused collaborators rather than stacking behavior through inheritance hierarchies.
- Each collaborator implements a single-capability interface (
Movement,Weapon,Sensor), and the composed class delegates to them without knowing the concrete types. - Composition gives you M + N + 1 classes for M Γ N combinations of behavior; inheritance gives you M Γ N classes. The gap widens with every new axis of variation.
- Combine composition with the Strategy pattern to swap collaborators at runtime, but default to final fields for thread safety and simplicity.
- Inheritance is still correct for genuine IS-A relationships with shared state, Liskov substitutability, and framework contracts.
- The most common composition mistake is over-composition: injecting 8+ collaborators into one constructor, which masks a class that has too many responsibilities.
- In a design discussion, explain the collaborators and their contracts before discussing the pattern name. The important result is a clear change boundary, not a slogan.
Related OOP Concepts
- Composition vs Inheritance - Compares object composition with subclassing and gives tests for choosing between them.
- Abstraction - Shows how collaborator interfaces separate stable capabilities from implementation details.
- Association - Covers the reference relationship that lets objects collaborate without implying inheritance.