【Classic Design Principles Study】SOLID Design Principles

Understanding some classic design principles and applying them to our daily development will greatly improve the elegance, scalability, and maintainability of the code.
This article summarizes the content of the SOLID design principles in the "Beauty of Design Patterns" column on Geek Time for learning and use.

The SOLID principle is composed of 5 design principles, and SOLID corresponds to the beginning of the English letter of each principle:

  • Single Responsibility Principle (Single Responsiblity Principle)
  • Open Close Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle

1. Single responsibility principle

1. Definition of Single Responsibility Principle (SRP)

The single responsibility principle in English is Single Responsibility Principle, abbreviated as SRP.

The English description is:

A class or module shoule have a single responsibility

Translation means that a class or module is only responsible for completing one responsibility (function).

A simple understanding is that a class, or interface, or module can only be responsible for completing a function or business function.

for example:

If the order category contains detailed information about the user, it may have multiple responsibilities;

For another example, the order interface should only provide APIs such as order query and operation. If the user information query operation is provided, then it can be said that this interface does not conform to the single responsibility principle.

2. How to determine whether the responsibility of a class or module is single

In the above example, the order category contains user information, and the responsibilities may not be single. Possibility is used because the judgment criteria may be different in different scenarios. For example: Order information OrderDTO and user address information AddressDTO should belong to different domain models. However, an order may contain a harvest address, so there will be address information in the order category. In this scenario, the address information may belong to the order information, which can be regarded as a single responsibility.

Therefore, it is impossible to generalize whether the responsibility of a class or module is single, and different judgments need to be made in a specific scenario.

The faster the business development, the richer the functions, the more the content of the class, and the simpler the division of class responsibilities. Generally, the following criteria are used to determine whether a class or module has a single responsibility:

  • There are too many lines of code, functions, and attributes in the class (for example, when there are too many address information attributes in the order class OrderDTO, you can separate the address information from the order class OrderDTO and make a separate class AddressDTO);
  • There are too many other classes that the class depends on, or too many other classes that depend on the class (the ones that rely on the same functional class can be split out to form a separate functional class);
  • Too many private methods (such as a large number of operations on the time format and digital decimal point, you can split the private methods into the Util class to improve reusability);
  • It is more difficult to give a suitable name to the class;
  • A large number of methods in the class are focused on operating certain attributes in the class.

Of course, the class is not split as fine as possible, but also follow the principle of "high cohesion, low coupling" to split, otherwise it will affect the maintainability of the code.

2. Opening and closing principle (OCP)

The opening and closing principle is one of the most difficult to understand and use in development, but it is also one of the most useful design principles. It considers code scalability.

1. Definition of opening and closing principles

The opening and closing principle in English is: Open Close Principle, abbreviated as OCP.

The English description is:

Software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification。

The translation is:

Software entities (modules, classes, methods, etc.) should be "expanded and closed for modification."

In layman's terms:

Adding a new function should extend the code (add new modules, classes, methods, etc.) based on the existing code, rather than modify the existing code (such as modifying interface definitions, class information, method parameters, etc.).

2. The principle of opening and closing is applied in development

In fact, the principle of opening and closing is used more in our development, and a common example is the example of handlers.

The following is a demo example of Api alarm. When the amount of interface calls and QPS in the project exceed a certain threshold, it will alarm:

/**
 * Created by wanggenshen
 * Date: on 2019/12/11 09:55.
 * Description: API 接口监控
 */
public class ApiAlert {
    
    
    private AlertRule alertRule;
    private Notification notification;

    private static final String NOTIFY_MSG = "【%s】api:[%s] tps exceed max tps";

    public ApiAlert(AlertRule alertRule, Notification notification) {
    
    
        this.alertRule = alertRule;
        this.notification = notification;
    }

    /**
     * 是否需要发送告警
     *
     * @param api               接口名
     * @param requestCount      接口调用量
     * @param errorCount        接口调用失败次数
     * @param durationSeconds   窗口期
     */
    public void check(String api, long requestCount, long errorCount, long durationSeconds) {
    
    
        AlertRule alertRule = AlertRule.getMatchedRule(api);
        // calculate tps, to evaluate if need to send URGENCY notify
        long tps = requestCount / durationSeconds;
        if (tps > alertRule.getMaxTps()) {
    
    
            String notifyMsg = String.format(NOTIFY_MSG, "URGENCY", api);
            notification.notify(NotificationEmergencyLevelEnum.URGENCY.getCode(), notifyMsg);
        }

        // calculate errorCount, to evaluate if need to send URGENCY notify
        if (errorCount > alertRule.getMaxErrorLimit()) {
    
    
            String notifyMsg = String.format(NOTIFY_MSG, "SEVERE", api);
            notification.notify(NotificationEmergencyLevelEnum.SEVERE.getCode(), notifyMsg);
        }

    }
}

/**
 * Created by wanggenshen
 * Date: on 2019/12/11 09:42.
 * Description: 存储告警规则
 */
@Getter
public class AlertRule {
    
    
    private long maxTps;
    private long maxErrorLimit;

    public AlertRule(long maxTps, long maxErrorLimit) {
    
    
        this.maxTps = maxTps;
        this.maxErrorLimit = maxErrorLimit;
    }

    public static AlertRule getMatchedRule(String api) {
    
    
        // 模拟 "getOrder" 接口设置的最大tps和errorLimit, 设置的参数可以放到数据库或缓存
        if ("getOrder".equals(api)) {
    
    
            AlertRule orderAlertRule = new AlertRule(1000, 10);
            return orderAlertRule;
        } else if ("getUser".equals(api)) {
    
    
            AlertRule userAlertRule = new AlertRule(1500, 15);
            return userAlertRule;
        } else {
    
    
            AlertRule commonAlertRule = new AlertRule(500, 20);
            return commonAlertRule;
        }
    }
}

/**
 * Created by wanggenshen
 * Date: on 2019/12/11 09:42.
 * Description: 告警通知类,支持邮件、短信、微信、手机等多种通知渠道
 */
@Slf4j
@Getter
@AllArgsConstructor
public class Notification {
    
    

    private String notifyMsg;
    private int notifyType;


    /**
     * 发送消息告警
     *
     * @param notifyType    告警类型
     * @param notifyMsg     告警内容
     */
    public void notify(int notifyType, String notifyMsg) {
    
    
        log.info("Receive notifyMsg [{}] to push, type:{}", notifyMsg, notifyType);
    }
}

/**
 * Created by wanggenshen
 * Date: on 2019/12/11 09:48.
 * Description: 告警严重程度
 */
@Getter
public enum NotificationEmergencyLevelEnum {
    
    
    SEVERE(0, "严重"),
    URGENCY(1, "紧急"),
    NORMAL(2, "普通"),
    TRIVIAL(3, "无关紧要")

    ;

    private int code;
    private String desc;

    NotificationEmergencyLevelEnum(int code, String desc) {
    
    
        this.code = code;
        this.desc = desc;
    }
}

The ApiAlert.check() method is the specific implementation of the alarm, that is: when the interface QPS exceeds the threshold, the corresponding alarm is sent;

When the amount of interface call error exceeds the threshold, a corresponding alarm is sent.

If you need to count the TPS of the interface, you need to modify the original check() method and add new logic.

This violates the OCP principle, that is: adding new functions should not modify the existing code, but expand on the original code.

How to apply OCP principles? The following is a code example after applying OCP (due to too much code, only the core code is shown here. For detailed code, please see: My github ):

(1) First abstract parameters:

/**
 * Created by wanggenshen
 * Date: on 2019/12/11 10:27.
 * Description: API统计信息
 */
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class ApiStatInfo {
    
    
    private String api;
    private long requestCount;
    private long errorCount;
    private long durationSeconds;

}

(2) The abstract core method check() provides an entrance:

/**
 * Created by wanggenshen
 * Date: on 2019/12/11 09:55.
 * Description: API 接口监控
 */
@Component
public class ApiAlert {
    
    

    @Autowired
    private ApiInterceptorChainClient interceptorChainClient;

    /**
     * 是否需要发送告警
     */
    public void check(ApiStatInfo apiStatInfo) {
    
    
        interceptorChainClient.processApiStatInfo(apiStatInfo);
    }
}

(3) The implementation details of specific alarm handling handlers are implemented by different handler classes: ApiTpsAlertInterceptor, ApiErrorAlertInterceptor. Of course, there needs to be a manager class to initialize the handler class and trigger its execution:

/**
 * Created by wanggenshen
 * Date: on 2019/12/11 19:45.
 * Description: 负责拦截器链的初始化和执行
 */
@Component
public class ApiInterceptorChainClient {
    
    

    @Autowired
    private List<ApiAlertInterceptor> apiAlertInterceptors;

    @PostConstruct
    public void loadInterceptors() {
    
    
        if (apiAlertInterceptors == null || apiAlertInterceptors.size() <= 0) {
    
    
            return;
        }
        apiAlertInterceptors.stream().forEach(interceptor -> resolveInterceptorOrder(interceptor));

        // 按优先级排序, order越小, 优先级越高
        Collections.sort(apiAlertInterceptors, (o1, o2) -> o1.getOrder() - o2.getOrder());

    }

    private void resolveInterceptorOrder(ApiAlertInterceptor interceptor) {
    
    
        if (interceptor.getClass().isAnnotationPresent(InterceptorOrder.class)) {
    
    
            int order = interceptor.getClass().getAnnotation(InterceptorOrder.class).order();
            interceptor.setOrder(order);
        }
    }

    public void processApiStatInfo(ApiStatInfo apiStatInfo) {
    
    
        apiAlertInterceptors.stream().forEach(apiAlertInterceptor -> apiAlertInterceptor.handler(apiStatInfo));
    }
}

In this way, if you need to add API TPS alarm processing, you only need to extend the original code and add a handler class, and the original code hardly needs any processing. This meets the definition of the OCP design principle: close the modification, right Expansion and opening.

3. How to achieve "open for extension, closed for modification"

The principle of opening and closing is to deal with the problem of code scalability, and leave extension points for possible changes in the future when writing code;

The variable part is encapsulated and an abstract immutable interface is provided to call the upper system; when the specific implementation changes, only a new implementation needs to be extended, and the upstream system hardly needs to be modified.

Common methods used to improve code scalability are:

  • Polymorphism
  • Dependency injection
  • Programming based on interface rather than implementation
  • Design patterns (strategies, templates, chain of responsibility, etc.)

Third, the principle of Li substitution (LSP)

The Liskov Substitution Principle is called: Liskov Substitution Principle, which is defined as follows:

The object of subtype/derived class can replace any place where the object of base/parent class in the program (program) appears, and the logical behavior of the original program (behavior) is unchanged and the correctness is not changed. destroyed.

Li-style substitution is a design principle used to guide the design of subclasses in the inheritance relationship. The design of the subclass should ensure that when replacing the parent class, it does not change the logic of the original program and does not destroy the correctness of the original program.

For a simple example, what kind of code violates the LSP.

There is a method calculate() in the parent class A to calculate the sum of two numbers. When any of the two numbers is null, it returns 0.

After the subclass B inherits the parent class A and overrides the method calculate, an exception is thrown when either of a or b is null.

public class A {
    
    
    public Integer calculate(Integer a, Integer b) {
    
    
        if (a == null || b == null) {
    
    
            return 0;
        }
        return a + b;
    }
}

public class B extends A {
    
    
    @Override
    public Integer calculate(Integer a, Integer b) {
    
    
        if (a == null || b == null) {
    
    
            throw new RuntimeException("Null num exception");
        }
        return a + b;
    }
}

Refer to the parent class object to call the calculate method. When the passed-in parameter is null, the return is 0; refer to the subclass object to call the calculate method and the passed-in parameter is null, an exception is reported. This writing actually violates the LSP principle: subclasses can replace any place where the parent class appears.

How to ensure that the LSP principle is met during subclass design?

When subclasses are designed, they must abide by the behavioral conventions of the parent class. The parent class defines the behavior convention of the function, and the subclass can change the internal implementation logic of the function, but cannot change the original behavior convention of the function.

The behavioral conventions here include:

(1) The function to be declared by the function

For example, sortOrdersByAmount() provided by the parent class: a function to sort by order amount. When the subclass is rewritten, it is sorted by the order creation date. The function declaration function defined by the parent class is modified. The design of this subclass violates the LSP. .

(2) Input, output, abnormal

Input: The parent class only allows the parameter to be an integer, while the child class is any integer, which violates the LSP;

Output: the parent class returns an empty collection when an error occurs; the child class returns an exception under the same circumstances, violating the LSP;

Exception: The parent class does not throw exception or exception A when the program is running; the child class throws exception or exception B when the program is running, which violates the LSP.

(3) The subclass violates the annotation declaration in the parent class

For example, the function defined by the annotation on a function of the parent class is to add two numbers, but when the subclass is rewritten, it is to subtract two numbers, which violates the LSP.

LSP is generally a design principle to be followed when a subclass rewrites a parent class. Generally, as long as it satisfies "the place where the parent class object is referenced can be replaced with a subclass object".

Fourth, the principle of interface isolation (ISP)

Interface Segregation Principle (ISP): Interface Segregation Principle.

The interface isolation principle is somewhat similar to the single responsibility principle, which refers to:

If only part of the interface is used by some callers, this part of the interface needs to be isolated and used by this part of the caller separately, so that other dependents will not reference the unnecessary interface.

The interface in the ISP design principle is not only the concept of the interface we talk about in daily development, but can generally be understood as the following three types:

  • A set of API interfaces;
  • Single API interface or function;
  • Interface concept in OOP

(1) A set of API interfaces

If it is a set of API interfaces, for example, UserService provides a set of API interfaces related to user information, and it also provides an interface for clearing invalid users that is only called by the internal system.

public interface UserService {
    
     
  // 用户登录、查询相关API
  boolean register(String cellphone, String password); 
  boolean login(String cellphone, String password); 
  UserInfo getUserInfoById(long id); 
  UserInfo getUserInfoByCellphone(String cellphone);
  // 清除无效用户
  void cleanUnValidUserInfo(long id)}

When a third party calls UserService to query user information, cleanUnValidUserInfothis method can also be called , which violates the ISP principle. The correct approach is to put the cleanUnValidUserInfointerface in a separate class, not exposed to the outside, and only used by the internal system.

(2) Single API interface or function

In a single API or function, a method may involve multiple functions, such as User getUserAddress(long id)obtaining user address information, but returning user phone number, order and other information, which violates the ISP principle and actually violates the single responsibility principle.

(3) Interface concept in OOP

In Java development, interfaces interfaceare identified by keywords.

public interface Config {
    
    
   String getConfig();
   // 更新配置信息
   void update();
  // JSON化展示config信息
  String view();
}

public class RedisConfig implements Config {
    
    
   public String getConfig() {
    
    
     // ...
   }
   // 更新配置信息
   public void update() {
    
    
     // ...
   }
  
    // 空实现
   public String view() {
    
    
   }
}

public interface KafkaConfig implements Config {
    
    
   public String getConfig() {
    
    
     // ...
   }
   // 定时拉取最新配置信息
   public String view() {
    
    
     // ...
   }
  
  // 空实现
  public void update() {
    
         
   }
}

As shown in the figure, Config provides APIs for obtaining, updating, and jsonizing data of config information. RedisConfig only needs getConfig,

For these two update methods, KafkaConfig only needs two methods: getConfig and view, and none of the unnecessary methods are implemented.

Such writing violates the ISP design principles. Both RedisConfig and KafkaConfig rely on unnecessary interfaces: update and view. The correct approach is to separate the update and view separately and use separate APIs to provide them.

5. Dependence Reversal Principle (DIP)

Inversion of Control (IOC) and Dependency Injection (DI) should be the two techniques we have encountered the most.

Inversion of Control (IOC): Control refers to the control of program operation, and inversion refers to the inversion of the program executor. Inversion of control refers to the reversal of the program's running control from the programmer to the framework;

Dependency injection (DI): Instead of constructing the object in the new way, the object is constructed in advance and then passed to the class for use through the constructor or function.

So what is the difference between the Dependency Inversion Principle (DIP) and Inversion of Control and Dependency Injection?

Dependency Inversion Principle: Dependency Inversion Principle (DIP).

The meaning is:

High-level modules should not depend on low-level modules.

High-level modules and low-level modules should rely on each other through abstraction.

Abstraction should not depend on specific implementation details. Specific implementation details depend on abstraction.

For example, for the Web program running in the Tomcat container, Tomcat is the high-level module, and the Web program is the low-level module. Both rely on the Servlet specification, which is an abstraction.

In business development, high-level modules can directly call dependent low-level modules.

Guess you like

Origin blog.csdn.net/noaman_wgs/article/details/104089814