Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

This article also participated in the “Digitalstar Project” to win a creative gift package and creative incentive money

In software development, in order to improve the maintainability and reusability of the software system, and increase the scalability and flexibility of the software, programmers should try to develop programs according to the six principles, so as to improve the efficiency of software development, save software development costs and maintenance costs.

The open closed principle

Open for extensions, closed for modifications. When the program needs to be extended, you cannot modify the original code to achieve a hot plug effect. In short, to make the program extensible, easy to maintain and upgrade.

To achieve this, we need to use interfaces and abstract classes.

Because abstraction has good flexibility and wide adaptability, as long as abstraction is reasonable, it can basically maintain the stability of software architecture. The changeable details in software can be extended from the abstract implementation class. When the software needs to change, it only needs to derive an implementation class to extend according to the requirements.

The following skin sogou input method as an example to introduce the application of the open and close principle.

The skin design of Sogou input method.

Analysis: The skin of sogou input method is the combination of elements such as input background picture, window color and sound. Users can change the skin of their input method according to their own preference, or download a new skin from the Internet. These skins have common characteristics and it is possible to define an abstract class (AbstractSkin) for them, each of which (DefaultSpecificSkin< default skin > and BeautifulSpecificSkin< BeautifulSpecificSkin >) is a subclass. The user form can select or add new themes as needed without modifying the original code, so it is open and closed.

The UML diagram is as follows:

We define an abstract class (AbstractSkin) and each specific skin (DefaultSpecificSkin and BeautifulSpecificSkin) is a subclass of it, both of which are inherited. The SouGouInput class takes the skin attribute as the member variable of the class, so the two belong to the association relation. Then, according to the scene, the association relation is subdivided, and the relationship between the two is aggregation relation.

The code is as follows:

An abstract classAbstractSkin

public abstract class AbstractSkin {

    // Display method
    public abstract void display(a);
}
Copy the code

A subclassDefaultSpecificSkin

public class DefaultSkin extends AbstractSkin {

    public void display(a) {
        System.out.println("Default skin"); }}Copy the code

A subclassBeautifulSpecificSkin

public class BeautifulSpecificSkin extends AbstractSkin {

    public void display(a) {
        System.out.println("Beautify your skin."); }}Copy the code

Sogou input methodSouGouInput

public class SougouInput {

    private AbstractSkin skin;

    public void setSkin(AbstractSkin skin) {
        this.skin = skin;
    }

    public void display(a) { skin.display(); }}Copy the code

The test class

public class Client {
    public static void main(String[] args) {
        //1, create sogou input method object
        SougouInput input = new SougouInput();
        //2, create a skin object
        //DefaultSkin skin = new DefaultSkin();
        BeautifulSpecificSkin skin = new BeautifulSpecificSkin();
        //3, set the skin to the input method
        input.setSkin(skin);

        //4, display skininput.display(); }}Copy the code

Richter’s substitution principle

Richter’s substitution principle is one of the basic principles of object-oriented design.

Cough cough, the following is some mandarin, do not want to bother the students can directly see my vernacular summary

Richter’s substitution rule: Wherever a base class can appear, a subclass must appear. A subclass extends the functionality of its parent class, but does not change the functionality of its parent class. In other words, when a subclass inherits from a parent class, it should try not to override the methods of the parent class, except to add new methods to accomplish new functionality.

If the method of rewriting the parent class to complete the new function, although it is simple to write, but the reusability of the whole inheritance system will be relatively poor, especially when the use of more frequent polymorphism, the probability of program running error will be very large.

** Summary: ** Try not to overwrite methods already implemented by parent classes

Now let’s look at a classic example of Richter’s substitution principle

A square is not a rectangle.

In mathematics, a square is unquestionably a rectangle; it is a rectangle of equal length and width. So, we developed a software system related to geometry, and it is natural to let the square inherit from the rectangle.

The UML diagram is as follows:

Here we define a Rectangle parent class, because cube is a special cuboid, so we define a Square class inherit Rectangle class, c test class resize method parameter is a Rectangle type, so the two are dependent. This method is used to achieve a gradual increase in width

The code is as follows:

Rectangle class:

public class Rectangle {
    private double length;
    private double width;

    public double getLength(a) {
        return length;
    }

    public void setLength(double length) {
        this.length = length;
    }

    public double getWidth(a) {
        return width;
    }

    public void setWidth(double width) {
        this.width = width; }}Copy the code

Square:

Since the square is the same length and width, the setLength and setWidth methods require the same value for both length and width.

public class Square extends Rectangle {
    
    public void setWidth(double width) {
        super.setLength(width);
        super.setWidth(width);
    }

    public void setLength(double length) {
        super.setLength(length);
        super.setWidth(length); }}Copy the code

The RectangleDemo class, a component of our software system, has a method called Resize, which relies on the base class Rectangle, which is a method in the RectandleDemo class to achieve the effect of a gradual increase in width.

public class RectangleDemo {
    
    public static void resize(Rectangle rectangle) {
        while (rectangle.getWidth() <= rectangle.getLength()) {
            rectangle.setWidth(rectangle.getWidth() + 1); }}// Prints the length and width of the rectangle
    public static void printLengthAndWidth(Rectangle rectangle) {
        System.out.println(rectangle.getLength());
        System.out.println(rectangle.getWidth());
    }

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

        System.out.println("= = = = = = = = = = = =");

        Rectangle rectangle1 = new Square();
        rectangle1.setLength(10); resize(rectangle1); printLengthAndWidth(rectangle1); }}Copy the code

If we pass a normal rectangle as a parameter to the resize method, we will see a gradual increase in the width of the rectangle. When the width is greater than the length, the code will stop. If we pass another square as an argument to the resize method, we see that the square’s width and length grow, and the code keeps running until the system generates an overflow error. So, normal rectangles are appropriate for this code, squares are not. We conclude that a Rectangle parameter cannot be replaced by a Square parameter in the resize method. Thus, the inheritance relationship between the Square class and the Rectangle class violates the Richter’s substitution principle. The inheritance relationship between the Square class and the Rectangle class is not true.

How can it be improved? We need to redesign the relationship between them. Abstract a Quadrilateral interface and have the Rectangle and Square classes implement the Quadrilateral interface

Note: The Rectangle parameter for the resizef method is Rectangle.

The code is as follows:

5. Quadrilateral

public interface Quadrilateral {

    / / for long
    double getLength(a);

    / / get wide
    double getWidth(a);
}
Copy the code

Rectangle class:

public class Rectangle implements Quadrilateral {

    private double length;
    private double width;

    public void setLength(double length) {
        this.length = length;
    }

    public void setWidth(double width) {
        this.width = width;
    }

    public double getLength(a) {
        return length;
    }

    public double getWidth(a) {
        returnwidth; }}Copy the code

Square:

public class Square implements Quadrilateral {

    private double side;

    public double getSide(a) {
        return side;
    }

    public void setSide(double side) {
        this.side = side;
    }

    public double getLength(a) {
        return side;
    }

    public double getWidth(a) {
        returnside; }}Copy the code

test

public class RectangleDemo {
    public static void main(String[] args) {
        // Create a rectangle object
        Rectangle r = new Rectangle();
        r.setLength(20);
        r.setWidth(10);
        // Call the method to expand
        resize(r);

        printLengthAndWidth(r);
    }

    // The method of broadening
    public static void resize(Rectangle rectangle) {
        // Determine if the width is smaller than the length, expand the operation
        while(rectangle.getWidth() <= rectangle.getLength()) {
            rectangle.setWidth(rectangle.getWidth() + 1); }}// Print the length and width
    public static void printLengthAndWidth(Quadrilateral quadrilateral) { System.out.println(quadrilateral.getLength()); System.out.println(quadrilateral.getWidth()); }}Copy the code

Rely on the inversion principle

A high-level module should not depend on a low-level module; both should depend on its abstraction; Abstractions should not depend on details, details should depend on abstractions. Simply put, requiring that the abstraction be programmed, not the implementation, reduces the coupling between the client and the implementation module.

In fact, the dependency inversion principle is the concrete implementation of the open and closed principle, which is used in the example of the open and closed principle. Popular point is oriented to abstraction, interface programming

Let’s look at an example to understand the dependency inversion principle

Assemble a computer

Now to assemble a computer, you need accessories CPU, hard disk, memory. Only when these configurations are available can the computer run properly. There are many choices to choose from CPU, such as Intel, AMD, etc., hard disk can choose Seagate, Western digital, etc., memory can choose Kingston, Pirate Ship, etc.

The UML diagram is as follows:

The code is as follows:

XiJieHardDisk:

public class XiJieHardDisk implements HardDisk {

    public void save(String data) {
        System.out.println("Use Seagate hard drive to store data." + data);
    }

    public String get(a) {
        System.out.println("Use Seagate Seagate hard drive to fetch data.");
        return "Data"; }}Copy the code

Intel CPU:

public class IntelCpu implements Cpu {

    public void run(a) {
        System.out.println("Using an Intel processor"); }}Copy the code

KingstonMemory:

public class KingstonMemory implements Memory {

    public void save(a) {
        System.out.println("Use Kingston as a Memory chip"); }}Copy the code

Computer:

public class Computer {

    private XiJieHardDisk hardDisk;
    private IntelCpu cpu;
    private KingstonMemory memory;

    public IntelCpu getCpu(a) {
        return cpu;
    }

    public void setCpu(IntelCpu cpu) {
        this.cpu = cpu;
    }

    public KingstonMemory getMemory(a) {
        return memory;
    }

    public void setMemory(KingstonMemory memory) {
        this.memory = memory;
    }

    public XiJieHardDisk getHardDisk(a) {
        return hardDisk;
    }

    public void setHardDisk(XiJieHardDisk hardDisk) {
        this.hardDisk = hardDisk;
    }

    public void run(a) {
        System.out.println("Computer work");
        cpu.run();
        memory.save();
        String data = hardDisk.get();
        System.out.println("The data obtained from the hard disk is:"+ data); }}Copy the code

Test class (TestComputer)

Test classes are used to assemble computers.

public class TestComputer {
    public static void main(String[] args) {
        Computer computer = new Computer();
        computer.setHardDisk(new XiJieHardDisk());
        computer.setCpu(new IntelCpu());
        computer.setMemory(newKingstonMemory()); computer.run(); }}Copy the code

The code above shows that a computer has been assembled, but it seems that the CPU must be Intel, the memory must be Kingston, and the hard disk must be Seagate. This is not user-friendly, and users must want to choose their own accessories according to their own preferences.

Improvement according to the principle of dependency reversal:

In the code, we just need to modify the Computer class so that it relies on abstraction (interfaces for individual components) rather than on concrete implementation classes for individual components.

The UML diagram is as follows:

Computer:

public class Computer {

    private HardDisk hardDisk;
    private Cpu cpu;
    private Memory memory;

    public HardDisk getHardDisk(a) {
        return hardDisk;
    }

    public void setHardDisk(HardDisk hardDisk) {
        this.hardDisk = hardDisk;
    }

    public Cpu getCpu(a) {
        return cpu;
    }

    public void setCpu(Cpu cpu) {
        this.cpu = cpu;
    }

    public Memory getMemory(a) {
        return memory;
    }

    public void setMemory(Memory memory) {
        this.memory = memory;
    }

    public void run(a) {
        System.out.println("Computer work"); }}Copy the code

Object-oriented development is a good solution to this problem, in general, the probability of the change of abstraction is very small, so that the user program depends on abstraction, implementation details also depend on abstraction. Even if the implementation details are constantly changing, the client program does not need to change as long as the abstraction remains the same. This greatly reduces the coupling between the client and the implementation details.

Interface Isolation Principle

A client should not be forced to rely on methods it does not use; The dependency of one class on another should be based on the smallest interface.

Let’s look at an example to understand the interface isolation principle

Safety door case

We need to create a vegetable brand safety door, the safety door has the function of fire prevention, waterproof, anti-theft. Can be fire, waterproof, anti-theft function extraction into an interface, the formation of a set of specifications. The class diagram is as follows:

We found the problems in the above design. The safety door of Vegetable brand has the functions of anti-theft, waterproof and fire prevention. Now if we also need to create a zhang SAN brand security door, and the security door only has anti-theft, waterproof function? It is clear that implementing the SafetyDoor interface violates interface isolation, so how can we change it? Look at the following class diagram:

The code is as follows:

AntiTheft (interface) :

public interface AntiTheft {
    void antiTheft(a);
}
Copy the code

Fireproof (interface) :

public interface Fireproof {
    void fireproof(a);
}
Copy the code

Waterproof:

public interface Waterproof {
    void waterproof(a);
}
Copy the code

CaiCaiSafetyDoor (class)

public class CaiCaiSafetyDoor implements AntiTheft.Fireproof.Waterproof {
    public void antiTheft(a) {
        System.out.println("Security");
    }

    public void fireproof(a) {
        System.out.println("Fire");
    }


    public void waterproof(a) {
        System.out.println("Waterproof"); }}Copy the code

ZhangSanSafetyDoor:

public class ZhangSanSafetyDoor implements AntiTheft.Fireproof {
    public void antiTheft(a) {
        System.out.println("Security");
    }

    public void fireproof(a) {
        System.out.println("Fire"); }}Copy the code

Demeter’s rule

Demeter’s rule is also known as the least knowledge principle.

Talk only to your immediate friends and not to strangers.

The implication is that if two software entities do not communicate directly, then direct calls to each other should not occur and can be forwarded by a third party. Its purpose is to reduce the degree of coupling between classes and improve the relative independence of modules.

The “friend” in Demeter’s law refers to the current object itself, its member object, the object created by the current object, the method parameters of the current object, etc. These objects are associated, aggregated or combined with the current object, and can directly access the methods of these objects.

That’s what we call an agent

Let’s look at an example to understand Demeter’s rule

An example of the relationship between a star and his agent

As stars devote themselves to their art, their agents handle many daily tasks, from meeting fans to negotiating with media companies. Agents here are friends of the stars, and fans and media companies are strangers, so Demeter’s rule applies.

The UML diagram is as follows:

The code is as follows:

Star Category

public class Star {
    private String name;

    public Star(String name) {
        this.name=name;
    }

    public String getName(a) {
        returnname; }}Copy the code

Fans

public class Fans {
    private String name;

    public Fans(String name) {
        this.name=name;
    }

    public String getName(a) {
        returnname; }}Copy the code

Media Companies

public class Company {
    private String name;

    public Company(String name) {
        this.name=name;
    }

    public String getName(a) {
        returnname; }}Copy the code

Agent

public class Agent {
    private Star star;
    private Fans fans;
    private Company company;

    public void setStar(Star star) {
        this.star = star;
    }

    public void setFans(Fans fans) {
        this.fans = fans;
    }

    public void setCompany(Company company) {
        this.company = company;
    }

    public void meeting(a) {
        System.out.println(fans.getName() + "With the Stars" + star.getName() + "见面了。");
    }

    public void business(a) {
        System.out.println(company.getName() + "With the Stars" + star.getName() + "Negotiate business."); }}Copy the code

Principle of composite reuse

The principle of composite reuse is to use association relations such as composition or aggregation first, and then consider inheritance relations to achieve.

Generally, class reuse can be divided into inheritance reuse and composite reuse.

Although inheritance reuse has the advantage of being simple and easy to implement, it also has the following disadvantages:

  1. Inheritance reuse breaks class encapsulation. Because inheritance exposes the implementation details of the parent class to the child class, the parent class is transparent to the child class, so this reuse is also known as “white box” reuse.
  2. A subclass has a high degree of coupling with its parent. Any change in the implementation of the parent class results in a change in the implementation of the subclass, which is not conducive to the extension and maintenance of the class.
  3. It limits the flexibility of reuse. An implementation inherited from a parent class is static and defined at compile time, so it cannot be changed at run time.

In combination or aggregate reuse, existing objects can be incorporated into the new object and become a part of the new object. The new object can invoke the functions of the existing object, which has the following advantages:

  1. It maintains class encapsulation. Because the internal details of component objects are invisible to the new object, this reuse is also known as “black box” reuse.
  2. Low coupling between objects. You can declare abstractions at the member locations of a class.
  3. High flexibility of reuse. This reuse can occur dynamically at run time, with new objects dynamically referencing objects of the same type as component objects.

Let’s look at an example to understand the composite reuse principle

An automobile classification management program

Automobiles can be divided into gasoline automobiles and electric automobiles according to “power source”. According to the “color” can be divided into white cars, black cars and red cars. If you consider both categories, the combinations are numerous. The class diagram is as follows:

As you can see from the class diagram above, there are many subclasses using inheritance reuse. If there are new power sources or new colors, new classes need to be defined. Let’s try to change inheritance reuse to aggregate reuse and let’s see.