Interface Segregation Principle

Younes EspirituYounes Espiritu
3 min read

↝ This principle states that you have to make fine-grained interfaces that are client-specific. Again, what do we mean by that? Let's break it down. There are two main ideas in its name: Interface and segregation. What is an interface? What do we mean by segregation?

An interface in TypeScript is a blueprint that defines the structure of a class. It specifies required properties and methods that the class must have. Then, what about segregation? It refers to the act of separating something into distinct parts. So when we combine these two, it means that we are separating our interfaces into distinct parts. But why do we do that? When an interface gets larger and a class implements it, there are times when a class doesn't need some methods or properties. Let's take a look at the TypeScript code below:

interface Vehicle {
  // Shared properties between ElectricCar and GasCar
  numberOfWheels: number;
  getModel(): string;

  // Properties specific to ElectricCar
  batteryRangeInKm: number;
  recharge(): void;

  // Properties specific to GasCar (not implemented in ElectricCar)
  fuelCapacityInLiters: number;
  refuel(liters: number): void;
}
class ElectricCar implements Vehicle {
  numberOfWheels: number = 4;
  batteryRangeInKm: number = 300;

  getModel(): string {
    return "Tesla Model S";
  }

  recharge(): void {
    console.log("Charging...");
  }

  // These methods are not implemented (causing errors)
  fuelCapacityInLiters: number = 0; // Electric car doesn't have fuel capacity
  refuel(liters: number): void {
    throw new Error("Electric car cannot be refueled");
  }
}

// This will cause errors because ElectricCar cannot implement fuel-related methods
const Tesla = new ElectricCar();
Tesla.getModel(); // This works fine
Tesla.recharge(); // This works fine
Tesla.fuelCapacityInLiters; // Error: Property does not exist
Tesla.refuel(50); // Error: refuel is not a function

As you can see from the above code, our interface specifies fuelCapacityInLiters and refuel, which our ElectricCar doesn't actually use. Interfaces in TypeScript force our class to implement these features, or else it will throw an error when we're building the code for production. This break the interface segregation principle—why? Because the interface is now handling multiple responsibilities, which leads to methods and properties being unimplemented. To fix this, let's look at the code below:

interface ElectricVehicle {
  numberOfWheels: number;
  getModel(): string;
  batteryRangeInKm: number;
  recharge(): void;
}

interface GasVehicle {
  numberOfWheels: number;
  getModel(): string;
  fuelCapacityInLiters: number;
  refuel(liters: number): void;
}
class ElectricCar implements ElectricVehicle {
  numberOfWheels: number = 4;
  batteryRangeInKm: number = 300;

  getModel(): string {
    return "Tesla Model S";
  }

  recharge(): void {
    console.log("Charging...");
  }
}

class GasCar implements GasVehicle {
  numberOfWheels: number = 4;
  fuelCapacityInLiters: number = 60;

  getModel(): string {
    return "Toyota Camry";
  }

  refuel(liters: number): void {
    console.log(`Refueling ${liters} liters`);
  }
}

// Now you can use the interfaces without errors
const tesla = new ElectricCar();
tesla.getModel(); // Works
tesla.recharge(); // Works

const camry = new GasCar();
camry.getModel(); // Works
camry.refuel(50); // Works

What did we do here? We created separate interfaces, ElectricVehicle for electric cars and GasVehicle for gas cars. Each interface only defines the properties relevant to its type. Then, we implemented the respective interfaces in ElectricCar and GasCar classes. Now, ElectricCar only needs to implement the methods related to electric vehicles, and GasCar implements methods specific to gas vehicles.

You may wonder, what's the difference between the Liskov Substitution Principle (LSP) and Interface Segregation Principle (ISP)? In LSP, it talks about the relationship between the base class and subclass, where the subclass is required to implement all of the features and must obey the parent because it needs to 'substitute' the parent. This means methods from the subclass can work as if it's the base class. On the other hand, ISP talks about the relationship between the interface and client, where if the client needs to consume an interface, it needs to ensure that all of the properties and methods are implemented.

To sum up, LSP establishes a contract that a child must follow the parent and extend its abilities, while ISP aims to avoid unimplemented methods from the interface. Thank you and happy coding!

0
Subscribe to my newsletter

Read articles from Younes Espiritu directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Younes Espiritu
Younes Espiritu

I am a front-end developer from the Philippines who loves to do open-source projects and engage with a community that also loves web development. I enjoy reading blogs and books to keep my knowledge up-to-date.