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. This example made sense in the context of inheriting due to historical design flaws, which are so common that they were explicitly mentioned by the Gang of Four: Ideally all reuse can be achieved by assembling existing components, but in practice inheritance is often needed to make new ones. But if we were to redesign the application from scratch then this obviously wouldn't be the ideal solution. But what would be? The inheritance 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: 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. In this situation you mostly have bad choices available, the two most common ones being: keep the base class minimal, often leading to numerous template and abstract methods with most sub types just duplicating the same implementation for them, or include the most common implementation in the base class, then have the exceptional sub types override this implementation, often breaking the Liskov substitution principle. 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 door... } Step 2: Implement varying sub types of these smaller parts: 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(), ...); Step 4: Automate the creation process with creational patterns: var iceVehicle = iceVehicleFactory.create(); var ev = evFactory.create(); 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 making a mechanic simulator, then yes, constructing the internal combustion engine out of even smaller parts is very much something you will need to implement. 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 output might be enough: var ev = new Car(new EvSound()); var iceVehicle = new Car(new IceSound()); 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.
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.
This example made sense in the context of inheriting due to historical design flaws, which are so common that they were explicitly mentioned by the Gang of Four:
Ideally all reuse can be achieved by assembling existing components, but in practice inheritance is often needed to make new ones.
But if we were to redesign the application from scratch then this obviously wouldn't be the ideal solution. But what would be?
The inheritance 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:
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.
In this situation you mostly have bad choices available, the two most common ones being:
- keep the base class minimal, often leading to numerous template and
abstract
methods with most sub types just duplicating the same implementation for them, or - include the most common implementation in the base class, then have the exceptional sub types override this implementation, often breaking the Liskov substitution principle.
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 door...
}
Step 2: Implement varying sub types of these smaller parts:
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(), ...);
Step 4: Automate the creation process with creational patterns:
var iceVehicle = iceVehicleFactory.create();
var ev = evFactory.create();
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 making a mechanic simulator, then yes, constructing the internal combustion engine out of even smaller parts is very much something you will need to implement.
- 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
andICE
directly from theCar
base class. - And sometimes with simple applications you might not need the car sub types at all, as simply sub typing the car's output might be enough:
var ev = new Car(new EvSound());
var iceVehicle = new Car(new IceSound());
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.
What's Your Reaction?