Consider this possibly familiar Java class hierarchy:
abstract class Animal {
public abstract void talk();
}
class Dog extends Animal {
@Override
public void talk() {
System.out.println("Woof!");
}
}
class Cat extends Animal {
@Override
public void talk() {
System.out.println("Meow!");
}
}
This ubiquitous example of the power of polymorphism and object oriented design is often the first piece of code introduced in any beginning object oriented (OO) class. Unfortunately, it implies that everything you’re trying to build can be cleanly modeled with inheritance, and that your class relationships should mirror real world relationships (“why, of course cats and dogs are animals!").
Here’s another example from this StackOverflow question about multiple inheritance, which I’ll discuss later:
Car extends WheeledVehicle, KIASpectra extends Car and Electronic, KIASpectra contains Radio. Why doesn’t KIASpectra contain Electronic?
Because it is an Electronic. Inheritance vs. composition should always be an is-a relationship vs. a has-a relationship.
Because it is an Electronic. There are wires, circuit boards, switches, etc. all up and down that thing.
Because it is an Electronic. If your battery goes dead in the winter, you’re in just as much trouble as if all your wheels suddenly went missing.
Notice that the writer didn’t even mention what this design is intended to do or what problem it’s intended to solve. Instead, the writer is obsessed with modeling real world behaviors and relationships: cars run on batteries, have circuits, and all composition vs. inheritance decisions boil down to has-a vs is-a. The writer asks about the pros and cons of multiple inheritance, but evaluates its merits through the lens of faithfulness to the real world. I think the popularity of this lens is because of the inordinate emphasis of OO literature and computer science classes on contrived examples like these that implicitly purvey the “Cats are Animals, Cars have Wheels” style of OO design. But the real goal of OO isn’t to model the world — it’s to design a clean application. There can be no meaningful discussion or evaluation of an application of OO without understanding its intended purpose.
When I was first introduced to OO, I believed in all the same fallacies I just described, and I walked a long and confusing path before finally learning how to actually apply OO. I’d like to share that path in hopes that you might learn something from the journey too.
Cats are Animals, Cars have Wheels
In my freshman year, I played the beautiful game Cave Story and was inspired to write my game. Turns out, writing a game from scratch is really difficult, let alone in C++, a language I barely knew. Nevertheless, I eagerly set to work laying out a class hierarchy for my game.
- GameObj
- Player
- Enemy
- BlobEnemy
- Projectile
- Tile
I thought this was a natural and faithful representation of my game’s world:
there is a tile-based environment with a player, different kind of enemies, and
projectiles shot between the player and the enemies. The GameObj
class is the
root of the non-world objects. How clean, how faithful! A diet of “Cats are
Animals, Cars have Wheels” instruction had led me to live in a world where
nothing could go wrong because I had faithfully modeled the world with
inheritance. How wrong I was.
Inheritance for Everything!
Speaking from the present, let’s run with this example and add some common kinds of enemies, since many enemies do roughly the same thing. As a fastidious engineer, I don’t want to repeat identical enemy logic in every such enemy. So, let’s factor that logic into a few new subclasses:
- Enemy
- AggressiveEnemy
- BlobEnemy
- PeacefulEnemy
- FlowerEnemy
I’ve now subclassed enemies by their disposition towards the player, then
subclassed them further for each specific enemy. This is where the problem
starts. Let’s say I want to add another trait, such as a WalkingEnemy
vs a
HoppingEnemy
, in order to encapsulate the enemy’s movement logic. This
movement trait of an enemy should be separate from its disposition trait, so
I can construct enemies with any of the four choices of behavior combinations.
Here’s a way I might shoehorn this new behavior into the hierarchy:
- Enemy
- AggressiveEnemy
- AggressiveWalkingEnemy
- AggressiveHoppingEnemy
- PeacefulEnemy
- PeacefulWalkingEnemy
- PeacefulHoppingEnemy
Apart from the clear negatives of repeating the walking/hopping trait, notice
that AggressiveWalkingEnemy
and PeacefulWalkingEnemy
have no parent that
represents their shared ability to walk, which means that I can’t refer to a
type that pertains exclusively to walking enemies, like WalkingEnemy
. This
approach is clearly problematic. How can we actually separate movement from
disposition?
Multiple Inheritance for Everything!
If inheritance doesn’t work, we clearly weren’t using enough of it. C++ supports inheriting from multiple classes, so perhaps we could say:
public class BlobEnemy : public AggressiveEnemy,
public WalkingEnemy {
...
}
This could totally work, but I can’t actually tell you how, because I don’t fully understand how C++ multiple inheritance works. It’s incredibly complicated and many other techniques have been created to mitigate its issues. Skim this excerpt from Wikipedia, describing how C++ handles the classic diamond problem (B inherits A, C inherits A, D inherits B and C):
C++ by default follows each inheritance path separately, so a
D
object would actually contain two separateA
objects, and uses ofA
’s members have to be properly qualified. If the inheritance fromA
toB
and the inheritance fromA
toC
are both marked “virtual
” (for example, “class B : virtual public A
"), C++ takes special care to only create oneA
object, and uses ofA
’s members work correctly. If virtual inheritance and nonvirtual inheritance are mixed, there is a single virtualA
and a nonvirtualA
for each nonvirtual inheritance path toA
. C++ requires stating explicitly which parent class the feature to be used is invoked from i.e. “Worker::Human.Age”. C++ does not support explicit repeated inheritance since there would be no way to qualify which superclass to use (i.e. having a class appear more than once in a single derivation list [class Dog : public >Animal, Animal]). C++ also allows a single instance of the multiple class to be created via the virtual inheritance mechanism (i.e. “Worker::Human” and “Musician::Human” will reference the same object).
Suffice it to say that applying multiple inheritance to solve basic problems like factoring enemy behaviors would be like using an exploding spiky sledgehammer to open a chestnut: complex, dangerous, and unnecessary. Java, the other popular OO language, doesn’t even support multiple inheritance, so you’d have to find another way.
Well, we’re out of options here, as far as inheritance is concerned. Hopelessly confused, my freshman year self hit a roadblock that took two years to overcome. Exit “Cats are Animals, Cars have Wheels”, enter composition.
Composition: building from data up, not concepts down
A different OO technique from inheritance, composition seeks to build functionality out of simple classes working together. It ditches the more abstract questions like “What inherits from what?” in favor of utilitarian, concrete questions like “What is the simplest way to represent this functionality?” A good composition-heavy OO design is a great way to solve complex problems by breaking them into smaller problems, each tackled by an opaque module communicating with other modules over well-defined interfaces.
Let’s rewrite our Enemy snippet in accordance with this philosophy, remembering that Enemies are aggressive or peaceful, and hop or walk.
public class Enemy {
public final boolean isAggressive;
public final boolean doesHop;
public Enemy(final boolean isAggressive,
final boolean doesHop) {
this.isAggressive = isAggressive;
this.doesHop = doesHop;
}
public void move() {
// Pivot off of isAggressive to alter my behavior.
// I might set my position to be closer to the
// player if isAggressive is true, for example.
// Pivot off of doesHop to alter how I move around.
// I might bounce into the air if doesHop is
// true, for example.
}
}
This code wonderfully captures all we know about Enemies: that they are
aggressive or peaceful, and that they hop or walk. Member fields encode these
traits far better than inheritance for several reasons. First, fields are
inherently orthogonal — they can be added and removed without affecting other
traits, in contrast to inheritance. Recall how adding hopping/walking
functionality to AggressiveEnemy
forced us to split it into
AggressiveWalkingEnemy
and AggressiveHoppingEnemy
.
Secondly, fields are far simpler to understand. The way a class accesses its fields is aggregated in the class’s definition, in contrast to the way inheritance spreads crucial logic across multiple classes, further obfuscated by overrides.
Finally, the fields can be changed at runtime. I can trivially change my Enemy from aggressive to peaceful by flipping a boolean. It would be almost impossible to do this with inheritance, since you cannot change inheritance at runtime.
I choose booleans for isAggressive
and doesHop
because there are only two
choices for each: aggressive/peaceful and hopping/walking. Should I need three
or more choices, like hopping, walking, or flying enemies, I can encode this
choice as an enum:
public enum MovementType {
WALKING,
HOPPING,
RUNNING
};
So in our Enemy class, boolean doesHop
becomes MovementType movementType
,
and my move
method would switch
on this field to calculate movement.
Enemy 2.0
Let’s demonstrate a few more composition tricks by introducing a new requirement for our Enemy: it’s no longer possible to adequately describe the aggressive/peaceful trait of an Enemy as a binary. We’d like to make enemies with more complex behavior, like enemies that always run away from the player.
The first step to building this is to think about how to model this new
functionality. First, it’s clear that boolean isAggressive
won’t suffice to
model the new kind of movement for an Enemy. We’ll need to generalize the
disposition trait currently modeled by isAggressive
into a new movement
trait that is more capable of expressing the full gamut of movement behaviors.
To represent this trait, we’ll use another class. Here’s what we have:
public class Enemy {
public final DestinationComputer movementComputer;
public final MovementType movementType;
public Enemy(final DestinationComputer movementComputer
final MovementType movementType) {
this.movementComputer = movementComputer;
this.movementType = movementType;
}
public void move() {
final Coordinates targetPosition =
movementComputer.getDestination();
// Pivot off of movementType to achieve getting to
// targetPosition. I might hop, fly, or walk there.
}
}
In this snippet, we’ve composed a destination computer (DestinationComputer
)
inside the Enemy
class, and computation of my Enemy’s destination has been
delegated to this DestinationComputer
. The Enemy
class’s move
method now
consults this computer to determine where it’s going — just like how in real
life, we consult a GPS to determine where we’re going. move
then integrates
the results of this computation with movementType
to effect movement to the
computed destination.
Moving the destination logic from Enemy
to DestinationComputer
makes the
Enemy
class more flexible. Now, the Enemy
class relies on the computer
through a well-defined interface, providing clear abstractions that hide the
innards of destination computation from the Enemy
class. This interface is
defined so:
public interface DestinationComputer {
public Coordinates getDestination();
}
By defining an interface, we’ve created a contract between classes that want to
be destination computers and classes that want to use destination computers.
In order to be eligible to be a destination computer, a class must implement the
getDestination
operation. In order to use the destination computer, a calling
class invokes the getDestination
operation known to exist because of the
contract. Let’s look at a class that wants to be a movement computer:
public class FrightfulDestinationComputer {
@Override
public Coordinates getDestination() {
return /* some random place far, far away from the player.
This value may require intense computation
not represented here. */;
}
}
Here we define a concrete implementation that fulfills the DestinationComputer contract. Specifically, it computes a destination far away from danger. If we plug it into an enemy, the enemy will automatically move to avoid danger.
final Enemy frightfulEnemyThatFliesAwayFromDanger =
new Enemy(new FrightfulDestinationComputer(), MovementType.FLYING);
I can’t understate how monumental the above line is. We’ve allied two classes with different responsibilities into one greater unit. One class’s sole responsibility is to compute a destination; the other class’s sole responsibility is to integrate that destination with flying behavior to fly itself somewhere. Together, we’ve a built a new kind of Enemy out of two simple and clean building blocks.
Wrapping up
There are many other benefits of composition that I won’t discuss here, but you can probably read about them online. I only discussed composition through the lens of data and behavior modeling: how use of it arises naturally when you bind classes together to access one class’s functionality from another. Hopefully, it’s clear how adhering to “Cats are Animals, Cars have Wheels” creates unnecessary complexity and distracts from our real goal as engineers: building clean systems to solve complex problems.