Design objects to the data — don't model the world

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 separate A objects, and uses of A’s members have to be properly qualified. If the inheritance from A to B and the inheritance from A to C are both marked “virtual” (for example, “class B : virtual public A"), C++ takes special care to only create one A object, and uses of A’s members work correctly. If virtual inheritance and nonvirtual inheritance are mixed, there is a single virtual A and a nonvirtual A for each nonvirtual inheritance path to A. 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).

from Multiple inheritance on Wikipedia

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.