It’s almost the end of the year and I am reviewing key software development principles that will enable me to be a true software craftsman. One of the first is the Open-Closed Principle. The Open-Closed Principle can be briefly explained:

Open for extension, closed for modifications

This very brief definition needs an example. I’m going to write a few classes that explains this concept. I’ll be building an app that tracks efficiency for different types of vehicles. I’m going to start with my initial (and naive) implementation.

public class Car {

  private final float fuelCapacityInGallons;
  private final float milesPerGallon;

  public Car(float fuelCapacityGallons, float milesPerGallon) {
    this.fuelCapacityGallons = fuelCapacityGallons;
    this.milesPerGallon = milesPerGallon;
  }

  public float getFuelCapacityGallons() {
    return fuelCapacityInGallons;
  }

  public float getMilesPerGallon() {
    return milesPerGallon;
  }
}

Now I have some logic that can calculate the range of a fleet of vehicles:

public class VehicleUtil {

  public float getFleetRange(List<Object> vehicles) {
    float fleetRange;

    for (Object vehicle : vehicles)
      
      if (vehicle instanceof Car) {
        Car car = (Car) vehicle;
        fleetRange += car.getFuelCapacityGallons() * car.getMilesPerGallon();
      } else {
        throw new IllegalVehicleException();
      }
    }
    return fleetRange;
  }
}

For context, this logic is used in the following manner.

Car pickup = new Car(30.0f, 12.0f);
Car sedan = new Car(14.5f, 36.0f);
float range = VehicleUtil.getFleetRange(Arrays.asList(pickup, sedan));

This preceding, example, is simple enough (and brittle). Like most software, in future, we’ll be adding new types of vehciles. I’m going to add an electric car. I’m going to create a new class called ElectricCar.

public class ElectricCar {

  private final float batteryCapacityWattHours;
  private final float milesPerWattHour;

  public ElectricCar(float batteryCapacityWattHours, float milesPerWattHour) {
    this.batteryCapacityWattHours = batteryCapacityWattHours;
    this.milesPerWattHour = milesPerWattHour;
  }

  public float getBatteryCapacityWattHours() {
    return batteryCapacityWattHours;
  }

  public float getMilesPerWattHour() {
    return milesPerWattHour;
  }
}

I would need to update my fleet range calculation method:

public float getFleetRange(List<Object> vehicles) {
  float fleetRange;

  for (Object vehicle : vehicles)
    if (vehicle instanceof Car) {
      Car car = (Car) vehicle;
      fleetRange += car.getFuelCapacityGallons() * car.getMilesPerGallon();
    } else if (vehicle instanceof ElectricVehicle) {
      ElecticCar electicCar = (ElecticCar) vehicle;
      fleetRange += electicCar.getBatteryCapacityWattHours() * electricCar.getMilesPerWattHour();
    } else {
      throw new IllegalVehicleException();
    }
  }
  return fleetRange;
}

For every new type of vehicle we want to add to our app, we’d need to define it as a class, then update the functionality that calculates the range of a vehicle fleet in VehicleUtil.getFleetRange(). This is not sustainable in the long run, especially as the app matures. This also prevents us from releasing this code as a library other apps can use because it needs to be modified and is also hard to extend.

So, let’s revisit the definition of the Open-Closed Principle and how it would apply to our example.

Open for extension, closed for modification

Applied to our example:

  • Open for extension, we should design our system in a way that permits us to easily add new vehicle types as needed
  • Closed for modification, we should not need to modify functionality in the core our VehicleUtil class for every vehicle we add

Let’s apply this to our example. First, let’s pull out the high-level concept of every vehicle type having a some type of range calculation. Let’s write an interface!

public interface Vehicle {
  float getRange();
}

The Vehicle interface allows us abstract the specifics of calculating the range from VehicleUtil.getFleetRange();

Our two types of vehicles can be refactored.

public class Car implements Vehicle {

  private final float fuelCapacityInGallons;
  private final float milesPerGallon;

  public Car(float fuelCapacityGallons, float milesPerGallon) {
    this.fuelCapacityGallons = fuelCapacityGallons;
    this.milesPerGallon = milesPerGallon;
  }

  @Override
  public float getRange() {
    return fuelCapacityGallons * milesPerGallon;
  }
}
public class ElectricCar implements Vehicle {

  private final float batteryCapacityWattHours;
  private final float milesPerWattHour;

  public ElectricCar(float batteryCapacityWattHours, float milesPerWattHour) {
    this.batteryCapacityWattHours = batteryCapacityWattHours;
    this.milesPerWattHour = milesPerWattHour;
  }

  @Override
  public float getRange() {
    return batteryCapacityWattHours * milesPerWattHour;
  }
}

This refactoring leads to a very simple fleet range calculation:

public float getFleetRange(List<Vehicle> vehicles) {
  float fleetRange;

  for (Vehicle vehicle : vehicles)
    fleetRange += vehicle.getRange();
  }
  return fleetRange;
}

It gets even simpler if you write it in a functional way:

public float getFleetRange(List<Vehicle> vehicles) {
  return vehicles.stream().map(Vehicle::getRange).sum();
}

In use:

Vehicle pickup = new Car(30.0f, 12.0f);
Vehicle sedan = new Car(14.5f, 36.0f);
Vehicle electricCar = new ElectricCar(90.0f, 4.0f);
float range = VehicleUtil.getFleetRange(Arrays.asList(pickup, sedan, electricCar));

Now our code is open for extension (via the Vehicle interface) and closed for modification, thus adopting the Open-Closed Principle.

🧇