Naomi Dennis
Posted on November 15, 2019
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
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)
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
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
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
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)
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){}
}
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:
Posted on November 15, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
February 27, 2023