First operation – Architectural Principles

The following code represents the first operation:

ReadProducts(publicProductReader);
ReadProducts(privateProductRepository);

Since the PublicProductReader and PrivateProductRepository classes implement the IProductReader interface, the ReadProducts method accepts them, leading to the following output:

Reading from PublicProductReader.
Reading from PrivateProductRepository.

That means we can centralize some code that reads from both implementations without changing them.

Second operation

The following code represents the second operation:

WriteProducts(privateProductRepository);

Since only the PrivateProductRepository class implements the IProductWriter interface, the WriteProducts method accepts only the privateProductRepository variable and outputs the following:

Writing to PrivateProductRepository.

This is one advantage of well-segregated interfaces and responsibilities; if we try to execute the following line, the compiler yields the error saying that we “cannot convert from PublicProductReader to IProductWriter”:

ModifyProducts(publicProductReader);

That error makes sense because PublicProductReader does not implement the IProductWriter interface.

Third operation

The following code represents the third operation:

ReadAndWriteProducts(
    privateProductRepository,
    privateProductRepository
);
ReadAndWriteProducts(
    publicProductReader,
    privateProductRepository
);

Let’s analyze the two calls to the ReadAndWriteProducts method individually, but before that, let’s look at the console output:

Reading from PrivateProductRepository and writing to PrivateProductRepository.
Reading from PublicProductReader and writing to PrivateProductRepository.

The first execution reads and writes to the PrivateProductRepository instance, which is possible because it implements both the IProductReader and IProductWriter interfaces.The second call, however, reads from the public reader but writes using the private writer. The last example shows the power of the ISP, especially when mixed with the SRP, and how easy it is to swap one piece for another when segregating our interfaces correctly and designing our code for the program’s use cases.

You should not divide all your repositories into readers and writers; this sample only demonstrates some possibilities. Always design your programs for the specifications that you have.

Conclusion

To summarize the idea behind the ISP, if you have multiple smaller interfaces, it is easier to reuse them and expose only the features you need instead of exposing APIs that part of your program doesn’t need. Furthermore, it is easier to compose bigger pieces using multiple specialized interfaces by implementing them as needed than remove methods from a big interface if we don’t need them in one of its implementations.

The main takeaway is to only depend on the interfaces that you consume.

If you don’t see all of the benefits yet, don’t worry. All the pieces should come together as we move on to the last SOLID principle, to dependency injection, the rest of the book, and as you practice applying the SOLID principles.

Like the SRP, be careful not to overuse the ISP mindlessly. Think about cohesion and what you are trying to achieve, not how granular an interface can become. The finer-grained your interfaces, the more flexible your system will be but remember that flexibility has a cost, which can become very high very quickly. For example, your highly-flexible system may be very hard to navigate and understand, increasing the cognitive load required to work on the project.

Next, we explore the last of the SOLID principles.

You may also like