An introduction to Interface Segregation Principle (ISP)
Last week I wrote a post about Dependency Injection (DI) with some basic examples in C#. Today I would like to write and offer a simple example of what is known as Interface Segregation Principle (ISP) which can happen if you are not careful when using and implementing interfaces in your code.
What is Interface Segregation Principle (ISP)?
The goal behind ISP is that no client consuming an interface should be forced to depend on methods it does not use. For example, you might have a class that implements an interface called IPersist. Let’s call it ReservationDatabase, and this class has code that persists a hotel reservation details to a database. IPersist contains the public method declaration of the method in ReservationDatabase that performs the persistence to the database. IPersist looks like this:
public interface IPersist { bool Save(ReservationDetails reservation); }
As you can see, this interface is very simple and it only has one responsibility. You could implement this from several classes that have a public method labeled “Save” to save some data to a database, to a XML document, to other data repository, etc… Everything is good so far.
Now, let’s say that you have a new class for logging some of the tasks related to the reservation, and you would like this ReservationLog class to implement IPersist as well as one of the functions of ReservationLog is to save this to a log repository. To implement IPersist you simply add the following at the top of your class:
public class ReservationLog : IPersist
This is the entire ReservationLog class that now implements IPersist:
public class ReservationLog : IPersist { public bool Save(ReservationDetails reservation) { // imagine some code here to perist data to database... return true; } public void Log(ReservationDetails reservation) { // imagine some code here to log to a log repository } public void SendNotification(ReservationDetails reservation) { // imagine some code here that notifies about logging } }
Now, since we are already implementing IPersist in our ReservationLog class which has two other methods, we update IPersist so it includes the two other methods from ReservationLog because it will be easier for the class or classes consuming IPersist to access these methods when logging is needed. So the updated IPersist interface looks like this now:
public interface IPersist { bool Save(ReservationDetails reservation); void Log(ReservationDetails reservation); void SendNotification(ReservationDetails reservation); }
What we did above might not look like a big change but it is. What we did above is wrong if you didn’t first carefully consider the consequences of implementing IPersist in a class that has multiple public methods and then adding those new method declarations to IPersist. The main responsibility of IPersist was until now to abstract the details from classes that allowed to save reservation details to some data repository. By implementing IPersist on ReservationLog and adding all of its public method declarations to this interface, the result is broken code in all of the classes that are implementing IPersist IF they don’t have public methods that match with those in the IPersist interface.
For example, the original ReservationDatabase class looks like this:
public class ReservationDatabase : IPersist { public bool Save(ReservationDetails reservation) { // imagine some code here to persist data to database... return true; } }
This code won’t compile anymore as this class and perhaps other classes in our code that implement IPersist, do not have methods that implement the two new declarations we added to IPersist:
void Log(ReservationDetails reservation); void SendNotification(ReservationDetails reservation);
The common and not recommended “fix” for this is to “implement” these methods in all classes that implement IPersist and then just return a not implemented exception if anyone ever uses it. Using our example, our ReservationDatabase will look like this if we apply the quick fix:
public class ReservationDatabase : IPersist { public bool Save(ReservationDetails reservation) { // imagine some code here to persist data to database... return true; } public void Log(ReservationDetails reservation) { throw new NotImplementedException(); } public void SendNotification(ReservationDetails reservation) { throw new NotImplementedException(); } }
The code above is a clear example of breaking the interface segregation principle, which advises not to force any client to depend on methods that it does not use… like the two methods in our illustration above. If you ever see code like this in your projects, where the methods are implementing an interface and all its methods and just throwing NotImplementedException errors on some of them, the code is breaking the interface segregation principle. It is bad as the fix is quite simple and by fixing it you will end up with more maintainable code that can be easier to read and debug.
How to avoid violating the ISP principle?
It is actually very easy, it does require a bit more work but in the end you will have better organized code, you will be following this principle and a few more such as single responsibility and the universal KISS principle.
If you wanted to implement IPersist in your ReservationLog class you can still do it. However, do not update IPersist to accomodate for the other public methods in your ReservationLog class, instead create a new interface, implement it and use it to add any log related methods from your class to it. This is what this new interface might look like:
public interface ILog { void Log(ReservationDetails reservation); void SendNotification(ReservationDetails reservation); }
Now you can implement ILog in your ReservationLog class. You can do this by adding a comma and typing the name of it after IPersist which was implemented earlier.
public class ReservationLog : IPersist, ILog
That’s it! by doing this you can still implement IPersist without having to change other classes that might implement it and you also create a new interface that can be used by any classes wanting to implement the Log and SendNotification methods.
You could probably remove the SendNotification method and abstract it to yet another interface named INotify to keep your code simple, organized and maintainable.
Also, if your code is already violating ISP and you have interfaces that already have multiple method declarations then your should consider fixing this by refactoring your code using the Adapter pattern, take a look at this to see a clear example of it.
Hope this is useful to you. Happy coding!