Project – Interface Segregation – Architectural Principles
In this project, we start with the same class as the SRP example but extract an interface from the ProductRepository class. Let’s start by looking at the Product class as a reminder, which represents a simple fictive product:
public record class Product(int Id, string Name);
The code sample has no implementation because it is irrelevant to understanding the ISP. We focus on the interfaces instead. Please assume we implemented the data-access logic using your favorite database.
Now, let’s look at the interface extracted from the ProductRepository class:
namespace InterfaceSegregation.Before;
public interface IProductRepository
{
public ValueTask<IEnumerable<Product>> GetAllPublicProductAsync();
public ValueTask<IEnumerable<Product>> GetAllPrivateProductAsync();
public ValueTask<Product> GetOnePublicProductAsync(int productId);
public ValueTask<Product> GetOnePrivateProductAsync(int productId);
public ValueTask CreateAsync(Product product);
public ValueTask UpdateAsync(Product product);
public ValueTask DeleteAsync(Product product);
}
At this point, the IProductRepository interface breaks the SRP and the ISP the same way the ProductRepository class did before. We already identified the SRP issues earlier but did not reach the point of extracting interfaces.
The ProductRepository class implements the IProductRepository interface and is the same as the SRP example (all methods throw new NotImplementedException()).
In the SRP example, we identified the following responsibilities:
Based on our previous analysis, we have two functional requirements (public and private access). By digging deeper, we can also identify five different database operations. Here’s the result in a grid:
Public | Private | |
Read one product | Yes | Yes |
Read all products | Yes | Yes |
Create a product | No | Yes |
Update a product | No | Yes |
Delete a product | No | Yes |
We can extract the following families of database operations from Table 3.3:
Based on that more thorough analysis, we can extract the IProductReader and IProductWriter interfaces representing the database operation. Then we can create the PublicProductReader and PrivateProductRepository classes to implement our functional requirements.Let’s start with the IProductReader interface:
namespace InterfaceSegregation.After;
public interface IProductReader
{
public ValueTask<IEnumerable<Product>> GetAllAsync();
public ValueTask<Product> GetOneAsync(int productId);
}
With this interface, we cover the read one product and read all products use cases. Next, the IProductWriter interface covers the other three database operations:
namespace InterfaceSegregation.After;
public interface IProductWriter
{
public ValueTask CreateAsync(Product product);
public ValueTask UpdateAsync(Product product);
public ValueTask DeleteAsync(Product product);
}
We can cover all the database use cases with the preceding interfaces. Next, let’s create the PublicProductReader class:
namespace InterfaceSegregation.After;
public class PublicProductReader : IProductReader
{
public ValueTask<IEnumerable<Product>> GetAllAsync()
=> throw new NotImplementedException();
public ValueTask<Product> GetOneAsync(int productId)
=> throw new NotImplementedException();
}
In the preceding code, the PublicProductReader only implements the IProductReader interface, covering the identified scenarios. We do the PrivateProductRepository class next before exploring the advantages of the ISP:
namespace InterfaceSegregation.After;
public class PrivateProductRepository : IProductReader, IProductWriter
{
public ValueTask<IEnumerable<Product>> GetAllAsync()
=> throw new NotImplementedException();
public ValueTask<Product> GetOneAsync(int productId)
=> throw new NotImplementedException();
public ValueTask CreateAsync(Product product)
=> throw new NotImplementedException();
public ValueTask DeleteAsync(Product product)
=> throw new NotImplementedException();
public ValueTask UpdateAsync(Product product)
=> throw new NotImplementedException();
}
In the preceding code, the PrivateProductRepository class implements the IProductReader and IProductWriter interfaces, covering all the database needs. Now that we have covered the building blocks, let’s explore what this can do. Here’s the Program.cs file:
using InterfaceSegregation.After;
var publicProductReader = new PublicProductReader();
var privateProductRepository = new PrivateProductRepository();
ReadProducts(publicProductReader);
ReadProducts(privateProductRepository);
// Error: Cannot convert from PublicProductReader to IProductWriter
// ModifyProducts(publicProductReader); // Invalid
WriteProducts(privateProductRepository);
ReadAndWriteProducts(privateProductRepository, privateProductRepository);
ReadAndWriteProducts(publicProductReader, privateProductRepository);
void ReadProducts(IProductReader productReader)
{
Console.WriteLine(
“Reading from {0}.”,
productReader.GetType().Name
);
}
void WriteProducts(IProductWriter productWriter)
{
Console.WriteLine(
“Writing to {0}.”,
productWriter.GetType().Name
);
}
void ReadAndWriteProducts(IProductReader productReader, IProductWriter productWriter)
{
Console.WriteLine(
“Reading from {0} and writing to {1}.”,
productReader.GetType().Name,
productWriter.GetType().Name
);
}
From the preceding code, the ReadProducts, ModifyProducts, and ReadAndUpdateProducts methods write messages in the console to demonstrate the advantages of applying the ISP.The publicProductReader (instance of PublicProductReader) and privateProductRepository (instance of PrivateProductRepository) variables are passed to the methods to show what we can and cannot do with the current design.Before getting into the weed, when we execute the program, we obtain the following output:Reading from PublicProductReader.
Reading from PrivateProductRepository.
Writing to PrivateProductRepository.
Reading from PrivateProductRepository and writing to PrivateProductRepository.
Reading from PublicProductReader and writing to PrivateProductRepository.