1. Overview of design patterns

1.1 Background of software design pattern

Design patterns did not originally appear in software design, but were used in architectural design

1.2 The concept of software design patterns

Software Design Pattern, also known as Design Pattern, is a set of repeatedly used, most people know, code Design experience summary. He describes some recurring problems in the software design process and the solutions to these problems. That is to say, it is a series of routines to solve a specific problem. It is a summary of the code design experience of predecessors. It has a certain universality and can be used repeatedly.

1.3 The necessity of learning design patterns

The essence of design pattern is the practical application of object-oriented design principles, and it is a full understanding of the encapsulation, inheritance and polymorphism of classes, as well as the association and combination of classes.

Using design patterns correctly has the following advantages:

  • It can improve the program’s thinking ability, programming ability and design ability
  • Is the program design more standardized, code writing more engineering, is the software development efficiency greatly improved, thus shortening the software development cycle
  • The designed code is reusable, readable, reliable, flexible and maintainable

1.4 Classification of design patterns

1.4.1 Create Mode

Used to describe “how to create objects”, it is characterized by “separating the creation and use of objects”, including: simple profit, prototype, factory method, abstract factory, builder, etc. 5 creation patterns.

1.4.2 Structural mode

Used to describe how classes or objects are grouped into a larger structure based on a certain layout, including seven structural patterns: broker, adapter, bridge, decorator, facade, share, and composition.

1.4.3 Behavioral patterns

Describes how classes or objects work together to accomplish tasks that a single object cannot accomplish alone, and how responsibilities are assigned, including: template methods, policies, commands, responsibility chains, states, observers, mediators, iterators, visitors, memos, interpreters, and other 11 behavioral patterns

2. UML diagrams

Unified Modeling Language (UML) is a visual Modeling Language used for software design. It is simple, Unified, graphical, and can express dynamic and static information in software design.

UML defines nine diagrams including use case diagram, class diagram, object diagram, state diagram, activity diagram, sequence diagram, collaboration diagram, component diagram and deployment diagram from different points of view of target system.

2.1 Overview of class diagrams

The Class diagram shows the static structure of the model, especially the existing classes in the model, the internal structure of the classes and their relationship with other classes. Class diagrams, which do not display transient information, are a major part of object-oriented modeling.

2.2 Functions of class diagrams

  • In software engineering, class diagram is a kind of static structure diagram, which describes the collection of classes of the system, the properties of classes and the relations between classes, which can simplify people’s understanding of the system
  • Class diagram is an important product of system analysis and design stage and an important model of system coding and testing

2.3 Class diagram representation

2.3.1 Representation of classes

In UML class diagrams, classes are represented by delimited rectangles that contain the class name, attribute (field), and method (method). For example, the following figure shows an Employee class that contains the name, age, and Address attributes, as well as the work() method

The plus and minus signs before a property/method name indicate the visibility of the property/method. There are three notations for visibility in UML class diagrams:

  • + :public
  • – :private
  • # :protected

The full representation of an attribute is: Visibility Name: type [= default]

The full representation of the method is: visibility name (argument list) [: return type]

Note:

1. The content in parentheses indicates that it is optional

2, there’s also putting the type in front of the variable name, your return value type in front of the anti-hit method name

2.3.2 Representation of relationships between classes
2.3.2.1 Association Relationship

An association relationship is a reference relationship between objects. It is used to represent the relationship between one type of object and another type of object, such as teacher and student, teacher and apprentice, husband and wife, etc. Association relation is the most commonly used relation between classes, which can be divided into general relation, aggregate relation and combinatorial relation.

Association can be divided into one-way association, bidirectional association and self-association.

  • One-way link

One-way associations are represented by an implementation with an arrow in a UML class diagram. The figure above shows that each Customer has an Address, which is achieved by having the Customer class hold a member class of type Address

  • Bidirectional association

As you can see from the figure above, a bidirectional association is one in which each party holds a member variable of the other’s type.

In UML class diagrams, bidirectional associations are represented by a straight line without arrows. The Customer class in the figure above maintains a List , indicating that a Customer can buy more than one item; A member variable of type Customer is maintained in the Product class to indicate which Customer purchased the Product.

  • Since the correlation

Autocorrelation is represented in a UML class diagram as a line with an arrow pointing at itself. The Node class contains member variables of type Node.

2.3.2.2 Aggregation relationship

Aggregation relation is a kind of strong correlation relation, which is the relation between whole and part.

Aggregation relationships are also implemented through member objects, which are part of the overall object, but can exist independently of the overall object. For example, the relationship between the school and the teacher, the school includes the teacher, but if the school closes, the teacher still exists.

In UML class diagrams, aggregation relationships can be represented by solid lines with hollow diamonds that point to the whole. The diagram below shows the relationship between universities and faculty:

2.3.2.3 Combination relation

Composition represents a whole-part relationship between classes, but it is a stronger aggregation relationship.

In combinatorial relation, the whole object can control the life cycle of some objects. Once the whole object does not exist, some objects will not exist, and some objects cannot exist without the whole object. For example, the relationship between the head and the mouth, without the head, the mouth would not exist.

In UML class diagrams, composition relationships are represented by solid lines with solid diamonds that point to the whole. Here is a head and mouth diagram:

2.3.2.4 Dependency Relationships

Dependency is a kind of use relation, it is the weakest coupling degree between objects of a kind of association, is a temporary association. In code, a method of one class performs some responsibility by accessing some method in another (dependent class) through local variables, method arguments, or calls to static methods.

In UML class diagrams, dependencies are represented by dotted lines with arrows that point from the using class to the dependent class. The diagram below shows the relationship between the driver and the car. The driver drives the car:

2.3.2.5 Inheritance Relationship

Inheritance relation is a relation with the largest degree of coupling between objects. It represents the relation between general and special. It is the relation between parent class and subclass, and it is an inheritance relation.

In UML class diagrams, generalization relationships are represented by solid lines with hollow triangular arrows that point from subclasses to parent classes. When the code is implemented, the object-oriented inheritance mechanism is used to realize the generalization relationship. For example, the Student and Teacher classes are subclasses of the Person class, and the class diagram looks like this:

2.3.2.6 Implementing relationships

An implementation relationship is a relationship between an interface and an implementation class. In this relationship, the class implements the interface, and the operations in the class implement all the abstract operations declared in the interface.

In UML class diagrams, implementation relationships are represented by dashed lines with hollow triangular arrows that point from the implementation class to the interface. For example, cars and boats implement transportation, and their class diagram is shown below:

3. Software design principles

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.

3.1 Open and close 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 preferences, or download a new skin from the Internet. These skins have common characteristics, and you can define an abstract class (AbstractSkin) for them, with each specific skin (DefaultSpecificSkin and HeimaSpecificSkin) as a subclass. The user form can select or add new themes as needed without changing the original code, so it is open and closed.

3.2 Richter’s substitution principle

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

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.

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 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

A Square (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 an ordinary rectangle as a parameter to the resize method, we will see the rectangle gradually grow in width. When the width is greater than the length, the code will stop. This behavior is as expected. If we pass in another square as an argument to the resize method, we see that the width and length of the square 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.

3.3 Dependency 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.

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 of CPU, such as Intel, AMD, etc., hard disk can choose Seagate, Western digital, etc., memory can choose Kingston, Pirate Ship, etc.

The class 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

A Computer (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

The test class (TestComputer) :

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 class diagram is as follows:

A Computer (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.

3.4 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 dark horse brand safety door, the safety door with fire, waterproof, anti-theft functions. 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 Dark Horse brand has the functions of anti-theft, waterproof and fire prevention. Now if we also need to create a brand of safety door, and the safety 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 (interface) :

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

HeiMaSafetyDoor (class) :

public class HeiMaSafetyDoor 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

ItcastSafetyDoor (class) :

public class ItcastSafetyDoor implements AntiTheft.Fireproof {
    public void antiTheft(a) {
        System.out.println("Security"); }
    public void fireproof(a) { System.out.println("Fire"); }}Copy the code

3.5 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.

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 class diagram is as follows:

The code is as follows:

Star (Star)

public class Star {
    private String name;
    public Star(String name) {
        this.name=name;
    }
    public String getName(a) {
        returnname; }}Copy the code

Fans (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() + "Yes.");
    }
    public void business(a) {
        System.out.println(company.getName() + "With the Stars" + star.getName() + "Negotiate business."); }}Copy the code

3.6 Principles 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:

  • 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.
  • 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.
  • 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:

  • 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.
  • Low coupling between objects. You can declare abstractions at the member locations of a class.
  • 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.