Have you ever reviewed a code where you have a class hierarchy but stumbled upon many methods that throw a NotImplementedException?
If so, then the Interface Segregation Principle in C# could eliminate the code that throws those exceptions.
The interface segregation principle (ISP) in C# is a design guideline for class hierarchies. It states that no class should be forced to implement a method in an interface that it doesn’t use. If there is no reason to use some method, an implementing class shouldn’t have to implement it.
In case you want to learn more about the Interface Segregation Principle and see some code examples, then keep reading!
What is Interface Segregation Principle in C#? – Deep dive
There are a few good reasons for keeping your code interfaces as clean as possible. One of them is that it will make it easier for you to use other people’s code. If you have a class with an interface with million methods, it will be impossible (and probably not even desirable) to use it everywhere you want to use it.
Not to mention that if you have that kind of interface, it will be a nightmare to implement and maintain it. The interface segregation principle is all about breaking your classes into smaller, more manageable pieces.
The Interface Segregation Principle (ISP) states that a class should not be forced to depend on methods it does not use. In a nutshell, ISP encourages developers to break apart interfaces that are very big and only expose the most commonly used methods.
The point is to group methods that belong together into a common interface.
One example of the Interface Segregation Principle violation: if an interface for a bank account has a withdrawal method, but you don’t want to withdraw money, then the interface should be broken into an interface for depositing and one for withdrawing. This allows developers to use only the interfaces they need.
The Interface Segregation Principle is a part of SOLID design principles.
Simple Example of Interface Segregation Principle in C#
Ok, time for some examples.
Let’s say there is an interface called ILaundry:
interface ILaundry
{
void WashClothes();
void DryClothes();
}
And the two following implementations:
class ComboWashingMashine : ILaundry
{
public void WashClothes()
{
//do the work
}
public void DryClothes()
{
//do the work
}
}
class SimpleWashingMashine : ILaundry
{
public void WashClothes()
{
//yep, I can do that as well
}
public void DryClothes()
{
//oops, no can't do
throw new NotImplementedException();
}
}
As you can see, the problem is in the DryClothes method. The concrete class SimpleWashingMashine doesn’t know how to dry clothes, and therefore it throws the exception. This means that having one single interface is not optimal in this case.
The solution would be to split the interface into two fine-grained interfaces:
interface IWashLaundry
{
void WashClothes();
}
interface IDryLaundry
{
void DryClothes();
}
And then, the second class can only implement the IWashLaundry interface:
class ComboWashingMashine : IWashLaundry, IDryLaundry
{
public void WashClothes()
{
//do the work
}
public void DryClothes()
{
//do the work
}
}
class SimpleWashingMashine : IWashLaundry
{
public void WashClothes()
{
//yep, I can do that as well
}
}
Therefore the SimpleWashingMashine class doesn’t have to implement the unneeded method.
How the Interface Segregation Principle relates to other SOLID principles
After seeing an example of ISP in the code, let me quickly explain how the Interface Segregation Principle relates to other SOLID principles.
Single Responsibility Principle
The Single Responsibility Principle, often abbreviated as the SRP, is one of the most popular and fundamental object-oriented programming principles. It states that every class should have one and only one responsibility. This principle encourages developers to implement a class or a module that solves only one problem.
If you have interfaces that have one responsibility, then there is a high chance that they are also following the ISP.
Open-Closed Principle
The Open-Closed Principle states that classes should be open for extension but closed for modification. In other words, the class should allow its behavior to be extended without modifying its source code, but we should not modify its source code to change its behavior.
If it happens for you that you need to change an interface signature because you need to change different areas of your code, then think about whether your interface is doing too much work. What you should do instead is to have a separate interface for each module in the code where the original interface is used.
Liskov Substitution
The Liskov Substitution Principle is a test that is used when you are writing an object-oriented program. The principle aims to ensure that if code works with a base class, it should also work with its sub-classes. For example, if code works with a specific car model that acts as a parent class, it should also work with a car model derived from the original.
How can you know that an interface is violating the Liskov substitution? Search for NotImplementedException. If a class can’t implement one interface completely, this probably means that the hierarchy probably doesn’t follow the Liskov substitution principle.
Dependency Inversion Principle
One of the key concepts of object-oriented programming is decoupling the dependencies between layers of a program. Dependency Inversion SOLID principle is based on the principle of substitutability, which states that objects (or classes) that use other objects (or classes) should be replaceable with instances of those objects (or classes). In other words, a class should not be dependent on the specific class of objects that it uses but rather should only be dependent on the interfaces that those objects provide.
For example, let’s say you have an interface that has 15 methods. But several classes that have a dependency on it only use five of them, then you should consider extracting a new interface from the original so that the new interface only contains those five methods those classes use.
How to break a large interface into smaller interfaces? Real-world example of Interface Segregation Principle in C#
Let’s explore the following fat interface:
public interface ICryptoService
{
Task<List<Coin>> GetEthereumLatestPrices();
Task<List<Coin>> GetBitcoinLatestPrices();
Task<List<Transaction>> GetTransactions(bool forceReload = false);
Task<TResult> GetAsync<TResult>(string uri);
Task<TResult> PostAsync<TResult>(string uri, string jsonData);
Task<List<Coin>> GetDogeCoinLatestPrices();
Task<TResult> PutAsync<TResult>(string uri, string jsonData);
Task DeleteAsync(string uri);
}
The ICryptoService defines an interface that performs various network requests. It has eight methods that have different responsibilities. The problem here is not how many methods the interface has. The problem is that the methods are divided into different categories of responsibility:
- GetEthereumLatestPrices, GetBitcoinLatestPrices, and GetDogeCoinLatestPrices belong to the group that fetches the cryptocurrency prices.
- GetAsync, PostAsync, PutAsync, and DeleteAsync are part of the group that performs generic network requests.
- GetTransactions is a different method. It doesn’t belong to the groups above.
You might be wondering how to split ICryptoService into multiple interfaces? Let’s see; the easiest thing is to put GetTransactions into a separate interface:
public interface IWalletController
{
Task<List<Transaction>> GetTransactions(bool forceReload = false);
}
The next thing is to create a separate interface for the methods that perform generic network requests:
public interface INetworkService
{
Task<TResult> GetAsync<TResult>(string uri);
Task<TResult> PostAsync<TResult>(string uri, string jsonData);
Task<TResult> PutAsync<TResult>(string uri, string jsonData);
Task DeleteAsync(string uri);
}
Finally, ICrpytoService is now much smaller:
public interface ICryptoService
{
Task<List<Coin>> GetEthereumLatestPrices();
Task<List<Coin>> GetBitcoinLatestPrices();
Task<List<Coin>> GetDogeCoinLatestPrices();
}
The end solution leaves us now with three fine-grained interfaces.
Benefits of Interface Segregation Principle
One of the most critical applications of the Interface Segregation Principle is that it keeps our code modular, which in turn enhances reusability and maintainability.
You can do this by breaking down the code into smaller chunks. By doing this, you can develop, test, and deploy independently and integrated with other components to form a larger application.
Thus, the Interface Segregation principle helps keep the code clean and eliminates the chances of coupling of modules, hence reducing the chances of defects.
Conclusion – What is the main takeaway
It seems like a solution to having a better code always comes down to a word small: small method, small class, small interface. If you split your code into smaller chunks, it will be easier to change and maintain it.
Divide et impera. Divide and conquer.