This is the second day of my participation in the More text Challenge. For more details, see more text Challenge

preface

This article is the first in the series on design patterns, which is the basis for learning design patterns. In this article, you will learn an overview of design patterns, uml fundamentals, and several important software design principles.

I. Overview of design patterns

1.1 Background of software design patterns

“Design patterns” were not originally used in software design, but were used in architectural design.

In 1977, Christopher Alexander, a famous American architect and director of the Center for Environmental Structure at the University of California, Berkeley, wrote in his book Architectural Pattern Language: Town, Building, Structure describes some common architectural design problems and proposes 253 basic patterns for the design of towns, neighborhoods, houses, gardens, and rooms.

In 1990, software engineering began to discuss the topic of design patterns, and later held many seminars on design patterns. Until 1995, Eric Gamma, Richard Helm, Ralph Johnson, John Vlissides and other four authors published design Patterns: Fundamentals of Reusable Object-oriented Software, in which 23 design patterns are included, is a landmark event in the design patterns field, leading to a breakthrough in software design patterns. The Four authors are also known in the software development world as the “Gang of Four” (GoF).

1.2 Concept of software design patterns

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

1.3 The necessity of learning design patterns

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

Proper use of design patterns has the following advantages.

  • Can improve the programmer’s thinking ability, programming ability and design ability.
  • Make the program design more standardized, code compilation more engineering, so that the efficiency of software development is greatly improved, so as to shorten the software development cycle.
  • The design of code is highly reusable, readable, reliable, flexible and maintainable.

1.4 Classification of design patterns

  • Creation pattern

    Used to describe how objects are created. Its main feature is to “separate the creation of objects from their use.” The GoF book provides five creation patterns: singletons, prototypes, factory methods, abstract factories, and builders.

  • Structural mode

    To describe how classes or objects can be grouped into a larger structure in a certain layout, the GoF book provides seven structural patterns: agent, adapter, bridge, decoration, facade, enjoy element, and composition.

  • Behavioral pattern

    Used to describe how classes or objects work together to accomplish tasks that cannot be done by a single object alone, and how responsibilities are assigned. The GoF book provides 11 behavioral patterns, including template methods, policies, commands, chains of responsibility, states, observers, intermediaries, iterators, visitors, memos, and interpreters.

Second, the UML diagrams

Unified Modeling Language (UML) is a visual Modeling Language used to design software. It is characterized by simplicity, unity, graphics, and can express dynamic and static information in software design.

UML defines nine kinds of diagrams, including use case diagram, class diagram, object diagram, state diagram, activity diagram, timing diagram, collaboration diagram, component diagram and deployment diagram, from different angles of target system.

2.1 Overview of class Diagrams

A Class diagram shows the static structure of a model, especially the classes in the model, their internal structures, and their relationships with other classes. Class diagrams do not display temporary information. Class diagrams are the main component of object-oriented modeling.

2.2 Functions of class diagrams

  • In software engineering, class diagram is a static structure diagram, which describes the collection of classes, the properties of classes and the relationship between classes, and can simplify people’s understanding of the system.
  • Class diagram is an important product of system analysis and design, 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 rectangles with dividing lines that contain the class name, property (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 the property/method name indicate the visibility of the property/method. There are three symbols for visibility in UML class diagrams:

  • + : indicates public

  • – : indicates private

  • #:表示protected

The full representation of a property is: Visibility name: type [= default value]

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

Note:

1, the content in brackets is optional

2, it is also possible to place the type before the variable name and the return value type before the method name

Here’s an example:

The Demo class in the figure above defines three methods:

  • Method () : modifiers are public, no arguments, and no return value.
  • Method1 () method: modifiers are private, no arguments, and returns a value of type String.
  • Method2 () : The modifier is protected. It takes two arguments, the first of type int, the second of type String, and returns a value of type int.
2.3.2 Representation of relationships between classes
A. Association

Association relationship is a reference relationship between objects, used to represent the relationship between one class of objects and another class of objects, such as teacher and student, master and apprentice, husband and wife, etc. Association relationship is one of the most commonly used relationships between classes, which can be divided into general association relationship, aggregation relationship and combination relationship. Let’s start with general associations.

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

1. Unidirectional association

Unidirectional associations in UML class diagrams are represented by a solid line with an arrow. The figure above shows that each Customer has an Address, which is achieved by having the Customer class hold a member variable class of type Address.

2. Bidirectional correlation

From the figure above, it is easy to see that the so-called bidirectional association is that each party holds member variables of the other type.

In UML class diagrams, bidirectional associations are represented by a straight line without an arrow. In the figure above, a List<Product> is maintained in the Customer class, indicating that a Customer can purchase multiple items; Maintain a member variable of type Customer in the Product class to indicate which Customer purchased the Product.

3. Self-correlation

Self-associations are represented in UML class diagrams by a line with an arrow pointing to itself. The above figure means that the Node class contains member variables of type Node.

B. Aggregation

Aggregation relationship is a kind of association relationship. It is a strong association relationship. It is the relationship between the whole and the parts.

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 is closed, the teacher still exists.

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

C. Combinatorial relation

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

In the combinational relationship, 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 apart from 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, composite relationships are represented by solid lines with solid diamonds pointing to the whole. Here is a diagram of the head and mouth:

D. Dependence

Dependency relationship is a kind of usage relationship, which is the weakest coupling between objects and is a temporary association. In code, methods of one class perform some responsibility by accessing methods of another class (the dependent class) through local variables, method arguments, or calls to static methods.

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

E. Inheritance

Inheritance relationship is a relationship with the greatest coupling degree between objects. It represents the general and special relationship, the relationship between the parent class and the child class, and is an inheritance relationship.

In UML class diagrams, generalization relationships are represented by solid lines with hollow triangular arrows pointing from a child class to a superclass. In the code implementation, the object-oriented inheritance mechanism is used to implement the generalization relationship. For example, the Student and Teacher classes are both subclasses of the Person class, and the class diagram looks like this:

F. Realization relationship

An implementation relationship is the 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 pointing from the implementation class to the interface. For example, cars and boats implement vehicles whose class diagrams are shown in Figure 9.

3. Software design principles

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

3.1 Open and Close principle

Open for extension, closed for modification. When the program needs to be expanded, the original code should not be modified to achieve a hot swap effect. In short, to make the program scalable, easy to maintain and upgrade.

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

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

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

Skin design of sogou input method.

Analysis: The skin of sogou input method is a combination of elements such as the input method background picture, window color and sound. Users can change their skin according to their own preferences, or they can download a new skin from the Internet. These skins have common characteristics, you can define an AbstractSkin (AbstractSkin), and each specific skin (DefaultSpecificSkin and HeimaSpecificSkin) is its subclass. The user form can select or add new themes as needed without modifying the original code, so it meets the open closed principle.

3.2 Richter substitution principle

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

Richter’s substitution principle: wherever a base class can appear, a subclass must appear. A subclass can extend the function of a parent class, but cannot change the original function of the parent class. In other words, when a subclass inherits from a parent class, try not to override the methods of the parent class, except by adding new methods to complete the added functionality.

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

Let’s look at a classic example of the Richter substitution principle

A square is not a rectangle.

In mathematics, a square is unquestionably a rectangle; it is a rectangle whose length and width are equal. So, we developed a software system related to geometric shapes, so that the square can inherit from the rectangle.

The code is as follows:

Rectangle class (Rectangle)

@Data
public class Rectangle {
    private double length;
    private double width;
}
Copy the code

Square:

Since the square has the same length and width, the setLength and setWidth methods need to assign the same value to 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 class RectangleDemo is a component in our software system. It has a resize method that relies on the base Class Rectangle, and the resize method is a method in the RectandleDemo class that implements 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); }}// Print 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 run this code, we will see that if we pass an ordinary rectangle as an argument to the resize method, we will see that the width of the rectangle gradually increases. 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 will see that the width and length of the square continue to grow, and the code will continue to run until an overflow error is generated. So, a normal rectangle is good for this code, and a square is not. We conclude that in the resize method, the Rectangle argument cannot be replaced by the Square argument, which would not yield the expected result. Therefore, the inheritance relationship between the Square class and the Rectangle class violates the Richter substitution principle. The inheritance relationship between them does not hold. A Square is not a Rectangle.

How to improve? At this point we need to redesign their relationship. Abstract a Quadrilateral interface and let the Rectangle and Square classes implement the Quadrilateral interface

3.3 Dependency reversal principle

High-level modules should not depend on low-level modules, both should depend on their abstractions; Abstractions should not depend on details; details should depend on abstractions. Simply put, it requires programming the abstraction, not the implementation, thus reducing the coupling between the client and the implementation module.

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

Assemble the computer

Now to assemble a computer, need accessories CPU, hard disk, memory bar. Only with these configurations can the computer run properly. There are many options to choose CPU, such as Intel, AMD, etc., hard disk can choose Seagate, WestDigital, etc., memory can choose Kingston, Corsair, etc.

The class diagram is as follows:

The code is as follows:

Seagate (XiJieHardDisk) :

public class XiJieHardDisk implements HardDisk {

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

    public String get(a) {
        System.out.println("Using a 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("Use Intel processors"); }}Copy the code

KingstonMemory:

public class KingstonMemory implements Memory {

    public void save(a) {
        System.out.println("Using Kingston as memory"); }}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("Data obtained from the hard drive is:"+ data); }}Copy the code

Test class (TestComputer)

The test class is used to build the computer.

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 above code shows that a computer has been assembled, but it seems that the computer’s CPU can only be Intel, the memory can only be Kingston, and the hard disk can only be Seagate, which is not friendly to users. Users must want to choose their own accessories according to their own preferences.

Improvement based on the principle of dependency reversal:

In the code, we only need to modify the Computer class so that it depends on the abstraction (the interface of each component), rather than on the concrete implementation class of each component.

The class 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 change in the abstraction is very small, so that the user program depends on the abstraction, and the implementation details depend on the abstraction. As long as the abstraction stays the same, the client doesn’t need to change even if the implementation details change. This greatly reduces the coupling of the client with the implementation details.

3.4 Interface Isolation Rules

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 of safety door, the safety door with fire, waterproof, anti-theft function. Can be fire, waterproof, anti-theft functions extracted into an interface, forming a set of specifications. The class diagram is as follows:

We found problems with the above design. The safety door of the 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’s clear that if implementing the SafetyDoor interface violates the interface isolation principle, how do 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 law

Demeter’s law 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 need to communicate directly, then direct calls to each other should not occur and can be forwarded through a third party. The 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 objects, objects created by the current object, and method parameters of the current object. These objects are associated, aggregated, or combined with the current object, and the methods of these objects can be directly accessed.

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

An example of the relationship between a star and his agent

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

The class diagram is as follows:

3.6 Composition Reuse principle

The principle of composite reuse is to use association relationships such as composition or aggregation first and inheritance relationships second.

Generally, class reuse is divided into two types: inheritance reuse and synthesis reuse.

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

  1. Inheritance reuse breaks the encapsulation of a class. Because inheritance exposes the implementation details of the parent class to the child class, and the parent class is transparent to the child class, this reuse is also known as “white box” reuse.
  2. High coupling degree between subclass and superclass. Any change in the implementation of the parent class will result in a change in the implementation of the child class, which is detrimental to the extension and maintenance of the class.
  3. It limits the flexibility of reuse. An implementation inherited from a parent class is static, defined at compile time, so it is not likely to change at run time.

In combination or aggregation reuse, existing objects can be added to a new object and become part of the new object. The new object can call the functions of the existing object. The new object has the following advantages:

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

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

Procedures for classifying and managing automobiles

Automobiles can be divided into gasoline automobiles and electric automobiles according to “power source”; According to the “color” division can be divided into white cars, black cars and red cars, etc. If you consider these two categories together, there are many combinations. The class diagram is as follows:

As you can see from the class diagram above, many subclasses have been created using inheritance reuse, and new classes need to be defined if there is a new power source or new color. Let’s try to change inheritance reuse to aggregation reuse. Let’s see.

3.7 Single responsibility principle

Definition: There should be no more than one cause for class changes. For example, a class has two methods, each of which is responsible for one function. If the responsibilities of method one need to be changed, then the class file needs to be modified, and this change may cause method two, which has been working properly, to fail. For this method of frequent changes in the two responsibilities, it is necessary to establish corresponding Java files for each function to avoid mutual influence when one side changes temporarily and the other side remains unchanged. Generally we have low single-responsibility requirements for classes, but try to keep interfaces and methods as single-responsibility as possible. Write a class or interface method that meets your business requirements, and you don’t need to extend it if you need to. If you do, write another method, class, or interface.

In Java code: a class/interface/method is responsible for only one responsibility.

Advantages: Reduced class complexity, improved class readability, improved system maintainability, and reduced risk from changes.

Four, summary

This article is an introduction to design patterns, and I will begin my journey of design patterns.