A true master always has the heart of an apprentice

The introduction

For a software platform, the quality of the software platform code directly affects the overall quality and stability of the platform. It also affects the creative passion of students who write code. Imagine if you clone a project out of Git and it’s messy, hard to understand, hard to read, and you want to rewrite it. You’ll lose your initial enthusiasm for writing good code in git. On the other hand, if the clone code is clean, elegant and easy to understand, you will be embarrassed to write bad code. I believe that the difference between the work of the students are deeply experienced, so we see so much code, what kind of code is good code? Do they have any common characteristics or principles? This article discusses how to write good code by explaining the design principles of elegant code.

Code Design Principles

Good code is designed, refactored, and iterated. After we receive the requirements and outline the design, we start coding. However, before the actual coding, we need to do domain hierarchical design and code structure design. So how to design a more elegant code structure? There are some principles of elegant code design that have been summarized by the gods. Let’s take a look at them separately.

SRP

The so-called SRP (Single Responsibility Principle) is the Single Responsibility Principle. But just because we can see it doesn’t mean we can use it. Sometimes we think we can use it, but we will encounter this or that problem in practical application. The reason is that we have not thought through the problem thoroughly, without deep thinking, knowledge is just knowledge, and has not been transformed into our ability. For example, the single responsibility principle here refers to whose single responsibility is it, class or module or domain? Domains can contain multiple modules, and modules can contain multiple classes, which are all problems.

For the sake of illustration, the single responsibility design principle is illustrated by a class. For a class that is responsible for only one responsibility or function, we can say that the class complies with the single responsibility principle. Recall that we already use the single responsibility design principle, consciously or unconsciously, in actual coding. Because it actually fits in with the way we think. Why do you say that? Think about it. When we organize our closets, we put our summer clothes in a closet, and our winter clothes in a closet. So when the season changes, we can just go to the corresponding closet and pick up the clothes directly. Otherwise, we’ll have a hard time finding clothes if we keep both winter and summer clothes in the same closet. When it comes to software code design, we need to adopt this kind of categorical thinking. When designing classes, design small, single-purpose classes rather than large, all-in-one classes.

For chestnuts, in the student management system, if a class has both student information operations such as create or delete action, and of course to create and modify the action, then we can think this class can not meet the needs of the design of the single responsibility principle, because it will be two different business domain business mixed together, so we need to break up, Splitting this large, comprehensive class into two business domains, student and course, is more granular and cohesive.

According to its own experience, the author sums up the need to consider for single responsibility split a few games, hopes to determine whether to split a simple judgment standard: 1, the different business fields need to be split, like the example above, the other if the other classes depend on too much, also need to consider whether it should be split; 2. If we find that private methods have some generality when we write code in a class, such as determining whether IP is valid, parsing XML, etc., then we can consider extracting these methods to form a public utility class, so that other classes can easily use them. The idea of a single responsibility is not only used in code design, but also to a certain extent when we split microservices.

OCP

OCP (Open Closed Principle) refers to the Principle of Closed to modifications and Open to extensions. I personally find this to be the most difficult design Principle. Not only is there a certain threshold to understand, but it is not easy to do in the actual coding process. First of all, we need to be clear about the difference between modification and extension. To be honest, when I first saw this principle, I always thought that modification and open meant the same thing. It’s a little confusing to think about it. Later, in continuous project practice, I gradually deepened my understanding of this design principle.

The modification mentioned in the design principle refers to the modification of the original code, while the extension refers to the extension of capabilities based on the original code, without modifying the original existing code. This is the biggest difference between modification and extension, one requires modification of the original code logic, the other does not. That’s why it’s closed for modifications but open for extensions. Once we understand the difference between modification and extension, we can ask ourselves why we should close for modification and open for extension. We all know that software platforms are constantly being updated and iterated, so we need to constantly develop in old code. If the code design is not good and the expansibility is not strong, the existing code will be modified every time the function iteration is carried out, which may introduce bugs and cause instability of the system platform. So we need to turn off changes for platform stability. But what about adding new features? That is, by extension, so you need to be open to extension.

Here we use an example that might otherwise be a bit abstract. In a monitoring platform, we need to monitor the running information such as CPU and memory occupied by services. The code of the first version is as follows.

public class Alarm {
	private AlarmRule alarmRule;
    private AlarmNotify alarmNotify;
    
    public Alarm(AlarmRule alarmRule, AlarmNotify alarmNotify) {
        this.alarmRule = alarmRule;
        this.alarmNotify = alarmNotify;
    }
    
    public void checkServiceStatus(String serviecName, int cpu, int memory) {
        if(cpu > alarmRule.getRule(ServiceConstant.Status).getCpuThreshold) {
            alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
        }
        
         if(memory > alarmRule.getRule(ServiceConstant.Status).getMemoryThreshold) {
            alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
        } 
    
    }

}

Copy the code

The code logic is simple. It is based on the threshold of the corresponding alarm rule to determine whether the condition for triggering the alarm notification is reached. If there is a requirement, you need to add a condition to determine whether alarm notification is required based on the corresponding service status. Let’s take a look at the low modification method first. We add the parameter of service status in checkServiceStatus method, and add the logic to judge the status in the method.

public class Alarm {
	private AlarmRule alarmRule;
    private AlarmNotify alarmNotify;
    
    public Alarm(AlarmRule alarmRule, AlarmNotify alarmNotify) {
        this.alarmRule = alarmRule;
        this.alarmNotify = alarmNotify;
    }
    
    public void checkServiceStatus(String serviecName, int cpu, intThe memory,int status) {
        if(cpu > alarmRule.getRule(ServiceConstant.Status).getCpuThreshold) {
            alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
        }
        
         if(memory > alarmRule.getRule(ServiceConstant.Status).getMemoryThreshold) {
            alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
        } 
        
         if(status == alarmRule.getRule(ServiceConstant.Status).getStatusThreshold) {
            alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
        } 
    
    }

}

Copy the code

Obviously this modification method is very unfriendly, why say so? If the method parameters are modified first, the place where the method is called may also need to be modified. In addition, if the modified method has a unit test method, the unit test case must also need to be modified. Adding new logic to the previously tested code also increases the risk of bug introduction. So we need to avoid this kind of modification. So how can a change be closed for change and open for extension? First of all, we can abstract the attributes about the ServiceStatus into a ServiceStatus entity, and take ServiceStatus as the parameter in the corresponding check method. In this way, if there are additional attributes about the ServiceStatus in the future, we only need to add them in ServiceStatus. You don’t need to change the parameters in the method or where the method is called, and you don’t need to change the unit test method either.

@Data
public class ServiceStatus {
    String serviecName;
    int cpu;
    int memory;
    int status;

}

Copy the code

In addition, in the detection method, how can we modify to reflect extensible? Instead of adding processing logic to the detection methods. A better way to implement this is through abstract detection methods, which are implemented in each implementation class. In this way, even if the new detection logic, only need to expand the detection implementation method can, do not need to modify the original code logic, code extensible.

LSP

This design Principle, I think, is simpler than the previous two. An object of subtype/derived class can replace an object of base/parent class anywhere in a program, In addition, the logic behavior of the original program is guaranteed to remain unchanged and the correctness is not destroyed.

The Li substitution principle is a principle that guides how subclasses should be designed in inheritance relationships. To understand the principle of Chinese substitution, the most important thing is to understand the words “design by contract”. The superclass defines the “conventions” (or protocols) of a function, and the subclass can change the internal implementation logic of the function, but not the original “conventions” of the function. The conventions here include: function declaration to implement the function; Conventions for inputs, outputs, and exceptions; Even any special instructions listed in the notes.

How do we know if we violated an LSP? I think there are two key points to judge. One is whether the subclass changes the business function that the parent class claims to implement, and the other is whether it violates the parent class’s rules about input, output, and exception throwing.

ISP

The Interface Segregation Principle (ISP) is used to provide interfaces that the caller needs, and does not force interfaces that the caller does not need. As an example, here are the interfaces for products, including interfaces for creating products, deleting products, getting products by ID, and updating products. What if we need to provide an interface to get products by category? Many students will say, this is not simple, we directly add in this interface according to the category of product query interface OK ah. Let’s think about what’s wrong with this.

public interface ProductService { 
    boolean createProduct(Product product); 
    boolean deleteProductById(long id); 
    Product getProductById(long id); 
    int updateProductInfo(Product product);
}

public class UserServiceImpl implements UserService { / /... }

Copy the code

This solution seems to be fine, but if you think about it further, the external system only needs a function to query products by product category, but in reality, the interface we provide also includes the interface to delete and update products. If these interfaces are incorrectly tuned by other systems, product information may be deleted or incorrectly updated. Therefore, we can isolate the interfaces that are called by third parties so that there are no miscalls and no random proliferation of interface capabilities.

public interface ProductService { 
    boolean createProduct(Product product); 
    boolean deleteProductById(long id); 
    Product getProductById(long id); 
    int updateProductInfo(Product product);
}

public interface ThirdSystemProductService{
    List<Product> getProductByType(int type);
}

public class UserServiceImpl implements UserService { / /... }

Copy the code

LOD

LOD (Law of Demeter) is the last Law of code design that we will introduce. Let’s take a look at how the original article describes this design principle. Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; As I understand it, the core idea or goal of Demeter’s design principle is to minimize the impact of code changes on the existing system. So you need to implement classes, modules, or services that can achieve high cohesion and low coupling. Don’t have dependencies between classes that shouldn’t have direct dependencies; Try to rely on only the necessary interfaces between classes that have dependencies. Demeter’s rule wants to reduce coupling between classes and make them as independent as possible. Each class should know less about the rest of the system. Once a change occurs, there are fewer classes that need to know about it. It’s like an underground organization during the War of Resistance against Japan, for example, which is connected together, but with as little contact with the outside world as possible, which is low coupling.

conclusion

This paper summarizes the five principles in software code design. According to my own understanding, these five principles are the internal work of program code design, and twenty-three design patterns are actually the programming moves generated by internal work. Therefore, in-depth understanding of the five design principles is the basis for us to use good design patterns. These are some of the common norms that we follow when we design code structures. Only by honing the cycle of designing code – following specifications – writing code – refactoring can we write elegant code.


Hi, I’m Mufeng. Thanks for your likes, favorites and comments. See you next time! Wechat search: Mufeng technical notes, quality articles continue to update, we have learning punch group can pull you in, together with the impact of the big factory, in addition to a lot of learning and interview materials to provide you.