How To Design An Abstraction
How do we design a useful abstraction? Our notion of a good abstraction evolves as we encounter more specific types.
OK, let’s investigate with an example:
Say we have a system modelling cars. Initially, the program only caters to petrol cars—we create a
PetrolCar must keep track of how much petrol remains in the tank. To that end, we could create a property called
PetrolCar petrolCar = new PetrolCar(); petrolCar.FillUp(); petrolCar.LitresOfPetrolRemaining.Should().Be(50); petrolCar.Drive(100); petrolCar.LitresOfPetrolRemaining.Should().Be(40);
OK, that works well.
We receive a change request: Our application should also be able to model Diesel cars, and these must be usable wherever we currently accept
It’s time for us to do some designing. We could have a new type,
DieselCar. Since we want to be able to use
DieselCar interchangeably, we need a common abstraction, an interface or abstract class, that we shall call
Car will contain the declarations for common methods and properties:
void Drive(int kilometres)
One of these looks a bit off.
LitresOfPetrolRemaining is fine for
PetrolCar but doesn’t work well for
var dieselRemaining = dieselCar.LitresOfPetrolRemaining; // ?!?
OK, we need a common abstraction that will work for
DieselCar. How about
Car petrolCar = new PetrolCar(); petrolCar.FillUp(); var petrol = petrolCar.LitresOfFuelRemaining; Car dieselCar = new DieselCar(); dieselCar.FillUp(); var diesel = dieselCar.LitresOfFuelRemaining;
Electric cars are becoming popular, and requirements have changed again. We have been asked to modify the system to include electric cars. We will include a new type,
ElectricCar, that extends abstraction
Electric cars do not use liquid fuel the way petrol and Diesel cars do. For propulsion, they use electric charge stored in batteries. The existing abstraction in
Car does not work for
var electricCharge = electricCar.LitresOfFuelRemaining; // ?!?
Yuch—that will not do. We will need to come up with a better abstraction that works for all types of
Yes, that may work. Fuel is still fuel even when we are holding it in electric charge. Using percentages (0% – empty, 100% – full) instead of litres allows us to use all fuel types. Nice!
Let’s see how it works out with the different kinds of
Car petrolCar = new PetrolCar(); petrolCar.FillUp(); petrolCar.PercentOfFuelRemaining.Should().Be(100); petrolCar.Drive(100); petrolCar.PercentOfFuelRemaining.Should().Be(80); Car dieselCar = new DieselCar(); dieselCar.FillUp(); dieselCar.PercentOfFuelRemaining.Should().Be(100); dieselCar.Drive(300); dieselCar.PercentOfFuelRemaining.Should().Be(60); Car electricCar = new electricCar(); electricCar.FillUp(); electricCar.PercentOfFuelRemaining.Should().Be(100); electricCar.Drive(200); electricCar.PercentOfFuelRemaining.Should().Be(50);
It can be tricky to design an abstraction that works well with all specific types. However, we can usually achieve it by reflecting on the common aspects of the various particular implementations.