Interface separation principle

Interfaces are a very important weapon in object-oriented programming. Interfaces represent the boundary between the requirements of the client code and the concrete implementation of the requirements. The interface separation principle states that interfaces should be small enough that large and complete contracts (interfaces) are meaningless.

Cause of interface separation

The reasons for splitting a large interface into smaller interfaces are as follows:

(1) Modify the interface separately

② Required by the client

③ Architecture needs

The interface needs to be individually decorated

We illustrate the main benefits of applying the principle of interface separation in large numbers by taking apart a single jumbo interface into several smaller interfaces, creating a variety of modifiers in the process.

The following interface contains five methods for users to perform CRUD operations on persistent storage of entity objects.

public interface ICreateReadUpdateDelete<TEntity>
{
    void Create(TEntity entity);
    TEntity ReadOne(Guid identity);
    IEnumerable<TEntity> ReadAll();
    void Update(TEntity entity);
    void Delete(TEntity entity);
}
Copy the code

ICreateReadUpdateDelete is a generic interface that can accept different entity types. Clients need to first declare their dependency on TEntity. Each operation in CRUD is performed by the corresponding ICreateReadUpdateDelete interface implementation, including the decorator implementation.

Some modifiers, such as log modifiers, apply to all methods. Logging modifiers are crosscutting concerns, of course, and to avoid repeated implementation in multiple interfaces, you can also use section-oriented programming (AOP) to modify all implementations of the interface.

public class CrudLogging<TEntity> : ICreateReadUpdateDelete<TEntity> { private readonly ICreateReadUpdateDelete<TEntity> decoratedCrud; private readonly ILog log; public CrudLogging(ICreateReadUpdateDelete<TEntity> decoratedCrud, ILog log) { this.decoratedCrud = decoratedCrud; this.log = log; } public void Create(TEntity entity) { log.InfoFormat("Create entity of type {0}", typeof(TEntity).Name); decoratedCrud.Create(entity); } public void Delete(TEntity entity) { log.InfoFormat("Delete entity of type {0}", typeof(TEntity).Name); decoratedCrud.Delete(entity); } public IEnumerable<TEntity> ReadAll() { log.InfoFormat("Reading all entities of type {0}", typeof(TEntity).Name); return decoratedCrud.ReadAll(); } public TEntity ReadOne(Guid identity) { log.InfoFormat("Reading entity of type {0}", typeof(TEntity).Name); return decoratedCrud.ReadOne(identity); } public void Update(TEntity entity) { log.InfoFormat("Update entity of type {0}", typeof(TEntity).Name); decoratedCrud.Update(entity); }}Copy the code

But some modifiers only apply to some methods of the interface, not all of them. Suppose you now have a requirement to prompt the user before deleting an entity from the persistent store. Remember not to directly modify an existing class implementation, as this would violate the open and closed principle. Instead, create a new implementation that the client uses to delete the entity.

public class DeleteConfirm<TEntity> : ICreateReadUpdateDelete<TEntity> { private readonly ICreateReadUpdateDelete<TEntity> decoratedCrud; public DeleteConfirm(ICreateReadUpdateDelete<TEntity> decoratedCrud) { this.decoratedCrud = decoratedCrud; } public void Create(TEntity entity) { decoratedCrud.Create(entity); } public IEnumerable<TEntity> ReadAll() { return decoratedCrud.ReadAll(); } public TEntity ReadOne(Guid identity) { return decoratedCrud.ReadOne(identity); } public void Update(TEntity entity) { decoratedCrud.Update(entity); } public void Delete(TEntity entity) { Console.WriteLine("Are you sure you want to delete the entity ? [y/n]"); var keyInfo = Console.ReadKey(); if(keyInfo.Key == ConsoleKey.Y) { decoratedCrud.Delete(entity); }}}Copy the code

In the code above, DeleteConfirm only decorates the Delete method; the rest of the methods are directly dependent (without any decorations, as if the interface method being modified were called directly). Even though these direct supports do nothing, you still need to implement them one by one and write tests to verify that the method behaves correctly, which is much more cumbersome than interface separation.

We can separate the Delete method from the ICreateReadUpdateDelete interface, which gives us two interfaces:

 public interface ICreateReadUpdate<TEntity>
 {
     void Create(TEntity entity);
     TEntity ReadOne(Guid identity);
     IEnumerable<TEntity> ReadAll();
     void Update(TEntity entity);
 }

 public interface IDelete<TEntity>
 {
     void Delete(TEntity entity);
 }
Copy the code

Then provide an implementation of the validation decorator only for the IDelete interface:

public class DeleteConfirm<TEntity> : IDelete<TEntity> { private readonly IDelete<TEntity> decoratedDelete; public DeleteConfirm(IDelete<TEntity> decoratedDelete) { this.decoratedDelete = decoratedDelete; } public void Delete(TEntity entity) { Console.WriteLine("Are you sure you want to delete the entity ? [y/n]"); var keyInfo = Console.ReadKey(); if(keyInfo.Key == ConsoleKey.Y) { decoratedDelete.Delete(entity); }}}Copy the code

As a result, the intent of the code is clearer, the amount of code is reduced, there are not as many direct supports, and the corresponding testing effort is reduced.

Client needs

Clients only need what they need. Large interfaces tend to give users more control, and interfaces with large numbers of members allow clients to do much more than they should. It’s much better to program defensively early on to prevent other developers (including your future self) from inadvertently using your interface to do things they shouldn’t.

There is now a scenario that accesses the current theme of the application through a user configured interface, as follows:

public interface IUserSettings { string Theme { get; set; }}Copy the code
public class UserSettingsConfig : IUserSettings { private const string ThemeSetting = "Theme"; private readonly Configuration config; public UserSettingsConfig() { config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); } public string Theme { get { return config.AppSettingd[ThemeSetting].value; } set { config.AppSettingd[ThemeSetting].value = value; config.Save(); ConfigurationManager.RefreshSection("appSettings"); }}}Copy the code

Interface different clients use the same attribute for different purposes:

public class ReadingController { private readonly IUserSettings userSettings; public ReadingController(IUserSettings userSettings) { this.userSettings = userSettings; } public string GetTheme() { return userSettings.Theme; } } public class WritingController { private readonly IUserSettings userSettings; public WritingController(IUserSettings userSettings) { this.userSettings = userSettings; } public void SetTheme(string theme) { userSettings.Theme = theme; }}Copy the code

While the ReadingController class now only uses the Theme property reader, the WritingController class only uses the Theme property setter. But because of the lack of interface separation, we can’t prevent the WritingController class from getting the topic data, nor can we prevent the ReadingController class from modifying the topic data, which is a big problem, especially with the latter.

To prevent and eliminate the possibility of misusing the interface, you can split the original interface into two parts: one responsible for reading the topic data and one responsible for modifying the topic data.

public interface IUserSettingsReader { string Theme { get; } } public interface IUserSettingsWriter { string Theme { set; }}Copy the code

The UserSettingsConfig implementation class now implements the IUserSettingsReader and IUserSettingsWriter interfaces, respectively

public class UserSettingsConfig : IUserSettings

= >

public class UserSettingsConfig:IUserSettingsReader,IUserSettingsWriter

Clients now rely on only the interfaces they really need, respectively:

public class ReadingController { private readonly IUserSettingsReader userSettings; public ReadingController(IUserSettingsReader userSettings) { this.userSettings = userSettings; } public string GetTheme() { return userSettings.Theme; } } public class WritingController { private readonly IUserSettingsWriter userSettings; public WritingController(IUserSettingsWriter userSettings) { this.userSettings = userSettings; } public void SetTheme(string theme) { userSettings.Theme = theme; }}Copy the code

Architecture needs to be

Another driving force for interface separation comes from architectural design. In asymmetric architectures, such as the command query responsibility separation mode (read/write separation), the intent is to instruct you to do some interface separation action.

Database (table) design itself is data-oriented, set-oriented; Today’s major programming languages have an object-oriented aspect to them. Data (collection) orientation and object orientation are in conflict, but database is an essential part of modern systems. To solve this impedance imbalance, ORM (Object relational mapping) came into being. Completely isolating the database, allowing us to manipulate the database as if it were an object. The general practice today is to use ORM for add, delete, change operations and native SQL for queries. For queries, the simpler, the more efficient (development efficiency and execution efficiency) is best.

The schematic diagram is as follows:

Client build

The design of an interface (whether generated separately or otherwise) affects the type of interface implemented and the client that uses it. If a client wants to use an interface, it must first get an instance of the interface in some way. How an interface instance is provided to a client depends partly on the number of interface implementations. If each interface has its own unique implementation, then instances of all implementations need to be constructed and provided to the client. If all implementations of interfaces are contained in a single class, then only instances of that class need to be built to satisfy all dependencies of the client.

Multiple implementations, multiple instances

Assuming that the IRead, ISave, and IDelete interfaces have their own implementation classes, the client needs to introduce all three interfaces simultaneously. This is also the most common way in our daily development. Based on composite implementation, the corresponding interface is introduced, similar to a pluggable component development.

public class OrderController { private readonly IRead<Order> reader; private readonly ISave<Order> saver; private readonly IDelete<Order> deleter; public OrderController(IRead<Order> reader, ISave<Order> saver, IDelete<Order> deleter) { this.reader = reader; this.saver = saver; this.deleter = deleter; } public void CreateOrder(Order order) { saver.Save(order); } public Order GetOrder(Guid orderID) { return reader.ReadOne(orderID); } public void UpdateOrder(Order order) { saver.Save(order); } public void DeleteOrder(Order order) { deleter.Delete(order); }}Copy the code

Single implementation, single instance

This approach of inheriting and implementing multiple separate interfaces in a single class may seem perverse (the purpose of separating interfaces is not to unify them again in a single implementation). The leaf implementation class commonly used for interfaces, that is, is neither a decorator nor an adapter implementation class, but an implementation class that does the work. This applies to the leaf implementation class because the context of all implementations in the leaf class is consistent. This approach is often applied to classes that work directly with persistence frameworks such as Entity Framework.

public class CreateReadUpdateDelete<TEntity>:
    IRead<TEntity>,ISave<TEntity>,IDelete<TEntity>
{
    public void Save(TEntity entity)
    {
       
    }
    public IEnumerable<TEntity> ReadAll()
    {
        return new List<TEntity>();
    }
    public void Delete(TEntity entity)
    {
        
    }
}

public OrderController CreateSingleService()
{
    var crud = new CreateReadUpdateDelete<Order>();
    return new OrderController(crud,crud,crud);
}
Copy the code

The super interface is in anti-mode

It is a common mistake to aggregate all separated interfaces into the same interface. These interfaces are aggregated to form a “super interface”, which undermines the benefits of interface separation.

public interface CreateReadUpdateDelete<TEntity>:
    IRead<TEntity>,ISave<TEntity>,IDelete<TEntity>
{
    
}
Copy the code

conclusion

Interface separation, whether used as an aid in decoration, to hide functionality from clients that they should not see, or as a product of architectural design. The technical principle of interface separation should all be kept in mind when creating any interface, and it is best to apply interface separation from the outset.

reference

# Agile Development Practices

Author: CoderFocus
Wechat Official Account: