DEV Community

Naomi Dennis
Naomi Dennis

Posted on

Introduction to the Interface Segregation Principle

The interface segregation principle is the I in SOLID principles. According Robert Martin's Agile Software Development: Principles, Patterns and Practices, the principle is defined as, “Clients should not be forced to depend on methods that they do not use.” In other words, classes should not have access to behavior it does not use. Let’s look at a Python example.

We’ve been hired to create a game where the player sets up entertainment systems. Each piece of the system (television, dvd player, game console, etc.) uses a specific cord to connect to another device. We know that a TV uses an HDMI cord to connect to a game console, and a dvd player uses RCA cords to connect to a TV. Both the game console and TV connects to a router via an ethernet cord so they can access the internet. And lastly, all the devices are connected to the wall via a power cord so they can turn on.

Say we create a class to handle all the connections listed above.

class EntertainmentDevice:
  def _connectToDeviceViaHDMICord(self, device): None
  def _connectToDeviceViaRCACord(self, device): None
  def _connectToDeviceViaEthernetCord(self, device): None 
  def _connectDeviceToPowerOutlet(self, device): None 
Enter fullscreen mode Exit fullscreen mode

Now, all we have to do is extend this interface to our device classes.

class Television(EntertainmentDevice):
  def connectToDVD(self, dvdPlayer):
    self._connectToDeviceViaRCACord(dvdPlayer)

  def connectToGameConsole(self, gameConsole):
    self._connectToDeviceViaHDMICord(gameConsole)

  def plugInPower(self): 
    self._connectDeviceToPowerOutlet(self)


class DvdPlayer(EntertainmentDevice):
  def connectToTV(self, television):
    self._connectToDeviceViaHDMICord(television)

  def plugInPower(self):
    self._connectDeviceToPowerOutlet(self) 


class GameConsole(EntertainmentDevice):
  def connectToTV(self, television):
    self._connectToDeviceViaHDMICord(television)

  def connectToRouter(self, router):
    self._connectToDeviceViaEthernetCord(router)

  def plugInPower(self):
    self._connectDeviceToPowerOutlet(self)


class Router(EntertainmentDevice):
  def connectToTV(self, television):
    self._connectToDeviceViaEthernetCord(television)

  def connectToGameConsole(self, gameConsole):
    self._connectToDeviceViaEthernetCord(gameConsole)

  def plugInPower(self):
    self._connectDeviceToPowerOutlet(self)

Enter fullscreen mode Exit fullscreen mode

Looking at each class, you’ll notice that every class only uses a method or two from EntertainmentSystemDevice. For instance, DVDPlayer only uses #connectToTV and #connectToPowerOutlet methods. There’s also a bit of duplication across the classes. Each class uses #connectToPowerOutlet. First, let’s work on separating the EntertainmentSystemDevice interface.

We can extract #connectToDeviceViaEthernetCord and create a class dedicated to internet devices.

class InternetDevice: 
  def _connectToDeviceViaEthernetCord(self, device): None
Enter fullscreen mode Exit fullscreen mode

Both HDMI and RCA cords are used for transmitting audio and video data. The difference between the two, is RCA handles analog signals, while HDMI handles digital signals. We can use this difference to create two classes, AnalogDevice, and DigitalDevice that handles devices connected by RCA and HDMI respectively.

class AnalogDevice:
  def _connectToDeviceViaRCACord(self, device): None 

class DigitalDevice:
  def _connectToDeviceViaHDMICord(self, device): None 
Enter fullscreen mode Exit fullscreen mode

Our EntertainmentSystemDevice class now only has the #connectToPowerOutlet method. Because it only handles behavior for connecting a device to an outlet, the class name should change to reflect that. We can also define a #plugInPower method that uses the #connectDeviceToPowerOutlet method. Let’s rename EntertainmentSystemDevice to WallPoweredDevice.

class WallPoweredDevice: 
  def plugInPower(self, device):
    self.__connectToDeviceViaPowerOutlet(device)

  def __connectToDeviceViaPowerOutlet(self, device): None 
Enter fullscreen mode Exit fullscreen mode

Our classes are looking pretty good.

class Television(WallPoweredDevice, InternetDevice, AnalogDevice, DigitalDevice):
  def connectToDVD(self, dvdPlayer):
    self._connectToDeviceViaRCACord(dvdPlayer)

  def connectToGameConsole(self, gameConsole):
    self._connectToDeviceViaHDMICord(gameConsole)

  def connectToRouter(self):
    self._connectToDeviceViaEthernetCord(self)


class DvdPlayer(WallPoweredDevice, AnalogDevice):
  def connectToTV(self, television):
    self._connectToDeviceViaRCACord(television)


class GameConsole(WallPoweredDevice, InternetDevice, DigitalDevice):
  def connectToTV(self, television):
    self._connectToDeviceViaHDMICord(television)

  def connectToRouter(self):
    self._connectToDeviceViaEthernetCord(self)


class Router(WallPoweredDevice, InternetDevice):
  def connectToTV(self, television):
    self._connectToDeviceViaEthernetCord(television)

  def connectToGameConsole(self, gameConsole):
    self._connectToDeviceViaEthernetCord(gameConsole)
Enter fullscreen mode Exit fullscreen mode

In the end, we were able to refactor our device interfaces to only have access to behavior it uses. Because the behavior is separated based on the sole behavior the class supports, the extended classes act as a description of the subclass. For example, we know that a router uses the internet and needs to be plugged into the wall without having to look at the inner functionality of the class.

We can take this idea one step further and say, classes should only use objects where it uses the entire interface of said object. This is a bit difficult to show in a dynamically typed language like Python, so we will convert our Router class to Java and convert our InternetDevice class to a Java interface.

interface InternetDevice{
  public void connectToDeviceViaEthernetCord(InternetDevice device); 
}

class Router extends WallPoweredDevice implements InternetDevice{
    public void connectToDeviceViaEthernetCord(InternetDevice device){}
}
Enter fullscreen mode Exit fullscreen mode

In this example, Router can only connect to another device that uses the internet. Because we want it to only have access to behavior associated with an InternetDevice, we tell it to only understand objects that use the InternetDevice interface.

This way, even if we implement an InternetDevice to other classes like Television or GameConsole, Router's #connectToDeviceViaEthernetCord will only have access to the functions associated with InternetDevice.

To learn more about Java interfaces, follow the link here.

When adding additional features to the program, such as connecting to the internet not only via ethernet, but also via Wi-Fi, be sure to pay attention to the cohesion of the methods. How well do the methods relate to each other? If a class were given the set of behavior defined in the base class or acknowledged in an interface, would the class actually need the behavior? By answering these questions we are able to create reusable, general classes that are easier to delegate.

Gists:

Top comments (1)

Collapse
 
sahasrara62 profile image
prashant rana

good explanation, it would be great if you add UML diagram to tell this