Six principles of design pattern

The study of design patterns can enhance their awareness of code reuse. At the same time, you can also clearly express your programming ideas. This paper will introduce the six principles of design mode:

Single responsibility principle

definition

Do not have more than one reason for the class change** Generally speaking, a class is only responsible for one responsibility.

Origin of the problem

Class T is responsible for two different responsibilities: responsibility P1 and responsibility P2. When class T needs to be modified due to the change of responsibility P1 requirements, it may lead to the failure of responsibility P2 function which is normally running.

Solution

Follow the principle of single responsibility. Two classes T1 and T2 are established respectively to enable T1 to complete the function of duty P1 and T2 to complete the function of duty P2. In this way, when modifying class T1, there is no risk of failure of responsibility P2; Similarly, when T2 is modified, there is no risk of failure of responsibility P1.

When it comes to the principle of single responsibility, many people will despise it. Because it's too simple. Even if experienced programmers have never read the design pattern or heard of the single responsibility principle, they will consciously abide by this important principle when designing software, because it is common sense. In software programming, no one wants to modify one function and cause other functions to fail. The way to avoid this problem is to follow the principle of single responsibility. Although the principle of single responsibility is so simple and considered common sense, even programs written by experienced programmers will have code that violates this principle. Why does this happen? Because of the proliferation of responsibilities. The so-called responsibility diffusion means that for some reason, responsibility P is divided into finer responsibilities P1 and P2.

For example, class T is only responsible for one responsibility P, so the design conforms to the principle of single responsibility. Later, for some reason, maybe the requirements have changed or the designer's level of the program has improved. It is necessary to subdivide the responsibility P into finer responsibilities P1 and P2. At this time, if the program is to follow the principle of single responsibility, it is necessary to decompose class T into two classes T1 and T2, which are respectively responsible for P1 and P2. But when the program has been written, it takes too much time. Therefore, it is a good choice to simply modify class T and use it to take charge of two responsibilities, although it is contrary to the principle of single responsibility. (the risk of doing so lies in the uncertainty of responsibility diffusion, because we will not think that this responsibility P may spread to P1, P2, P3, P4... Pn in the future. So remember to refactor the code immediately before the responsibility spreads beyond our control.)

For example, use a class to describe the scene of animal breathing:

    class Animal{
        public void breathe(String animal){
            System.out.println(animal+"Breathe air");
        }
    }
    public class Client{
        public static void main(String[] args){
            Animal animal = new Animal();
            animal.breathe("cattle");
            animal.breathe("sheep");
            animal.breathe("pig");
        }
    }
copy

Operation results:

Cattle breathe air  
Sheep breathe air  
Pigs breathe air
copy

After the program went online, a problem was found. Not all animals breathe air. For example, fish breathe water. If the principle of single responsibility is followed during modification, the Animal class needs to be subdivided into Terrestrial and Aquatic animals, with the code as follows:

    class Terrestrial{
        public void breathe(String animal){
            System.out.println(animal+"Breathe air");
        }
    }
    class Aquatic{
        public void breathe(String animal){
            System.out.println(animal+"Breathing water");
        }
    }

    public class Client{
        public static void main(String[] args){
            Terrestrial terrestrial = new Terrestrial();
            terrestrial.breathe("cattle");
            terrestrial.breathe("sheep");
            terrestrial.breathe("pig");

            Aquatic aquatic = new Aquatic();
            aquatic.breathe("fish");
        }
    }
copy

Operation results:

Cattle breathe air  
Sheep breathe air  
Pigs breathe air  
Fish breathe water
copy

We will find that if such modification costs a lot, in addition to decomposing the original class, we also need to modify the client. The direct modification of class Animal to achieve the purpose violates the principle of single responsibility, but the cost is much smaller. The code is as follows:

    class Animal{
        public void breathe(String animal){
            if("fish".equals(animal)){
                System.out.println(animal+"Breathing water");
            }else{
                System.out.println(animal+"Breathe air");
            }
        }
    }

    public class Client{
        public static void main(String[] args){
            Animal animal = new Animal();
            animal.breathe("cattle");
            animal.breathe("sheep");
            animal.breathe("pig");
            animal.breathe("fish");
        }
    }
copy

As you can see, this modification method is much simpler. But there are hidden dangers: one day, if you need to divide the fish into fresh water breathing fish and sea water breathing fish, you need to modify the breathe method of Animal class, and the modification of the original code will bring risks to the call of "pig", "cow", "sheep" and other related functions. Maybe one day, you will find that the result of the program running will become "cow sucking water". This modification method directly violates the principle of single responsibility at the code level. Although it is the simplest to modify, the hidden danger is the greatest. There is another modification method:

    class Animal{
        public void breathe(String animal){
            System.out.println(animal+"Breathe air");
        }

        public void breathe2(String animal){
            System.out.println(animal+"Breathing water");
        }
    }

    public class Client{
        public static void main(String[] args){
            Animal animal = new Animal();
            animal.breathe("cattle");
            animal.breathe("sheep");
            animal.breathe("pig");
            animal.breathe2("fish");
        }
    }
copy

It can be seen that this modification method does not change the original method, but adds a new method to the class. Although it also violates the single responsibility principle, it conforms to the single responsibility principle at the method level, because it does not change the code of the original method. These three methods have their own advantages and disadvantages, so which one is used in actual programming? In fact, it's really hard to say. It needs to be determined according to the actual situation. My principle is: only if the logic is simple enough, can the principle of single responsibility be violated at the code level; Only when the number of methods in the class is small enough, can the principle of single responsibility be violated at the method level;

For example, the example given in this article is too simple. It has only one method, so whether it violates the principle of single responsibility at the code level or at the method level, it will not have much impact. Classes in practical applications are much more complex. In case of responsibility diffusion and need to modify the class, unless the class itself is very simple, it is better to follow the principle of single responsibility.

The advantages of following a single responsibility are:

  • It can reduce the complexity of classes. A class is only responsible for one responsibility, and its logic must be much simpler than that of multiple responsibilities;
  • Improve the readability of the class and the maintainability of the system;
  • The risk caused by the change is reduced. The change is inevitable. If the principle of single responsibility is well observed, the impact on other functions can be significantly reduced when modifying one function.

It should be noted that the principle of single responsibility is not unique to the idea of object-oriented programming. As long as it is modular programming, the principle of single responsibility is applicable.

Richter substitution principle

Many people must be as confused about the name of this principle as when I first saw it. In fact, the reason is that this principle was first proposed by Barbara Liskov, a woman surnamed Li at MIT in 1988.

Definition 1

If there is object o2 of type T2 for each object o1 of type T1, so that the behavior of program P does not change when all objects o1 are replaced with o2, then type T2 is a subtype of type T1.

Definition 2

All references to the base class must be able to use the objects of its subclasses transparently.

Origin of the problem

There is a function P1, which is completed by class A. Now it is necessary to expand the function P1. The expanded function is p, in which p is composed of the original function P1 and the new function P2. If the new function p is completed by subclass B of class A, subclass B may cause failure of the original function P1 while completing the new function P2.

Solution

When using inheritance, follow the Richter substitution principle. When class B inherits class A, in addition to adding new methods to complete the new function P2, try not to rewrite the methods of parent class A or overload the methods of parent class A.

Inheritance contains such a layer of meaning: all implemented methods in the parent class (compared with abstract methods) are actually setting a series of specifications and contracts. Although it does not force all subclasses to comply with these contracts, if subclasses modify these non abstract methods arbitrarily, it will destroy the whole inheritance system. The Richter substitution principle expresses this meaning.

As one of the three characteristics of object-oriented, inheritance not only brings great convenience to program design, but also brings disadvantages. For example, the use of inheritance will bring invasiveness to the program, reduce the portability of the program and increase the coupling between objects. If a class is inherited by other classes, all subclasses must be considered when the class needs to be modified, and all functions involving subclasses may fail after the parent class is modified.

To illustrate the risk of inheritance, we need to complete a function of subtracting two numbers, and class A is responsible for it.

    class A{
        public int func1(int a, int b){
            return a-b;
        }
    }

    public class Client{
        public static void main(String[] args){
            A a = new A();
            System.out.println("100-50="+a.func1(100, 50));
            System.out.println("100-80="+a.func1(100, 80));
        }
    }
copy

Operation results:

100-50=50  
100-80=20
copy

Later, we need to add a new function: complete the addition of two numbers, and then sum with 100. Class B is responsible for it. That is, class B needs to complete two functions:

  • Subtract two numbers.
  • Add the two numbers together and then add 100.

Since class A has implemented the first function, class B only needs to complete the second function after inheriting class A. the code is as follows:

    class B extends A{
        public int func1(int a, int b){
            return a+b;
        }

        public int func2(int a, int b){
            return func1(a,b)+100;
        }
    }

    public class Client{
        public static void main(String[] args){
            B b = new B();
            System.out.println("100-50="+b.func1(100, 50));
            System.out.println("100-80="+b.func1(100, 80));
            System.out.println("100+20+100="+b.func2(100, 20));
        }
    }
copy

After class B is completed, the operation results are as follows:

100-50=150  
100-80=180  
100+20+100=220
copy

We found an error in the subtraction function that was working normally. The reason is that class B inadvertently rewrites the method of the parent class when naming the method, resulting in all the code running the subtraction function calling the rewritten method of class B, resulting in an error in the original function. In this example, an exception occurs after referencing the function completed by base class A and replacing it with subclass B. In actual programming, we often complete new functions by rewriting the parent class. Although it is simple to write, the reusability of the whole inheritance system will be poor. Especially when polymorphism is used frequently, the probability of program running error is very high. If you have to rewrite the method of the parent class, the more common method is: the original parent class and child class inherit a more popular base class, remove the original inheritance relationship, and replace it with dependency, aggregation, combination and other relationships.

Generally speaking, the principle of Richter substitution is that subclasses can expand the functions of the parent class, but cannot change the original functions of the parent class. It contains the following four meanings:

  • Subclasses can implement the abstract methods of the parent class, but they cannot override the non abstract methods of the parent class.
  • Subclasses can add their own unique methods.
  • When the method of the subclass overloads the method of the parent class, the preconditions of the method (i.e. the formal parameters of the method) are more relaxed than the input parameters of the parent method.
  • When the method of the subclass implements the abstract method of the parent class, the post condition of the method (that is, the return value of the method) is more stringent than that of the parent class.

It seems incredible, because we will find that we often violate the Richter replacement principle in our programming, and the program still runs well. So everyone will have such a question, if I have to follow the Richter replacement principle, what will be the consequences?

The consequence is that the probability of problems in the code you write will be greatly increased.

Dependency Inversion Principle

definition

High level modules should not rely on low-level modules, and both should rely on their abstraction; Abstractions should not rely on details; Details should rely on abstraction.

Origin of the problem

Class a directly depends on class B. If you want to change class A to dependent class C, you must modify the code of class A. In this scenario, class A is generally a high-level module, which is responsible for complex business logic; Class B and class C are low-level modules responsible for basic atomic operations; If class A is modified, it will bring unnecessary risks to the program.

Solution

Modifying class A to rely on interface I, class B and class C implement interface I respectively, and class A is indirectly connected with class B or class C through interface I, which will greatly reduce the probability of modifying class A.

The dependency inversion principle is based on the fact that abstract things are much more stable than the variability of details. The architecture based on abstraction is much more stable than that based on detail. In java, abstraction refers to interfaces or abstract classes, and details are concrete implementation classes. The purpose of using interfaces or abstract classes is to formulate specifications and contracts without involving any specific operations, and hand over the task of showing details to their implementation classes.

The core idea of dependency inversion principle is interface oriented programming. We still use an example to illustrate the advantages of interface oriented programming over implementation oriented programming. The scene is like this. The mother tells a story to her child. As long as she gives her a book, she can tell a story to her child according to the book. The code is as follows:

    class Book{
        public String getContent(){
            return "Once upon a time, there was an Arab story";
        }
    }

    class Mother{
        public void narrate(Book book){
            System.out.println("Mother began to tell stories");
            System.out.println(book.getContent());
        }
    }

    public class Client{
        public static void main(String[] args){
            Mother mother = new Mother();
            mother.narrate(new Book());
        }
    }
copy

Operation results:

Mother began to tell stories  
Once upon a time, there was an Arab story
copy

It works well. If one day, the demand becomes like this: not a book, but a newspaper. Let the mother tell the story in the newspaper. The code of the newspaper is as follows:

    class Newspaper{
        public String getContent(){
            return "Lin Shuhao 38+7 Lead the Knicks to beat the Lakers";
        }
    }
copy

The Mother couldn't do it, because she couldn't read the story in the newspaper. It was ridiculous. She just changed the Book into a newspaper and had to revise Mother to read it. What if you need to change to magazines in the future? Change to a web page? And constantly modify Mother, which is obviously not a good design. The reason is that the coupling between Mother and Book is too high. We must reduce the coupling between them.

We introduce an abstract interface IReader. Reading materials, as long as they are with words, belong to reading materials:

    interface IReader{
        public String getContent();
    }
copy

The Mother class has a dependency relationship with the interface IReader, and both Book and Newspaper belong to the category of reading materials. They each implement the IReader interface, which conforms to the principle of dependency inversion. The code is modified as follows:

    class Newspaper implements IReader {
        public String getContent(){
            return "Lin Shuhao 17+9 Help the Knicks defeat the eagle";
        }
    }
    class Book implements IReader{
        public String getContent(){
            return "Once upon a time, there was an Arab story";
        }
    }

    class Mother{
        public void narrate(IReader reader){
            System.out.println("Mother began to tell stories");
            System.out.println(reader.getContent());
        }
    }

    public class Client{
        public static void main(String[] args){
            Mother mother = new Mother();
            mother.narrate(new Book());
            mother.narrate(new Newspaper());
        }
    }
copy

Operation results:

Mother began to tell stories  
Once upon a time, there was an Arab story  
Mother began to tell stories  
Lin Shuhao 17+9 Help the Knicks defeat the eagle
copy

After this modification, no matter how to extend the Client class in the future, there is no need to modify the Mother class. This is just a simple example. In fact, the Mother class representing the high-level module will be responsible for completing the main business logic. Once it needs to be modified, there is a great risk of introducing errors. Therefore, following the dependency inversion principle can reduce the coupling between classes, improve the stability of the system and reduce the risk caused by modifying the program.

Using the dependency inversion principle brings great convenience to multi person parallel development. For example, in the above example, when the original Mother class is directly coupled with the Book class, the Mother class can only be encoded after the Book class is encoded, because the Mother class depends on the Book class. The modified program can start at the same time without affecting each other, because Mother has nothing to do with the Book class. The greater the number of people involved in the development, the more significant it will be. The popular TDD development mode now is the most successful application of relying on the inversion principle.

There are three ways to transfer dependencies. The method used in the above example is interface transfer, and there are two other transfer methods: construction method transfer and setter method transfer. I believe those who have used the Spring framework will not be unfamiliar with the transfer method of dependencies. In actual programming, we generally need to do the following three points:

  • Low level modules should have abstract classes or interfaces, or both.
  • The declaration type of variables should be abstract classes or interfaces.
  • Follow the Richter substitution principle when using inheritance.

The core of the dependency inversion principle is to make us understand interface oriented programming. If we understand interface oriented programming, we will understand dependency inversion.

Interface isolation principle

definition

The client should not rely on interfaces it does not need; The dependence of one class on another should be based on the smallest interface.

Origin of the problem

Class a depends on class B through interface I, and class C depends on class D through interface I. If interface I is not the smallest interface for class A and class B, class B and class D must implement methods they do not need.

Solution

The bloated interface I is divided into several independent interfaces, and class A and class C establish dependencies with the interfaces they need respectively. That is, the principle of interface isolation is adopted.

Give an example to illustrate the interface isolation principle:

Figure 1 - design that does not follow the principle of interface isolation

This figure means that class a depends on Method 1, method 2 and method 3 in interface I, and class B is the implementation of class A. Class C depends on Method 1, method 4 and method 5 in interface I. class D is the implementation of class C dependency. For class B and class D, although they have unused methods (that is, the methods marked in red in the figure), they must also implement these unused methods because they implement interface I. If you are not familiar with the class diagram, you can refer to the program code. The code is as follows:

    interface I {
        public void method1();
        public void method2();
        public void method3();
        public void method4();
        public void method5();
    }

    class A{
        public void depend1(I i){
            i.method1();
        }
        public void depend2(I i){
            i.method2();
        }
        public void depend3(I i){
            i.method3();
        }
    }

    class B implements I{
        public void method1() {
            System.out.println("class B Implementation interface I Method 1");
        }
        public void method2() {
            System.out.println("class B Implementation interface I Method 2");
        }
        public void method3() {
            System.out.println("class B Implementation interface I Method 3");
        }
        //method4 and method5 are not required for class B, but because there are two methods in interface A,
        //Therefore, in the implementation process, even if the method bodies of the two methods are empty, the two useless methods should be implemented.
        public void method4() {}
        public void method5() {}
    }

    class C{
        public void depend1(I i){
            i.method1();
        }
        public void depend2(I i){
            i.method4();
        }
        public void depend3(I i){
            i.method5();
        }
    }

    class D implements I{
        public void method1() {
            System.out.println("class D Implementation interface I Method 1");
        }
        //method2 and method3 are not required for class D, but because there are two methods in interface A,
        //Therefore, in the implementation process, even if the method bodies of the two methods are empty, the two useless methods should be implemented.
        public void method2() {}
        public void method3() {}

        public void method4() {
            System.out.println("class D Implementation interface I Method 4");
        }
        public void method5() {
            System.out.println("class D Implementation interface I Method 5");
        }
    }

    public class Client{
        public static void main(String[] args){
            A a = new A();
            a.depend1(new B());
            a.depend2(new B());
            a.depend3(new B());

            C c = new C();
            c.depend1(new D());
            c.depend2(new D());
            c.depend3(new D());
        }
    }
copy

It can be seen that if the interface is too bloated, as long as the methods appear in the interface, whether they are useful to the classes that depend on them or not, they must be implemented in the implementation class, which is obviously not a good design. If the design is modified to conform to the interface isolation principle, the interface I must be split. Here, we split the original interface I into three interfaces. The split design is shown in Figure 2:

Figure 2 - Design following the principle of interface isolation)

Paste the program code as usual for the reference of friends who are not familiar with the class diagram:

    interface I1 {
        public void method1();
    }

    interface I2 {
        public void method2();
        public void method3();
    }

    interface I3 {
        public void method4();
        public void method5();
    }

    class A{
        public void depend1(I1 i){
            i.method1();
        }
        public void depend2(I2 i){
            i.method2();
        }
        public void depend3(I2 i){
            i.method3();
        }
    }

    class B implements I1, I2{
        public void method1() {
            System.out.println("class B Implementation interface I1 Method 1");
        }
        public void method2() {
            System.out.println("class B Implementation interface I2 Method 2");
        }
        public void method3() {
            System.out.println("class B Implementation interface I2 Method 3");
        }
    }

    class C{
        public void depend1(I1 i){
            i.method1();
        }
        public void depend2(I3 i){
            i.method4();
        }
        public void depend3(I3 i){
            i.method5();
        }
    }

    class D implements I1, I3{
        public void method1() {
            System.out.println("class D Implementation interface I1 Method 1");
        }
        public void method4() {
            System.out.println("class D Implementation interface I3 Method 4");
        }
        public void method5() {
            System.out.println("class D Implementation interface I3 Method 5");
        }
    }
copy

The meaning of the interface isolation principle is: establish a single interface, do not establish a huge and bulky interface, refine the interface as much as possible, and minimize the methods in the interface. In other words, we should establish a special interface for each class instead of trying to establish a huge interface for all classes that depend on it to call. In this example, the principle of interface isolation is used to change a huge interface into three special interfaces. In programming, relying on several special interfaces is more flexible than relying on a comprehensive interface. The interface is a "contract" set for the outside during design. By defining multiple interfaces in a decentralized manner, it can prevent the proliferation of external changes and improve the flexibility and maintainability of the system.

At this point, many people will feel that the interface isolation principle is very similar to the previous single responsibility principle, but it is not. First, the principle of single responsibility originally focused on responsibility; The interface isolation principle focuses on the isolation of interface dependencies. Second, the single responsibility principle is mainly the constraint class, followed by the interface and method, which aims at the implementation and details of the program; The interface isolation principle mainly restricts the interface, mainly for the abstraction and the construction of the overall framework of the program.

When using the interface isolation principle to restrict the interface, pay attention to the following points:

  • The interface should be as small as possible, but limited. It is a fact that refining the interface can improve the flexibility of program design, but if it is too small, it will cause too many interfaces and complicate the design. So we must be moderate.
  • Customizing services for classes that depend on interfaces only exposes the methods required by the calling class, while the methods it does not need are hidden. Only by focusing on providing customized services for a module can the minimum dependency be established.
  • Improve cohesion and reduce external interaction. Make the interface do the most with the least methods.

When using the principle of interface isolation, it must be appropriate. Too large or too small interface design is not good. When designing interfaces, only by spending more time thinking and planning can we accurately practice this principle.

Dimitt's law

definition

One object should have minimal knowledge of other objects.

Origin of the problem

The closer the relationship between classes, the greater the degree of coupling. When one class changes, the greater the impact on another class.

Solution

Minimize the coupling between classes.

Since we came into contact with programming, we have known the general principles of software programming: low coupling and high cohesion. Whether it is process oriented programming or object-oriented programming, only by making the coupling between each module as low as possible, can the reuse rate of code be improved. The advantages of low coupling are self-evident, but how can programming achieve low coupling? That's exactly what Demeter's law is going to accomplish.

Dimitri's law, also known as the least known principle, was first proposed by Ian Holland of Northeastern University in 1987. Generally speaking, a class knows as little as possible about the class it depends on. In other words, for the dependent classes, no matter how complex the logic is, the logic shall be encapsulated inside the class as much as possible, and no information shall be disclosed except the public method provided. There is a simpler definition of Dimitri's Law: only communicate with direct friends. First, let's explain what a direct friend is: each object will have a coupling relationship with other objects. As long as there is a coupling relationship between two objects, we say that the two objects are friends. There are many ways of coupling, such as dependency, association, combination, aggregation and so on. Among them, we call the classes that appear in member variables, method parameters and method return values as direct friends, while the classes that appear in local variables are not direct friends. In other words, unfamiliar classes should not appear inside the class as local variables.

For example: there is a group company whose subordinate companies have branches and departments directly under it. Now it is required to print out the employee ID of all subordinate companies. Let's start with a design that violates Dimitri's law.

    //Head office staff
    class Employee{
        private String id;
        public void setId(String id){
            this.id = id;
        }
        public String getId(){
            return id;
        }
    }

    //Branch employees
    class SubEmployee{
        private String id;
        public void setId(String id){
            this.id = id;
        }
        public String getId(){
            return id;
        }
    }

    class SubCompanyManager{
        public List getAllEmployee(){
            List list = new ArrayList();
            for(int i=0; i<100; i++){
                SubEmployee emp = new SubEmployee();
                //Assign an ID to branch personnel in order
                emp.setId("branch office"+i);
                list.add(emp);
            }
            return list;
        }
    }

    class CompanyManager{

        public List getAllEmployee(){
            List list = new ArrayList();
            for(int i=0; i<30; i++){
                Employee emp = new Employee();
                //Assign an ID to the head office personnel in order
                emp.setId("headquarters"+i);
                list.add(emp);
            }
            return list;
        }

        public void printAllEmployee(SubCompanyManager sub){
            List list1 = sub.getAllEmployee();
            for(SubEmployee e:list1){
                System.out.println(e.getId());
            }

            List list2 = this.getAllEmployee();
            for(Employee e:list2){
                System.out.println(e.getId());
            }
        }
    }

    public class Client{
        public static void main(String[] args){
            CompanyManager e = new CompanyManager();
            e.printAllEmployee(new SubCompanyManager());
        }
    }
copy

Now the main problem of this design lies in the CompanyManager. According to the Demeter's law, it only communicates with direct friends, while the SubEmployee class is not a direct friend of the CompanyManager class (the coupling in local variables does not belong to direct friends). Logically, the head office is only coupled with its branches, and there is no connection with the employees of the branches. This design obviously increases unnecessary coupling. According to Demeter's law, such coupling of indirect friends in classes should be avoided. The modified code is as follows:

    class SubCompanyManager{
        public List getAllEmployee(){
            List list = new ArrayList();
            for(int i=0; i<100; i++){
                SubEmployee emp = new SubEmployee();
                //Assign an ID to branch personnel in order
                emp.setId("branch office"+i);
                list.add(emp);
            }
            return list;
        }
        public void printEmployee(){
            List list = this.getAllEmployee();
            for(SubEmployee e:list){
                System.out.println(e.getId());
            }
        }
    }

    class CompanyManager{
        public List getAllEmployee(){
            List list = new ArrayList();
            for(int i=0; i<30; i++){
                Employee emp = new Employee();
                //Assign an ID to the head office personnel in order
                emp.setId("headquarters"+i);
                list.add(emp);
            }
            return list;
        }

        public void printAllEmployee(SubCompanyManager sub){
            sub.printEmployee();
            List list2 = this.getAllEmployee();
            for(Employee e:list2){
                System.out.println(e.getId());
            }
        }
    }
copy

After modification, the method of printing personnel ID is added for the branch company, and the head office is directly called to print, so as to avoid coupling with the employees of the branch company.

The original intention of dimitt's law is to reduce the coupling between classes. Because each class reduces unnecessary dependencies, it can indeed reduce the coupling relationship. However, everything has a degree. Although indirect communication can be avoided, communication must be connected through an "intermediary". For example, in this example, the head office contacts the employees of the branch through the "intermediary" of the branch. Excessive use of the Demeter principle will produce a large number of such mediation and delivery classes, resulting in greater system complexity. Therefore, it is necessary to weigh repeatedly when using Dimitri's law, so as to achieve not only clear structure, but also high cohesion and low coupling.

Opening and closing principle

definition

A software entity such as classes, modules and functions should be open to extensions and closed to modifications.

Origin of the problem

During the software life cycle, when the original code of the software needs to be modified due to changes, upgrades and maintenance, errors may be introduced into the old code, and we may have to reconstruct the whole function, and the original code needs to be retested.

Solution

When the software needs to change, try to realize the change by expanding the behavior of the software entity, rather than by modifying the existing code.

The opening and closing principle is the most basic design principle in object-oriented design. It guides us how to establish a stable and flexible system. The opening and closing principle may be the most vaguely defined of the six principles of design pattern. It only tells us to open to expansion and close to modification, but it doesn't tell us clearly how to open to expansion and close to modification. In the past, if someone told me "you must abide by the opening and closing principle when designing", I would feel that he didn't say anything, but it seems that he said everything. Because the opening and closing principle is really too empty.

After careful thinking and reading many articles on design patterns, I finally have a little understanding of the opening and closing principle. In fact, we follow the first five principles of design patterns, and the purpose of using 23 design patterns is to follow the opening and closing principles. In other words, as long as we follow the first five principles well, the designed software naturally conforms to the opening and closing principles. This opening and closing principle is more like the "average score" of the compliance degree of the first five principles. If we follow the first five principles well, the average score will naturally be high, indicating that the opening and closing principles of software design are well observed; If the first five principles are not well observed, it means that the opening and closing principles are not well observed.

In fact, the author believes that the opening and closing principle is nothing more than to express such a layer of meaning: building a framework with abstraction and expanding details with implementation. Because the abstraction has good flexibility and wide adaptability, as long as the abstraction is reasonable, the stability of the software architecture can be basically maintained. For the changeable details in the software, we use the implementation class derived from the abstraction to extend. When the software needs to change, we only need to re derive an implementation class to extend according to the requirements. Of course, the premise is that our abstraction should be reasonable, and we should be forward-looking and predictive of changes in requirements.

At this point, let's recall the five principles mentioned above, which just tell us the precautions for building a framework with abstraction and implementing the extension details: the single responsibility principle tells us that the implementation class should have a single responsibility; Richter's substitution principle tells us not to destroy the inheritance system; The dependency inversion principle tells us to face interface programming; The principle of interface isolation tells us to simplify and simplify the interface design; Demeter's law tells us to reduce coupling. The opening and closing principle is the general outline. He told us to open to expansion and close to modification.

Finally, explain how to abide by these six principles. Compliance with the these six principles is not a matter of the yes or no, but a matter of the more or less. That is to say, we generally do not say whether we have complied with the them, but how much we have complied with the them. Everything is too much, and the six design principles of design pattern are the same. The purpose of formulating these six principles is not to ask us to strictly abide by them, but to apply them flexibly according to the actual situation. As long as the degree of compliance with them is within a reasonable range, it is a good design. Let's illustrate with a picture.

Each dimension in the figure represents a principle. We draw a point on the dimension according to the degree of compliance with this principle. If the compliance with this principle is reasonable, this point should fall inside the red concentric circle; If the difference is observed, the point will be inside the small circle; If you follow too much, the point will fall outside the big circle. A good design is reflected in the figure. It should be a hexagon with six vertices in a concentric circle.

In the above figure, design 1 and design 2 are good designs, and their compliance with the six principles is within a reasonable range; Design 3 and design 4 although there are some deficiencies, the design is basically acceptable; The design 5 is seriously insufficient and does not comply with all principles well; Design 6 follows the transition. Design 5 and design 6 are designs that urgently need reconstruction.

Here, the six principles of design pattern have been written. The main reference books are "design pattern", "Zen of design pattern", "Dahua design pattern" and some scattered articles on the Internet, but the main content is my own perception of these six principles. On the one hand, the purpose of writing is to systematically sort out these six principles, and on the other hand, to share with the majority of netizens, because design patterns are indeed very important for programmers. Just as there is a saying that there are 1000 Hamlets in the eyes of 1000 readers, if your understanding of these six principles is different from me, please leave a message and discuss it together.

Posted by hack4lk on Wed, 11 May 2022 15:57:33 +0300