Skip to content

SOLID: Interface Segregation Principle

Published: at 05:33 PM

Example

Consider the following interface INumberOperations, which defines operations on complex numbers.

interface INumberOperations
{
    Number GetComplexConjugate();
    Number Addition();
}

In this example, the INumberOperations interface includes the GetComplexConjugate() method, which only applies to complex numbers, not real numbers. This raises the question: Why should all numbers, including real numbers, implement an interface method that only applies to complex numbers? To resolve this, we can split the interface into more specific ones, such as a general interface for number operations and a separate one for complex number operations:

internal interface IComplexNumberOperations
{
    Number GetComplexConjugate();
}

interface INumberOperations : IComplexNumberOperations
{
    Number Addition();
}

Breaking down the interface into smaller, more specific ones allows for improved code readability, testability, and maintainability.

Understanding the Interface Segregation Principle

The Interface Segregation Principle (ISP) states that:

Clients should not be forced to depend on methods they do not use.

Many developers tend to think about interfaces in terms of the concrete classes that implement them. However, the primary purpose of interfaces is to introduce loose coupling between components.

In terms of interfaces, we can rephrase the definition as follows:

An object should only depend on the interfaces it requires and should not be forced to implement methods (or properties) it doesn’t need.

A corollary to this definition:

Corollary: Prefer small, cohesive interfaces to large, “fat” ones.

What Does “Interface” Mean?

The term “interface” applies to any object-oriented programming (OOP) language, not just C#. In C#, this can refer to:

What Is a Client?

In this context, a “client” is any code that interacts with an instance of an interface—it’s the code that calls the methods defined in the interface. If your code uses an instance of a type, your code is the client. Whatever methods you can call on that instance constitute its interface.

The ISP dictates that your code should not depend on methods in that instance that it does not use.

The Problem with Large Interfaces

Imagine you have an interface with 50 methods, and you want to implement your own version of that interface. If you only need one or two methods out of the 50, should you still be required to implement the entire interface?

Forcing classes to depend on large interfaces that they do not fully utilize leads to higher coupling and makes the codebase harder to maintain, test, and extend. This situation increases the risk of breaking multiple clients when changes are made to the interface.

Detecting ISP Violations

Signs that you may be violating ISP include:

Example: Refactoring a Violating Interface

Consider the following interface:

public interface IMarketingService
{
    void SendEmail(string email, string subject, string body);
    void SendText(int areaCode, int number, string message);
}

This interface lacks cohesion, as shown by the following implementation:

public class EmailMarketingService : IMarketingService
{
    public void SendEmail(string email, string subject, string body)
    {
        // code to send an email
    }

    public void SendText(int areaCode, int number, string message)
    {
        throw new NotImplementedException();
    }
}

The EmailMarketingService does not fully implement the interface, leading to a NotImplementedException. This is a clear violation of ISP. To resolve this, we can split the interface into two smaller, more cohesive interfaces:

public interface IEmailMarketingService
{
    void SendEmail(string email, string subject, string body);
}

public interface ITextMarketingService
{
    void SendText(int areaCode, int number, string message);
}

This solution is more cohesive and adheres to ISP. While this example is simple, the same principle can be applied to larger interfaces by grouping related methods and separating them into their own interfaces.

Handling Legacy Code

What if you have legacy code that depends on a large interface, and you don’t want to break your code by refactoring? In such cases, C# allows you to use smaller, more cohesive interfaces while maintaining backward compatibility:

public interface IMarketingService : IEmailMarketingService, ITextMarketingService
{
}

Here, IMarketingService inherits from both IEmailMarketingService and ITextMarketingService, ensuring that legacy code continues to work without requiring changes. This approach works as long as you own the original interface. If you’re using an interface from a third-party framework or SDK, consider using the Adapter Design Pattern.

The ISP is closely related to the Liskov Substitution Principle (LSP). Larger interfaces are harder to implement fully, increasing the likelihood of violating LSP by creating classes that are not true substitutes for their base types.

ISP also relies heavily on the concept of cohesion, which is tied to the Single Responsibility Principle (SRP). Small, cohesive interfaces are preferable to large ones where not all methods are closely related.

Fixing ISP Violations

Here are some approaches to addressing ISP violations:

  1. Break Up Large Interfaces: Decompose large interfaces into smaller, more specific ones. You can use interface inheritance to maintain backward compatibility.

  2. Use the Adapter Design Pattern: If you don’t control the large interface, create an adapter that implements a smaller interface your code can work with, while the adapter handles the interactions with the larger interface.

  3. Client-Defined Interfaces: Allow client code to define the interfaces they work with. This ensures that clients only depend on the methods they actually use, making it impossible to violate ISP.

Summary

Remember, the purpose of interfaces is to serve the client. The client owns the interface, defining what it needs and not what the implementer might provide.