Interface Segregation Principle in C# – SOLID Design Principles – Part 4
Overview
In our introduction to the SOLID Design Principles, we mentioned the Interface Segregation Principle as one of the five principles specified. In this post we are going to dive into this design principle with a very simple example in C#.
In the last post on the Liskov Substitution Principle, we utilized the Interface Segregation Principle to refactor our code. The code that we write in this post will be very simple as well and will take that code and introduce another segregation of our interfaces. I generally do not write posts that are “recaps” of previous posts, but our implementation from the last post certainly warrants a “recap” in this one.
The Interface Segregation Principle states that clients (classes) should not be forced to implement interfaces that they do not use. Well this certainly sounds reasonable, doesn’t it?
So if we start with our original design from the Liskov Substitution Principle post, here is our original design and code:
public class Engine { public IgnitionResult Start(IStarter starter) { return starter.Start(); } } public interface IStarter { string Brand { get; set; } string Model { get; set; } IgnitionResult Start(); } public class ElectricStarter : IStarter { public string Brand { get; set; } public string Model { get; set; } public IgnitionResult Start() { //code here to initiate the electric starter return IgnitionResult.Success; } } public class PneumaticStarter : IStarter { public string Brand { get; set; } public string Model { get; set; } public IgnitionResult Start() { //code here to initiate the pneumatic starter return IgnitionResult.Success; } } public class HydraulicStarter : IStarter { public string Brand { get; set; } public string Model { get; set; } public IgnitionResult Start() { //code here to initiate the hydraulic starter return IgnitionResult.Success; } } public enum IgnitionResult { Success, Failure }
In our last post, instead of a single Starter class, we created a distinct type for each type of Starter – electric, pneumatic, and hydraulic. Then we realized that in actual implementation, each type of starter had a different requirement for the object it used to actually work. The ElectricStarter utilized a Battery, the PneumaticStarter utilized an AirCompressor, and the HydraulicStarter utilized a HydraulicPump. When we actually wrote the code needed to make these work, we realized that our single interface just did NOT make sense. See the code below.
public interface IStarter { string Brand { get; set; } string Model { get; set; } Battery Battery { get; set; } AirCompressor Compressor { get; set; } HydraulicPump Pump { get; set; } IgnitionResult Start(); } public class ElectricStarter : IStarter { public string Brand { get; set; } public string Model { get; set; } public Battery Battery { get; set; } public AirCompressor Compressor { get { throw new NotImplementedException("An Electric Starter does not use an AirCompressor."); } set { throw new NotImplementedException("An Electric Starter does not use an AirCompressor."); } } public HydraulicPump Pump { get { throw new NotImplementedException("An Electric Starter does not use a HydraulicPump."); } set { throw new NotImplementedException("An Electric Starter does not use an HydraulicPump."); } } public IgnitionResult Start() { //code here to initiate the electric starter return IgnitionResult.Success; } } public class PneumaticStarter : IStarter { public string Brand { get; set; } public string Model { get; set; } public Battery Battery { get { throw new NotImplementedException("An PneumaticStarter does not use a Battery."); } set { throw new NotImplementedException("An PneumaticStarter does not use an Battery."); } } public AirCompressor Compressor { get; set; } public HydraulicPump Pump { get { throw new NotImplementedException("An PneumaticStarter does not use a HydraulicPump."); } set { throw new NotImplementedException("An PneumaticStarter does not use an HydraulicPump."); } } public IgnitionResult Start() { //code here to initiate the pneumatic starter return IgnitionResult.Success; } } public class HydraulicStarter : IStarter { public string Brand { get; set; } public string Model { get; set; } public Battery Battery { get { throw new NotImplementedException("A HydraulicStarter does not use a Battery."); } set { throw new NotImplementedException("A HydraulicStarter does not use an Battery."); } } public AirCompressor Compressor { get { throw new NotImplementedException("A HydraulicStarter does not use an AirCompressor."); } set { throw new NotImplementedException("A HydraulicStarter does not use an AirCompressor."); } } public HydraulicPump Pump { get; set; } public IgnitionResult Start() { //code here to initiate the hydraulic starter return IgnitionResult.Success; } } public class Battery { } public class AirCompressor { } public class HydraulicPump { } public enum IgnitionResult { Success, Failure }
Upon reviewing this code more, it became painfully apparent that it was quite bad and required some refactoring. So, per the Interface Segregation Principle, we broke out the interfaces that we actually needed and went from one interface to four interfaces. As a result, each Starter class implemented its own interface that interface had only what was needed for that type. Nice! Our final code from the last post is below:
public interface IStarter { string Brand { get; set; } string Model { get; set; } IgnitionResult Start(); } public interface IElectricStarter : IStarter { Battery Battery { get; set; } } public interface IPneumaticStarter : IStarter { AirCompressor Compressor { get; set; } } public interface IHydraulicStarter : IStarter { HydraulicPump Pump { get; set; } }
We picked the interface segregation is our starting point because it sets the stage for the changes that need to be made to the concrete classes.
So then we rewrote our Starter classes to implement their respective interfaces:
public class ElectricStarter : IElectricStarter { public string Brand { get; set; } public string Model { get; set; } public Battery Battery { get; set; } public IgnitionResult Start() { //code here to initiate the electric starter return IgnitionResult.Success; } } public class PneumaticStarter : IPneumaticStarter { public string Brand { get; set; } public string Model { get; set; } public AirCompressor Compressor { get; set; } public IgnitionResult Start() { //code here to initiate the pneumatic starter return IgnitionResult.Success; } } public class HydraulicStarter : IHydraulicStarter { public string Brand { get; set; } public string Model { get; set; } public HydraulicPump Pump { get; set; } public IgnitionResult Start() { //code here to initiate the hydraulic starter return IgnitionResult.Success; } }
And in doing that, we were introduced to the Interface Segregation Principle and used it to help us adhere to another principle. The overlap among the SOLID design principles is so nice!
In the next post, we are going to dive more deeply into the Dependency Inversion Principle. See the links below for all posts related to the SOLID Design Principles.
The SOLID Design Principles
The Single Responsibility Principle (SRP)
The Open-Closed Principle (OCP)
The Liskov Substitution Principle (LSP)
The Interface Segregation Principle (ISP)
The Dependency Inversion Principle (DIP)