It’s your first week of “real” computer science courses (none of the if statements or for loops). You walk into your class and on the board–your professor has written: Polymorphism. You take a seat close to the front and he begins–

“Today, we’ll learn about one of the most fundamental principles of Object-Oriented-Programming; Polymorphism”

A classic example follows:

public interface Animal {
    String name()
    void speak()
}

public class Dog implements Animal {
    private String name;

    public Dog(final String name) {
    	this.name = name;
    }

    public String name() {
    	return this.name;
    }

    public void speak() {
    	System.out.Println("Woof");
    }
}

public class Cat implements Animal {
    private String name;

    public Cat(final String name) {
    	this.name = name;
    }

    public String name() {
    	return this.name;
    }

    public void speak() {
    	System.out.Println("Meow");
    }
}

At this point, you can sort-of see the how this can apply to “programming.” Either way, it makes perfect sense; a dog IS an animal and a cat IS an animal. In fact, starting from this point on, it becomes ingrained in your mind that all abstractions are an “IS-A” relationship.

Fast forward to a couple years later–your colleagues, people in the industry, and open-source code-bases are praising (and using) composition over inheritance. In fact, a popular programming language doesn’t even have the idea of “implementation” in its language details! (Can you guess which one?)

After years of inheriting (pun intended) this mindset I learned in college, I have to admit–I was lost and confused within the world of composition; I understood the ideas, but it never really made that much sense. A dog IS an animal! What is the point of remodeling this kind of abstraction?

After days of these ideas brewing in my head, it clicked.

I realized that it’s really about simplicity and modularity. Where inheritance looks to make perfect abstractions based on hierarchical models, composition focuses on behaviors; capabilities, if you will. Let’s take the animal example above and adopt it more with composition.

When you look at a dog, it has the capability to:

  1. Make noise.
  2. Walk.
  3. Run.
  4. Play Fetch.

Similarly, a cat has the capability to:

  1. Make noise
  2. Walk
  3. Run
  4. Prowl

In other words, you could also say: a dog is a noise-maker, walker, runner, and a play-fetcher. A cat, is also a noise-maker, walker, runner. However, a cat cannot play fetch (or doesn’t). A cat, however, is a prowler–unlike dogs.

Essentially, we define each “Object” with behaviors. By doing so, the abstractions for a given object is delegated to the higher-order role; a developer can determine that if a “thing” can make-noise, walk, run, play fetch–then it’s a dog!

Now, good abstractions are hard; you may need to adjust your language or interfaces as you bring in more objects and ideas–but composition makes it more apparent and modular when this happens. When you need to add or remove behaviors for a given object, you don’t affect anything else! For example, if we needed to add another animal –say, a bird– then now we have to think: “does it make sense to have a Fly() method on an animal? Since cats and dogs can’t fly, no–so we just have a bird have it’s own unique Fly() function. Sure, but what happens when you keep adding new animals? You’ll probably need to re-design your entire abstraction tree to classify different types of animals (land animals, sea animals, sky animals, etc)–and each time the inheritance tree grows, so does your complexity. With composition, it’s just as simple as adding a Flyer interface–and giving that capability to the bird.

highlight: It’s just as simple as adding a Flyer interface–and giving that capability to the bird.

And typically, I’d argue that great interfaces have a single method. For example, you could have the interface Printer with a single function Print() – any object that has the capability of printing now “becomes” a Printer! I’ll also note here that this type of language was a big source of my confusion when wrapping my head around composition; if a Dog has this Print() method, does this mean that a dog is a printer?? It doesn’t seem to make sense. That’s why I’m careful with the “is-a” relationship; in the composition world, it’s all about the capability of something. A dog object has the capability of “printing” whatever that may mean for a dog (prob just outputting the name). In either case, you can continue adding behaviors that make sense for that model and adjust it as requirements change. And by focusing on simple abstractions, the more complex objects build with the simple abstractions become easier to work with–and simpler! It becomes a flywheel effect.

Now I must also admit, with this approach, I found it quite difficult to really see a consolidated view of a given object and its behaviors–actually due to this modularity. Take this golang example:

type Blah struct {
    x string
    y string
}

func (b *Blah) DoOneThing() { /* something */ }

func (b *Blah) DoAnotherThing() { /* another thing */ }

func (b *Blah) Print() { /* printing thing */ }

func (b *Blah) FinalThing() { /* another final thing */ }

If I’m familiar with the package structure, have a good mental model of the codebase, and understand how everything is organized–I can probably tell you what interfaces this Blah struct uses; e.g. “This is a Printer and Thinger.” However, these interfaces are often scattered– and at first glance, while I know the individual behaviors, it’s not as easy as seeing all the interfaces that it gets its characteristics from–and determining what kind of object this is (perhaps this is just a golang problem or I’m just not as familiar with it yet?). Still, as long as it’s well organized, the modularity wins in the long run.

We’ve discussed how things become simpler when we change our frame of reference from inheritance to composition; for some, the composition ideas may be more natural than inheritance. But for others like myself who have long had this notion that inheritance is really all there is, it can be hard to truly understand the why and what of composition. For those in my shoes, I hope this article helped.

(p.s. if anyone knows how to solve my golang dilemma above, please reach out to me! I’m super curious)