👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
Interface Segregation Principle in SOLID

Interface Segregation Principle in SOLID

Authors - Abdul Rahman (Content Writer), Regina Sharon (Graphic Designer)

SOLID

6 Articles

Improve

Table of Contents

  1. What we gonna do?
  2. Why we gonna do?
  3. How we gonna do?
  4. Summary

What we gonna do?

In this article, let's learn about Interface Segregation Principle in SOLID Principles in .NET.

Note: If you have not done so already, I recommend you read the article on Liskov Substitution Principle in SOLID.

Why we gonna do?

The Interface Segregation Principle (ISP) is essential for creating software that is easy to maintain and scale. It ensures that clients are only exposed to the methods they require, and not to methods that are unnecessary. By doing so, it keeps interfaces lean and reduces the complexity of the codebase.

Without ISP, interfaces can become bloated, and it can be challenging to maintain them. If an interface has too many methods, it can become a barrier to change. Any modification to the interface requires clients to update their implementation, which can be a time-consuming and error-prone process.

The Interface Segregation Principle states that clients should not be forced to implement interfaces they don't use. Instead, interfaces should be designed to be specific to the needs of the clients that use them.

To put it simply, this principle suggests that you should break large interfaces into smaller, more specific ones that are easier to manage and maintain. Clients should only depend on the methods that they need, and not on the methods that are irrelevant to them.

How we gonna do?

Let's look at a real-world example where the Interface Segregation Principle (ISP) can make your codebase more maintainable and flexible. Imagine you have a generic repository interface that handles both read and write operations for your entities. Over time, this interface can become very large, and not all consumers need all the methods. For example, some services may only need to read data, while others need full CRUD operations.

Here is an example of a large repository interface that violates ISP:


public interface IAsyncRepository<T> where T : class
{
    Task<int> CountAsync(CancellationToken cancellationToken = default);
    Task<int> CountAsync(ISpecification<T> spec, string? cacheKey, CancellationToken cancellationToken = default);
    Task<Maybe<T>> FindAsync(Guid id, CancellationToken cancellationToken = default);
    Task<Maybe<T>> FirstOrDefaultAsync(ISpecification<T> spec, CancellationToken cancellationToken = default);
    Task<Maybe<TResult>> FirstOrDefaultAsync<TResult>(ISpecification<T, TResult> spec, CancellationToken cancellationToken = default);
    Task<IReadOnlyList<T>> FromSqlQueryAsync(FormattableString sql, CancellationToken cancellationToken = default);
    Task<IReadOnlyList<T>> FromSqlRawAsync(string sql, CancellationToken cancellationToken = default);
    Task<IReadOnlyList<T>> ListAsync(CancellationToken cancellationToken = default);
    Task<IReadOnlyList<T>> ListAsync(Specification<T> spec, string? cacheKey, CancellationToken cancellationToken = default);
    Task<IReadOnlyList<T>> ListAsync(ISpecification<T> spec, string? cacheKey, CancellationToken cancellationToken = default);
    Task<IReadOnlyList<TResult>> ListAsync<TResult>(ISpecification<T, TResult> spec, CancellationToken cancellationToken = default);
    
    Task<T> AddAsync(T entity, CancellationToken cancellationToken = default);
    Task<IEnumerable<T>> AddRangeAsync(IEnumerable<T> entity, CancellationToken cancellationToken = default);
    Task<IDbContextTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default);
    void CommitTransaction();
    Task<int> ExecuteSqlRawAsync(string sql, CancellationToken cancellationToken = default);
    Task<T> FirstAsync(ISpecification<T> spec, CancellationToken cancellationToken = default);
    void Remove(T entity);
    void RemoveRange(IEnumerable<T> entity);
    void RollbackTransaction();
    Task<Result> SaveChangesAsync(CancellationToken cancellationToken = default);
    T Update(T entity);
    void UpdateRange(IEnumerable<T> entity);
}
      

This interface has too many responsibilities. If a consumer only needs to read data, it is still forced to depend on all the write and transaction methods. This makes the code harder to maintain and violates the Interface Segregation Principle.

To follow ISP, you can split this large interface into smaller, more focused interfaces. For example, you can create a IAsyncReadRepository<T> for read operations and let IAsyncRepository<T> inherit from it for write operations:


public interface IAsyncReadRepository<T> where T : class
{
    Task<int> CountAsync(CancellationToken cancellationToken = default);
    Task<int> CountAsync(ISpecification<T> spec, string? cacheKey, CancellationToken cancellationToken = default);
    Task<Maybe<T>> FindAsync(Guid id, CancellationToken cancellationToken = default);
    Task<Maybe<T>> FirstOrDefaultAsync(ISpecification<T> spec, CancellationToken cancellationToken = default);
    Task<Maybe<TResult>> FirstOrDefaultAsync<TResult>(ISpecification<T, TResult> spec, CancellationToken cancellationToken = default);
    Task<IReadOnlyList<T>> FromSqlQueryAsync(FormattableString sql, CancellationToken cancellationToken = default);
    Task<IReadOnlyList<T>> FromSqlRawAsync(string sql, CancellationToken cancellationToken = default);
    Task<IReadOnlyList<T>> ListAsync(CancellationToken cancellationToken = default);
    Task<IReadOnlyList<T>> ListAsync(Specification<T> spec, string? cacheKey, CancellationToken cancellationToken = default);
    Task<IReadOnlyList<T>> ListAsync(ISpecification<T> spec, string? cacheKey, CancellationToken cancellationToken = default);
    Task<IReadOnlyList<TResult>> ListAsync<TResult>(ISpecification<T, TResult> spec, CancellationToken cancellationToken = default);
}

public interface IAsyncRepository<T> : IAsyncReadRepository<T> where T : class
{
    Task<T> AddAsync(T entity, CancellationToken cancellationToken = default);
    Task<IEnumerable<T>> AddRangeAsync(IEnumerable<T> entity, CancellationToken cancellationToken = default);
    Task<IDbContextTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default);
    void CommitTransaction();
    Task<int> ExecuteSqlRawAsync(string sql, CancellationToken cancellationToken = default);
    Task<T> FirstAsync(ISpecification<T> spec, CancellationToken cancellationToken = default);
    void Remove(T entity);
    void RemoveRange(IEnumerable<T> entity);
    void RollbackTransaction();
    Task<Result> SaveChangesAsync(CancellationToken cancellationToken = default);
    T Update(T entity);
    void UpdateRange(IEnumerable<T> entity);
}
      

Now, if a service only needs to read data, it can depend on IAsyncReadRepository<T> and ignore all the write and transaction methods. This makes your code more modular, easier to test, and easier to maintain. If you need full CRUD operations, you can use IAsyncRepository<T> which includes both read and write methods.

Summary

Advantages of Interface Segregation Principle:

  • Encourages Modularity and Extensibility: By segregating interfaces, each interface becomes smaller and focused on a specific functionality. This encourages modularization of code and makes it easy to extend the application by adding new functionality through new interfaces.
  • Improves Code Readability and Maintainability: Interfaces that are not cluttered with unnecessary methods are easier to read and maintain. This makes the code more understandable and less prone to errors, leading to better quality software.
  • Reduces coupling and Improves testability: By separating interfaces into smaller, more focused interfaces, we can reduce the coupling between classes. This makes the code more modular and easier to test. We can test each interface in isolation, which helps to identify any issues in the interface early in the development process.
  • Increases Code Reusability: Interfaces that are designed with the ISP in mind can be reused in multiple projects. This saves time and effort by reducing the need to write new code for each project.

The Interface Segregation Principle is an essential part of the SOLID principles in object-oriented design. It states that a client should not be forced to depend on methods it does not use. By separating interfaces into smaller, more focused interfaces, we can reduce coupling between classes, improve code readability and maintainability, and increase code reusability.

👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
  • SOLID
  • ISP
  • Interface Segregation