Composition or Inheritance: What’s better?

Photo by Paymo on Unsplash

Composition or Inheritance: What’s better?

“What’s better: composition or inheritance?”

I froze. Time stood still for me. I diverted my gaze leftward through a tall window in the room. The view wasn’t particularly inspiring: I could see the grey concrete wall of the office building across the street, complete with sparsely scattered windows. But at least it was a bright afternoon. That would calm my nerves.

I had previously heard of the phrase “prefer composition over inheritance”. Much like other words of wisdom, they had gone in without being properly digested. Trying to hide the blank look on my face, I did my best to deliver the most convincing answer I could.

It wasn’t enough. My interviewer saw right through the act and glanced down at his notes as I’m sure he’d done many times before with other candidates. “Composition”, he said as he calmly looked back up. “It gives more flexibility.”

Shortly after, I left the interview feeling tired. I had been on edge for the past three hours, trying to anticipate the questions I would be asked and being conscious of the signals my body language was sending. I nearly missed my train stop on the way home, and later received an email to confirm my suspicions: I didn’t get the job.

But I ended up with something more.

I had answers (or catalysts to answers) to a whole host of questions I didn’t know I had.

It would take a little while longer, but I eventually landed in a project where I was building a new API. I was designing models for request responses, and I noticed a few responses shared common data structures. “Ah – inheritance is great for reducing duplicated code” I though. After all, they were all variations of the same response. They would all come from the same API too. And so I built a hierarchy making use of inheritance. The result was a set of beautiful and clean data models.

After the feature was complete, I moved onto the next set of requirements. “Ok”, I thought. “We need to cater for a slight variation of the existing model set. That’s easily doable – I just need to tweak the responses”. Except I couldn’t. To achieve what I wanted, I would need multiple inheritance. This is something that isn’t possible with C# classes. At that moment I finally understood why composition offers more flexibility.

I chose inheritance when I originally designed the data models for two reasons:

  • To follow the DRY principle (an acronym for Don’t Repeat Yourself).

  • The models had a relationship with each other; they were different versions of the ‘same’ response.

On reflection, the models weren’t as tightly related as I had first thought, and the DRY principle could have been achieved by using composition. Let’s look at the latter point with a different example to better understand this concept.

A problem with Inheritance

Let’s imagine we want to model a hierarchy of vehicles. Among others, we want to include:

  • A bicycle.

  • A truck.

  • A helicopter.

Let’s start off by having a Vehicle base class.

Bicycles and trucks both travel on wheels, but the dimensions and number often differ between the two types of vehicle. As they both travel on the road, let’s make a RoadVehicle class. This will subclass Vehicle.

Next, let’s think about what a truck and helicopter have in common. They both have an engine and use fuel. We can create a MotorVehicle class to model this, again as a subclass of Vehicle.

When we come to add classes for our vehicles, we will come across the problem shown in Image 1.

Image 1: A Truck cannot be a subclass of both RoadVehicle and MotorVehicle

A truck is both a road vehicle and a motor vehicle. However, C# does not allow multiple class inheritance; it cannot be modelled like this in our class hierarchy. We also cannot solve this by rearranging RoadVehicle and MotorVehicle in the data structure:

  • A MotorVehicle isn’t necessarily a RoadVehicle: helicopters don’t travel on the road.

  • A RoadVehicle isn’t necessarily a MotorVehicle: bicycles typically don’t have a motor engine.

We could start introducing further subclasses such as RoadMotorVehicle but this introduces problems itself:

  • More complexity is introduced into the model because of the additional classes.

  • Multiple inheritance isn’t allowed, so we’d have to copy over the Fuel and Engine properties from MotorVehicle.

Modelling with Composition

We can increase the flexibility of our model by using composition. Instead of creating the RoadVehicle and MotorVehicle subclasses, let’s make Bicycle, Truck, and Helicopter direct subclasses of Vehicle. Let’s also create new classes to capture information about a vehicle’s wheels and engine. By using these to compose our fully derived vehicle classes, we gain two benefits:

  • We won’t lose data when removing the RoadVehicle and MotorVehicle classes.

  • We won’t have to copy the wheel and engine related properties to the fully derived vehicle classes, thus respecting the DRY principle.

Image 2 shows the result of using composition in place of inheritance. It might seem like we are losing the benefits of polymorphism by streamlining the object model. However, we can get them back by creating interfaces that we can use to mark classes as having a wheelset, an engine, or both.

Image 2: An updated object model using composition instead of inheritance

Summary

Composition and inheritance are two ways that you can use to build an object model. Both allow you to incorporate polymorphism – either through subclasses, or by interface implementation. Composition can offer more flexibility by allowing you to piece your models together with all the parts that you need. This could be more difficult (or impossible) when using inheritance because multiple class inheritance is not permitted in C#.

However, the semantics of each approach differs: each will give your model a different meaning. You shouldn’t automatically choose one by default because of its popularity or because people say so. It’s important that you use your unique knowledge of the system that you’re building to choose the best approach for your data.