Design pattern 01 - seven design principles

Posted by ashishag67 on Wed, 26 Jan 2022 02:06:55 +0100

Design pattern 01 - seven design principles

Opening and closing principle - Open Close

A software entity such as class, module and function should be open to extension and closed to modification. Build the framework with abstraction and extend the details with implementation. Improve the reusability and maintainability of the program.

Case: a course class with three attributes: course type, course name and selling price. The code implementation is as follows:

Course interface:

public interface ICourse {
    // Get type
    String getCategory();
    // Get name
    String getName();
    // Get price
    Double getPrice();
}

Java course implementation class:

public class JavaCourse implements ICourse {

    private String name;
    private Double price;
    private String category;

    public JavaCourse(String name, Double price, String category) {
        this.name = name;
        this.price = price;
        this.category = category;
    }

    @Override
    public String getCategory() {
        return category;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public Double getPrice() {
        return price;
    }
}

Test class:

public static void main(String[] args) {
    JavaCourse course = new JavaCourse("architect ", 998.00, "Java");
    System.out.println("Course:" + course.getName() 
                       + " Classification:" + course.getCategory() 
                       + " Price:" + course.getPrice());
}

Problem: at this time, the price should be modified according to the situation. For example, 20% off for double 11 and 50% off for the Spring Festival. We can modify it directly in the getPrice() method, which obviously violates the definition of modifying and closing in the opening and closing principle, such as the following:

@Override
public Double getPrice() {
    return price * 0.8;// Double 11 dozen 20% off
}

Problems caused by this:

  1. Frequently modify the existing code logic, such as the Spring Festival, double 11 and double 12. Each member may have different discount calculation methods. At this time, we have to modify our existing program to reduce the maintainability of the program
  2. It violates the opening and closing principle

What should we do at this time? Of course, we extend our program through the open definition of extension:

For example, we can define a discount class of JavaCourse to implement this logic:

public class JavaDiscountCourse extends JavaCourse {

    public JavaDiscountCourse(String name, Double price, String category) {
        super(name, price, category);
    }

    public Double getDiscountPrice() { // Get discount price
        return super.getPrice() * 0.8;
    }
}

Then modify our main program to test:

public static void main(String[] args) {
    JavaCourse course = new JavaDiscountCourse("architect ", 998.00, "Java");
    System.out.println("Course:" + course.getName() +
            " Classification:" + course.getCategory() +
            " Discount price:" + course.getPrice() +
            "original price:" + ((JavaDiscountCourse) course).getDiscountPrice()
    );
}

This not only conforms to the concept of opening and closing principle, but also improves the reusability and maintainability of our program.

Dependency Inversion Principle

Definition: high-level modules should not rely on low-level modules, and both should rely on their abstraction; Abstract should not rely on details, and details should rely on abstraction; For interface programming, not for implementation programming

Advantages: it can reduce the coupling between classes, improve system stability, improve code readability and maintainability, and reduce the risk caused by program modification

Concept of high-level and low-level: the closer to the caller, the higher the level. The closer to the callee, the lower the level. If I call a method of someone else's interface, my code is a high-level module, and what is called is the low-level module.

Possible problems caused by high-level dependence on low-level modules: if the low-level method parameter modifier and return value type change, the high-level caller should also make corresponding changes.

Case: Tom class needs to learn two courses: Java and Python

Tom class (callee: lower level):

public class Tom {

    public void studyJava() {
        System.out.println("study Java curriculum");
    }
    public void studyPython() {
        System.out.println("study Python curriculum");
    }

}

Test class (that is, our high-level call):

public class DIDemo {
    public static void main(String[] args) {
        Tom tom = new Tom();
        tom.studyJava();
        tom.studyPython();
    }
}

At this point, the program looks ok. However, if the studyJava() method in Tom is changed to javaStudy(), the high-level calls also need to be modified. In this way, the risk caused by modification is increased, and the coupling of the program is strong.

Next, make a modification: both high-level and low-level rely on abstraction:

First, establish the abstract (the abstract interface of the course):

public interface ICourse {
    void study();
}

Then create Java and Python learning classes and implement the course interface:

public class JavaStudy implements ICourse {
    @Override
    public void study() {
        System.out.println("study Java");
    }
}

public class PythonStudy implements ICourse {
    @Override
    public void study() {
        System.out.println("study Python");
    }
}

At this time, Tom class (lower level) depends on abstraction:

public class Tom {
	// Rely on abstract ICourse
    public void study(ICourse iCourse) {
        iCourse.study();
    }
}

At this time, our test class (high level) can be modified to rely on abstraction rather than details:

public static void main(String[] args) {
    Tom tom = new Tom();
    tom.study(new JavaStudy());
    tom.study(new PythonStudy());
}

This reduces the coupling of our program and improves the stability and readability of the program. Reduce the risk of modifying the program.

Version 3: of course, we can also optimize the Tom class by constructing methods:

public class Tom {

    private ICourse iCourse;

    public Tom(ICourse iCourse) {
        this.iCourse = iCourse;
    }

    public void study() {
        iCourse.study();
    }

}

In this way, you only need to pass in the implementation class of ICourse in the test class to construct Tom. Similarly, you can also use Set and other methods for injection.

One sentence summary: interface oriented programming

Single responsibility principle - Simple ResponsiBility

Definition: there should be no more than one cause of class change. To put it bluntly: a class, interface and method are only responsible for one responsibility.

Advantages: reduce code complexity, improve program readability, improve system maintainability and reduce risks caused by changes

Case: when watching two kinds of courses, the live class can't fast forward, and the video class can fast forward

Course category (different processing logic for different courses):

public class Course {
    public void study(String courseName) {
        if ("Live class".equals(courseName)) {
            System.out.println("Live class can't fast forward");
        } else {
            System.out.println("You can fast forward the recording and broadcasting class");
        }
    }
}

Test class:

public class StudyDemo {
    public static void main(String[] args) {
        Course course = new Course();
        course.study("Live class");
        course.study("Recording and broadcasting course");
    }
}

At this time, if we want to do different processing for different types of courses, such as encoding and decoding, we may need to modify the Course#study method, which is bound to increase the complexity of the code and reduce the readability.

Next, we modify it as follows: handle different courses in different classes:

Live class

public class LiveCourse {
    public void study(String courseName) {
        System.out.println(courseName + "Online viewing only");
    }
}

Recording and broadcasting class

public class ReplayCourse {
    public void study(String courseName) {
        System.out.println(courseName + "You can watch it again and again");
    }
}

In this way, we can directly call different classes for processing. It reduces the complexity and improves the readability.

However, there may be many new responsibilities for the course later: such as obtaining video streams, refunds, learning courses and obtaining basic course information. What should we do at this time?

Interface level:

Such as user interface, you can watch videos and refund:

public interface ICourseManager {
    void readVideo();// Watch videos
    void refundCourse();// refund
}

Course information interface, which can be used to obtain course information:

public interface ICourseInfo {
    String getCourseName();// Get course information
}

In this way, the new course information will not affect other responsibilities, so it meets the definition of single responsibility

public class CourseImpl implements ICourseInfo, ICourseManager {
    @Override
    public String getCourseName() {
        return null;
    }

    @Override
    public void readVideo() {
    }

    @Override
    public void refundCourse() {
    }
}

Method level:

Take modifying user information as an example

public class LoginMethod {
    private void updateUserName() {
        // Modify user name
    }
    private void updateUserPhone() {
        // Modify user mobile number
    }
    private void updateUserAddress() {
        // Modify user address
    }
}

As mentioned above, the separation method with smaller particle size makes it look clear, and the responsibilities are clear and will not affect each other.

Interface isolation principle - Interface aggregation

Definition: use multiple special interfaces instead of a single general interface. The client should not rely on the interfaces it does not need

Note: the dependency of a class corresponding to a class should be based on the smallest interface; Establish a single interface, not a huge and bloated interface; Refine the interface as much as possible and minimize the methods in the interface; Pay attention to the principle of moderation and be moderate

Advantages: high cohesion and low coupling, so as to improve readability, scalability and maintainability

For example, we have an animal interface, which provides some methods:

public interface IAnimal {

    // run
    void run();
    // fly
    void fly();
    // Swimming
    void swim();
    // Eat something
    void eat();

}

At this time, if we have a bird to implement the interface, we need to implement some methods that do not need to be implemented, such as swimming.

At this time, we can establish interfaces and provide methods for eating, running, flying and swimming. At this time, migratory birds only need to implement the interfaces of eating and flying.

Law of Demeter

Definition: an object should keep the least knowledge of other objects, also known as the least knowledge principle

Emphasize only communicating with friends and not talking to strangers

Concept of friend: the classes that appear in member variables, method input and output parameters are called member friend classes, while the classes that appear in the method body do not belong to friend classes

Function: decoupling to a certain extent

Case: the team leader asked the employee to query the number of existing courses

Courses:

public class Course {
    private String name;

    public String getName() {
        return name;
    }

    public Course setName(String name) {
        this.name = name;
        return this;
    }
}

Employee type (used to query the number of courses):

public class Employee {
    public int getCourseNumber(List<Course> courses) {
        return courses.size();
    }
}

Team leader:

public class TeamLeader {
    public int getNumbersByEmployee(Employee employee) {
        List<Course> courses = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            courses.add(new Course());
        }
        return employee.getCourseNumber(courses);
    }
}

Next is the test method:

public static void main(String[] args) {
    Employee employee = new Employee();
    TeamLeader teamLeader = new TeamLeader();
    System.out.println(teamLeader.getNumbersByEmployee(employee));
}

The problem is found here: the Course class referenced by TeamLeader does not meet the Dimitri rule, that is, Course is not its "friend" in TeamLeader.

Richter substitution principle Liskov Substitution

Definition: if there is an 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 extension: if a software entity is applicable to a parent class, it must be applicable to its subclasses. All places referencing the parent class must be able to use the objects of its subclasses transparently. The subclass objects can replace the parent objects, and the program logic remains unchanged

Extended meaning: subclasses can extend the functions of the parent class, but cannot change the original functions of the parent class. For example, in the case of the opening and closing principle, the discount course class has written a new discount method:

public class JavaDiscountCourse extends JavaCourse {

    public JavaDiscountCourse(String name, Double price, String category) {
        super(name, price, category);
    }
    
    public Double getDiscountPrice() { // Get discount price
        return super.getPrice() * 0.8;
    }

}

Meaning 1: subclasses can implement the abstract methods of the parent class, but cannot override the non abstract methods of the parent class

Meaning 2: you can add your own unique methods in subclasses

Meaning 3: when a subclass method overloads the method of the parent class, the preconditions of the method (i.e. method input and input parameters) are more relaxed than the input parameters of the parent class

Meaning 4: when the method of the subclass implements the method of the parent class (overriding, overloading or implementing abstract methods), the post conditions of the method (i.e. the output and return value of the method) are stricter or equal than those of the parent class.

Advantage 1: the overflow of constraint inheritance is an embodiment of the opening and closing principle

Advantage 2: strengthen the robustness of the program. At the same time, it can achieve very good compatibility when changing, and improve the maintainability and expansibility of the program. Reduce the risks introduced when requirements change

Example: taking a square rectangle as an example, obtain the width and height of the rectangle in the test class. If the width is greater than the height, modify the parameters until the width is less than or equal to the height. The code is as follows:

Rectangle class:

public class Rectangle {

    private Integer width;// wide

    private Integer height;// high

    public Integer getWidth() {
        return width;
    }

    public Rectangle setWidth(Integer width) {
        this.width = width;
        return this;
    }

    public Integer getHeight() {
        return height;
    }

    public Rectangle setHeight(Integer height) {
        this.height = height;
        return this;
    }
}

Square class:

public class Square extends Rectangle {

    private Integer length;

    public Integer getLength() {
        return length;
    }

    public Square setLength(Integer length) {
        this.length = length;
        return this;
    }

    @Override
    public Integer getWidth() {
        return this.length;
    }

    @Override
    public Rectangle setWidth(Integer width) {
        return setLength(width);
    }

    @Override
    public Integer getHeight() {
        return this.length;
    }

    @Override
    public Rectangle setHeight(Integer height) {
        return setLength(height);
    }
}

You can see that in order to meet the equal side length attribute of the square, we have modified the get and set methods of the width and height of its parent class.

Next, the test class defines a resize() method. If the width of the rectangle is greater than the height, the height will be + 1 in while:

public class TestDemo {
    private static void resize(Rectangle rectangle) {
        while (rectangle.getWidth() >= rectangle.getHeight()) {
            rectangle.setHeight(rectangle.getHeight() + 1);
            System.out.println("Current width:" + rectangle.getHeight());
        }
    }

    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle();
        rectangle.setWidth(20);
        rectangle.setHeight(10);
        resize(rectangle);
    }
}

Here we use the parent class to test, and the console prints as follows:

Current width: 11
 Current width: 12
 Current width: 13
 Current width: 14
 Current width: 15
 Current width: 16
 Current width: 17
 Current width: 18
 Current width: 19
 Current width: 20
 Current width: 21

Process finished with exit code 0

Next, test with the subclass Square:

Current width: 407744
 Current width: 407745
 Current width: 407746
 Current width: 407747
 Current width: 407748
 Current width: 407749
...Infinite loop

It can be seen that this resize() passes subclasses, which destroys the normal logic of the method and does not meet the Richter substitution principle.

Review the definition again: when all program P defined in T1 are replaced with o2, the behavior of program P does not change

So how to solve the above problems?

Define a quadrilateral interface:

public interface Quadrangle {
    Integer getHeight();// Get height
    Integer getWidth();// Get width
}

Define a square class:

public class Square implements Quadrangle {

    private Integer length;

    public Square setLength(Integer length) {
        this.length = length;
        return this;
    }

    @Override
    public Integer getHeight() {
        return this.length;
    }

    @Override
    public Integer getWidth() {
        return this.length;
    }
}

Define a rectangle class:

public class Rectangle implements Quadrangle {

    private Integer width;// wide

    private Integer height;// high

    public Rectangle setWidth(Integer width) {
        this.width = width;
        return this;
    }

    public Rectangle setHeight(Integer height) {
        this.height = height;
        return this;
    }

    @Override
    public Integer getHeight() {
        return this.height;
    }

    @Override
    public Integer getWidth() {
        return this.width;
    }
}

Test procedure:

public class TestDemo {

    private static void resize(Quadrangle quadrangle) {
        while (quadrangle.getWidth() >= quadrangle.getHeight()) {
            // Since the quadrilateral does not provide a set method, an error will be reported here
            quadrangle.setHeight(quadrangle.getHeight() + 1);
            System.out.println("Current width:" + quadrangle.getHeight());
        }
    }

    public static void main(String[] args) {
        Square square = new Square();
        square.setLength(10);// A square with 10 sides
        resize(square);
    }
}

Because the quadrilateral class does not provide setHeight() method, the fifth line of code here will report an error, which avoids the overflow of inheritance to a certain extent.

Composite & Aggregate Reuse

Definition: try to use object combination and aggregation instead of inheritance to achieve the purpose of software reuse

Aggregation: has - a, such as computers and U SB flash drives, can work together or computers can work alone

Combination: contains - a, such as various parts of the human body, can have a complete life cycle only when they are combined together

Inheritance: is - a

Advantages: it can make the system more flexible and reduce the coupling between classes. The change of one class has relatively less impact on other classes

rangle.getWidth() >= quadrangle.getHeight()) {
//Since the quadrilateral does not provide a set method, an error will be reported here
quadrangle.setHeight(quadrangle.getHeight() + 1);
System.out.println("current width:" + quadrangle.getHeight());
}
}

public static void main(String[] args) {
    Square square = new Square();
    square.setLength(10);// A square with 10 sides
    resize(square);
}

}

Because the quadrilateral class does not provide setHeight()Method, so the fifth line of code here will report an error, which avoids the overflow of inheritance to a certain extent.



## Composite & Aggregate Reuse

definition:**Try to use object composition and aggregation instead of inheritance to achieve the purpose of software reuse**

Aggregation:**has - a** , Like computers and U Disks can work together, and computers can work alone

Combination:**contains - a**,For example, all parts of the human body can have a complete life cycle only when they are combined together

Inheritance:**is - a**

advantage:**It can make the system more flexible and reduce the coupling between classes. The change of one class has relatively less impact on other classes**

**One sentence summary: you don't have to inherit if you can**

Finally, it is concluded that the design pattern is some norms and constraints in our development. In the actual development, we do not want to pursue perfection, but try to abide by the norms when time, cost and other aspects allow.

Topics: Java Design Pattern