DEV Community

Mahi
Mahi

Posted on

Composition over Inheritance through example: ICE vs EV

Recently, I wrote about why I don't seal my classes and included an example of a Car class that represented an internal combustion engine (ICE) vehicle—due to the designer not foreseeing future technologies. Years later someone needed an electric vehicle and had to inherit it from the ICE Car to keep things backwards compatible and to avoid code duplication.

Example misdesign: EV inherits Car/ICE

This example made sense in the context of inheriting due to historical design flaws, but if we were to redesign the application from scratch then this obviously wouldn't be the ideal solution. But what would be?

The common base class approach

The first idea most people think of is to separate the ICE and EV classes and have them inherit from a common base class:

EV and ICE both inherit from a common  raw `Car` endraw  base class

This is already a much better approach, and I would be happy to join such project. But there are flaws, especially when new technologies emerge (hydrogen, nuclear...).

First of all, you are often faced with the issue of sub types needing most of the common functionality, but not all of it:

Every sub type has one unique feature implementation.

In this situation you mostly have bad choices available, the two most common ones being:

  1. Keep the base class's implementation minimal and delegate the job to sub types. This often leads to numerous template and abstract methods, with most sub types just duplicating the same implementation for them.
  2. Include the most common implementations into the base class, then have the exceptional sub types override this implementation. This often ends up breaking the Liskov substitution principle, and is smelly anyways.

As you can tell, neither of these approaches are ideal. It's still a clear improvement over the initial approach, but we can do better.

The composition approach

My ideal approach is to use composition together with polymorphism.

Step 1: compose a Car base class out of its smaller parts:

class Car {
  PowerTrain powerTrain; // Motor, engine...
  EnergyStorage energyStorage; // Gas tank, battery pack...
  EnergyAccessPoint eap; // Charging port, refueling hatch...
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Implement varying sub types of these smaller parts:

ICE and ElectricMotor implement PowerTrain, GasTank and BatteryPack implement EnergyStorage

Step 3: Compose the smaller parts into new vehicles:

var iceVehicle = new Car(new ICE(), new GasTank(), ...);
var ev = new Car(new ElectricMotor(), new BatteryPack(), ...);
Enter fullscreen mode Exit fullscreen mode

Step 4: Automate the creation process with creational patterns:

var iceVehicle = iceVehicleFactory.create();
var ev = evFactory.create();
Enter fullscreen mode Exit fullscreen mode

How deep to go?

A good question some might have is:

Why implement sub types of these power trains and energy storages when you could compose them out of smaller parts as well?

The answer is simple: compose as deep as your domain problem requires you to.

  • If you're building a mechanic simulator, then yes, constructing the internal combustion engine out of even smaller parts is very much something you will need to implement.
  • But if you're implementing an open-world video game where the vehicles are just a minor part of the gameplay and the hoods of the cars will never be popped open, then you probably would have been fine with the initial approach of inheriting EV and ICE directly from the Car base class.
  • And sometimes with simple applications you might not need the car sub types at all, as simply sub typing the car's appearance might be enough:
var ev = new Car(new EvSound(), new Sprite("ev.png"));
var iceVehicle = new Car(new IceSound(), new Sprite("icev.png"));
Enter fullscreen mode Exit fullscreen mode

This is where experience and domain knowledge come into play: you have to stop the composing somewhere to avoid implementing atoms into your project, but you shouldn't stop too early to keep things modular and extendable. I can only provide you with the necessary tools to build great software, you're going to have to use them yourself. Don't be afraid to experiment and fail, that's how you learn.

Top comments (0)