[Design Patterns and Paradigms: Summary] 74 | Summarize and review the principles, ideas behind, application scenarios, etc. of 23 classic design patterns

As of today, all 23 classic design patterns have been explained. Our entire column has also completed 3/4, and we are about to enter the actual combat stage. Before entering the study of the new module, I will take you to do a summary review as usual. The 23 classic design patterns are divided into 3 types, which are creational, structural, and behavioral. Today, we divide these three types into three corresponding small modules, and take you to review the principle, implementation, design intent and application scenarios of each design pattern one by one.

Like the previous summary article, today's content is quite a lot, with nearly 10,000 words, but we have learned it before. It seems that it should not be too strenuous, but it can test whether you have really mastered the content.

Still the same sentence, if you feel impressed after reading it, it means that you have learned well; if you can form your own knowledge structure in your mind, and you can recall it when you close your eyes, it means that you have learned well. Good; if you can have your own understanding, and in project development, start thinking about code quality issues, and start using the design patterns you have learned to solve code problems, it means that you have mastered the essence of these contents.

insert image description here
Without further ado, let's officially start today's review!

1. Creational Design Patterns

Creational design patterns include: singleton pattern, factory pattern, builder pattern, and prototype pattern. It mainly solves the problem of object creation, encapsulates the complex creation process, and decouples the creation code and usage code of the object.

1. Singleton mode

The singleton pattern is used to create globally unique objects. A class is only allowed to create one object (or instance), then this class is a singleton class, and this design pattern is called the singleton pattern. There are several classic implementation methods of singleton, they are: hungry man style, lazy man style, double detection, static inner class, enumeration.
Although singleton is a very common design pattern, in actual development, we do often use it, but some people think that singleton is an anti-pattern (anti-pattern), and it is not recommended to use, the main The reasons are as follows:

  • Singleton is not friendly to support OOP features
  • Singletons hide dependencies between classes
  • Singletons are not friendly to code scalability
  • Singletons are not friendly to code testability
  • Singletons do not support parameterized constructors

So what's the alternative to a singleton solution? If we want to completely solve these problems, we may have to find other ways to implement globally unique classes from the root. For example, global uniqueness is guaranteed through factory patterns and IOC containers.

Some people regard singleton as an anti-pattern and advocate that it should not be used in projects. Personally, I find this a bit extreme. There is no right or wrong pattern, the key depends on how you use it. If the singleton class does not have the need for subsequent expansion and does not depend on external systems, then there is no big problem in designing it as a singleton class. For some global classes, if we create new in other places, we have to pass them between classes. It is better to directly make singleton classes, which are simple and convenient to use.

In addition, we also talked about extended knowledge points such as the only single case of the process, the only single case of the thread, the only single case of the cluster, and multiple cases. This part will not be used in actual development, but it can expand your Ideas, exercise your logical thinking. I won't take you back here, you can recall it yourself.

2. Factory pattern

The factory pattern includes three subdivision patterns: simple factory, factory method, and abstract factory. Among them, simple factories and factory methods are more commonly used, and the application scenarios of abstract factories are relatively special, so they are rarely used and are not the focus of our study.

The factory pattern is used to create different but related types of objects (a group of subclasses that inherit the same parent class or interface), and the given parameters determine which type of object to create. In fact, if the logic of creating objects is not complicated, then we can create objects directly through new, without using the factory pattern. When the creation logic is more complex and is a "big project", we consider using the factory pattern to encapsulate the object creation process and separate the creation and use of the object.

When the creation logic of each object is relatively simple, I recommend using the simple factory pattern to put the creation logic of multiple objects into a factory class. When the creation logic of each object is relatively complex, in order to avoid designing an overly large factory class, we recommend using the factory method pattern to split the creation logic into finer details, and the creation logic of each object is independent of its own factory in class.
In detail, the factory model has the following four functions, which are also the most essential reference standards for judging whether to use the factory model.

  • Encapsulation changes: The creation logic may change. After being encapsulated into a factory class, the change of the creation logic is transparent to the caller.
  • Code reuse: Create code that can be reused after it is extracted to an independent factory class.
  • Isolate complexity: Encapsulate complex creation logic, and the caller does not need to understand how to create objects.
  • Control complexity: Extract the creation code, so that the original function or class has a single responsibility and the code is more concise.

In addition, we also talked about a very classic application scenario of the factory pattern: dependency injection frameworks, such as Spring IOC, Google Guice, which are used to centrally create, assemble, and manage objects, decoupled from specific business codes, and let programmers Focus on the development of business code. The DI framework has become a necessary framework for our daily development. In the column, I also took you to implement a simple DI framework, you can go back and have a look.

3. Builder mode

The builder mode is used to create complex objects, which can be "customized" to create different objects by setting different optional parameters. The principle and implementation of the builder mode are relatively simple, and the focus is on mastering the application scenarios and avoiding excessive use.

If there are many attributes in a class, in order to avoid too long parameter list of the constructor, which affects the readability and ease of use of the code, we can use the constructor to cooperate with the set() method to solve the problem. However, if any of the following situations exist, we should consider using the builder pattern.

  • We put the required properties of the class in the constructor, forcing them to be set when the object is created. If there are many required attributes, if these required attributes are set in the constructor, then the constructor will have a long parameter list. If we set the required attributes through the set() method, then the logic to verify whether these required attributes have been filled has nowhere to be placed.

  • If there are certain dependencies or constraints between the attributes of the class, we continue to use the constructor with the design idea of ​​the set() method, and then there is nowhere to put the verification logic of these dependencies or constraints.

  • If we want to create an immutable object, that is, after the object is created, the internal attribute values ​​​​can no longer be modified. To achieve this function, we cannot expose the set() method in the class. The way that the constructor cooperates with the set() method to set the property value is not applicable.

4. Prototype pattern

If the creation cost of the object is relatively high, and there is little difference between different objects of the same class (most fields are the same), in this case, we can use the copy (or copy) of the existing object (prototype) ) way to create new objects to achieve the purpose of saving creation time. This method of creating objects based on prototypes is called the prototype mode.

There are two ways to implement the prototype mode, deep copy and shallow copy. Shallow copy will only copy the basic data type data in the object and the memory address of the referenced object, and will not recursively copy the referenced object and the referenced object of the referenced object... while the deep copy gets a completely independent object. Therefore, deep copying is more time-consuming and memory-consuming than shallow copying.

If the object to be copied is an immutable object, it is no problem to share the immutable object with shallow copy, but for mutable objects, the object obtained by shallow copy and the original object will share some data, and the data may be modified Risks are more complex. Unless the operation is very time-consuming, it is recommended to use a shallow copy, otherwise, there is no good reason not to use a shallow copy for a small performance improvement.

2. Structural design pattern

Structural patterns mainly summarize the classic structures in which some classes or objects are combined, and these classic structures can solve problems in specific application scenarios. Structural patterns include: proxy pattern, bridge pattern, decorator pattern, adapter pattern, facade pattern, combination pattern, flyweight pattern.

1. Proxy mode

The proxy mode defines a proxy class for the original class without changing the interface of the original class. The main purpose is to control access, not to enhance the function. This is the biggest difference from the decorator mode. In general, we let the proxy class and the original class implement the same interface. However, if the original class does not define an interface, and the original class code is not developed and maintained by us. In this case, we can implement the proxy pattern by letting the proxy class inherit the methods of the original class.

Static proxies need to create a proxy class for each class, and the code in each proxy class is a bit like template-style "repeated" code, which increases maintenance and development costs. For the problems of static proxy, we can solve it through dynamic proxy. Instead of writing a proxy class for each original class in advance, we dynamically create the proxy class corresponding to the original class at runtime, and then replace the original class with the proxy class in the system.

Proxy mode is often used to develop some non-functional requirements in business systems, such as: monitoring, statistics, authentication, current limiting, transactions, idempotence, and logs. We decouple these additional functions from business functions and put them in the agent class for unified processing, so that programmers only need to focus on business development. In addition, the proxy mode can also be used in application scenarios such as RPC and caching.

2. Bridge mode

The code implementation of the bridge mode is very simple, but it is a little difficult to understand, and the application scenarios are relatively limited. Therefore, relatively speaking, the bridge mode is not so commonly used in actual projects. You only need to understand it briefly, and you can understand it when you see it. That's fine, it's not the focus of our study.

There are two ways to understand the bridge mode. The first way of understanding is "decoupling abstraction and implementation so that they can be developed independently". This way of understanding is quite special, and there are not many application scenarios. Another way of understanding is simpler, which is equivalent to the design principle of "composition is better than inheritance". This way of understanding is more general and has more application scenarios. Regardless of the way of understanding, their code structure is the same, which is a combination relationship between classes.

For the first way of understanding, understanding the two concepts of "abstract" and "implementation" in the definition is the key to understanding it. The "abstract" in the definition does not refer to "abstract class" or "interface", but a set of abstracted "class library", which only contains the skeleton code, and the real business logic needs to be delegated to the "implementation" in the definition "To be done. The "implementation" in the definition is not an "interface implementation class", but a set of independent "class libraries". "Abstract" and "implementation" are developed independently and assembled through the composition relationship between objects.

3. Decorator pattern

The decorator pattern mainly solves the problem that the inheritance relationship is too complicated, replaces the inheritance by combination, and adds enhanced functions to the original class. This is also an important basis for judging whether to use the decorator pattern. In addition, the decorator pattern has another feature, that is, multiple decorators can be nested on the original class. In order to meet such requirements, when designing, the decorator class needs to inherit the same abstract class or interface as the original class.

4. Adapter mode

The proxy mode and the decorator mode provide the same interface as the original class, while the adapter provides a different interface from the original class. Adapter mode is used for adaptation, which converts incompatible interfaces into compatible interfaces, so that classes that could not work together due to incompatible interfaces can work together. There are two implementations of the adapter pattern: class adapters and object adapters. Among them, class adapters are implemented using inheritance relationships, and object adapters are implemented using composition relationships.

The adapter pattern is an afterthought remedial strategy used to remedy design flaws. Applying this model is considered a "helpless move". If we can avoid the problem of interface incompatibility at the early stage of design, then this mode will be useless. In actual development, under what circumstances will interface incompatibility occur? I have summarized the following 5 scenarios:

  • Encapsulate flawed interface design
  • Unify the interface design of multiple classes
  • Replace dependent external systems
  • Compatible with old version interface
  • Adapt to data in different formats

5. Facade pattern

The principle and implementation of the facade mode are very simple, and the application scenarios are relatively clear. It encapsulates fine-grained interfaces and provides a high-level interface that combines each fine-grained interface to improve the usability of the interface or solve problems such as performance and distributed transactions.

6. Combination mode

The composition mode is completely different from the "composition relationship (to assemble two classes through composition)" in the object-oriented design we talked about before. The "combination mode" mentioned here is mainly used to process tree-structured data. Because of the particularity of its application scenarios, the data must be represented in a tree structure, which also makes this mode not so commonly used in actual project development. However, once the data satisfies the tree structure, applying this pattern can play a big role and make the code very concise.

The design idea of ​​the combination mode is not so much a design mode as an abstraction of data structures and algorithms for business scenarios. Among them, data can be expressed as a data structure such as a tree, and business requirements can be realized through a recursive traversal algorithm on the tree. Combination mode organizes a group of objects into a tree structure, and treats both individual objects and composite objects as nodes in the tree to unify the processing logic, and it uses the characteristics of the tree structure to process each subtree recursively, simplifying in turn Code.

7. Flyweight mode

The so-called "flying yuan", as the name suggests, is a shared unit. The purpose of the flyweight pattern is to reuse objects and save memory, provided that the flyweight object is an immutable object.

Specifically, when there are a large number of duplicate objects in a system, we can use the flyweight pattern to design objects as flyweights, and only keep one instance in memory for multiple code references, which can reduce memory The number of objects to save memory. In fact, not only the same objects can be designed as flyweights, but for similar objects, we can also extract the same parts (fields) from these objects and design them as flyweights, so that these large numbers of similar objects can refer to these flyweights.

3. Behavioral Design Patterns

We know that the creational design pattern mainly solves the problem of "creation of objects", the structural design pattern mainly solves the problem of "combination of classes or objects", and the behavioral design pattern mainly solves the problem of "interaction between classes or objects" . There are many behavioral patterns, there are 11 kinds, they are: observer pattern, template pattern, strategy pattern, responsibility chain pattern, iterator pattern, state pattern, visitor pattern, memo pattern, command pattern, interpreter pattern, intermediary model.

1. Observer pattern

Observer pattern decouples observer and observed code. The observer mode has a wide range of application scenarios, ranging from decoupling at the code level to system decoupling at the architecture level, or some product design ideas, all of which have shadows of this mode, such as email subscriptions, RSS Feeds, Essentially the observer pattern.

Under different application scenarios and requirements, this mode also has completely different implementation methods: there are synchronous blocking implementation methods, and there are also asynchronous non-blocking implementation methods; there are intra-process implementation methods and cross-process implementation methods. Synchronous blocking is the most classic implementation method, mainly for code decoupling; asynchronous non-blocking can not only achieve code decoupling, but also improve code execution efficiency; observer mode decoupling between processes is more thorough, generally based on Message queue is used to realize the interaction between the observed and the observer between different processes.

The role of the framework is to hide implementation details, reduce development difficulty, realize code reuse, decouple business and non-business code, and let programmers focus on business development. For the asynchronous non-blocking observer mode, we can also abstract it into the EventBus framework to achieve this effect. EventBus translates as "event bus", which provides the skeleton code to implement the observer pattern. Based on this framework, we can easily implement the Observer pattern in our own business scenarios without developing from scratch.

2. Template mode

The template method pattern defines an algorithm skeleton in a method and defers certain steps to subclass implementation. The template method pattern allows subclasses to redefine certain steps in an algorithm without changing the overall structure of the algorithm. The "algorithm" here can be understood as "business logic" in a broad sense, and does not specifically refer to the "algorithm" in data structures and algorithms. The algorithm skeleton here is the "template", and the method containing the algorithm skeleton is the "template method", which is also the origin of the name of the template method pattern.

Template mode has two functions: reuse and extension. Among them, reuse means that all subclasses can reuse the code of the template method provided in the parent class. Extension means that the framework provides functional extension points through the template mode, allowing framework users to customize the functions of the framework based on the extension points without modifying the source code of the framework.

Besides that, we also talked about callbacks. It has the same role as the template pattern: code reuse and extension. It is often used in the design of some frameworks, class libraries, components, etc. For example, JdbcTemplate uses callbacks.

Compared with ordinary function calls, callbacks are a two-way calling relationship. Class A registers a certain function F to Class B in advance. When Class A calls the P function of Class B, Class B in turn calls the F function registered to it by Class A. The F function here is the "callback function". A calls B, and B calls A in turn. This calling mechanism is called a "callback".

Callbacks can be subdivided into synchronous callbacks and asynchronous callbacks. From the perspective of application scenarios, synchronous callbacks look more like template mode, and asynchronous callbacks look more like observer mode. The difference between callback and template mode is more in code implementation than in application scenarios. The callback is implemented based on the composition relationship, and the template mode is implemented based on the inheritance relationship. Callbacks are more flexible than template patterns.

3. Strategy pattern

The strategy pattern defines a family of algorithm classes, and encapsulates each algorithm separately so that they can replace each other. The Strategy pattern can make changes to algorithms independent of the clients that use them (clients here refer to the code that uses the algorithms). The strategy pattern is used to decouple the definition, creation, and use of strategies. In fact, a complete strategy pattern is composed of these three parts.

The definition of a strategy class is relatively simple, including a strategy interface and a group of strategy classes that implement this interface. The creation of policies is done by the factory class, which encapsulates the details of policy creation. The strategy mode contains a set of optional strategies. The client code chooses which strategy to use. There are two methods of determination: static determination at compile time and dynamic determination at runtime. Among them, "dynamic determination at runtime" is the most typical application scenario of the strategy pattern.
In actual project development, the strategy pattern is also commonly used. The most common application scenario is to use it to avoid lengthy if-else or switch branch judgments. However, it does more than that. It can also provide framework extension points, etc., like the template mode. In fact, the main role of the strategy pattern is to decouple the definition, creation and use of strategies, and to control the complexity of the code, so that each part will not be too complicated and the amount of code is too much. In addition, for complex code, the strategy pattern can also satisfy the open-close principle. When adding a new strategy, it minimizes and centralizes code changes and reduces the risk of introducing bugs.

4. Chain of Responsibility Model

In the Chain of Responsibility pattern, multiple processors process the same request sequentially. A request is first processed by processor A, and then passed to processor B, after processing by processor B, it is passed to processor C, and so on, forming a chain. Each processor in the chain assumes its own processing responsibilities, so it is called the responsibility chain mode.

In the definition of GoF, once a processor can handle the request, it will not continue to pass the request to subsequent processors. Of course, in actual development, there are also variants of this mode, that is, the request will not be terminated midway, but will be processed by all processors.

The responsibility chain mode is often used in framework development to implement filter and interceptor functions, allowing framework users to add new filtering and interception functions without modifying the framework source code. This also reflects the design principle of being open to extension and closed to modification mentioned earlier.

5. Iterator pattern

Iterator mode is also called cursor mode, which is used to traverse collection objects. The "collection object" mentioned here can also be called "container" and "aggregate object". In fact, it is an object that contains a group of objects, such as arrays, linked lists, trees, graphs, and jump lists. The main role of the iterator pattern is to decouple container code and traversal code. Most programming languages ​​provide ready-made iterators that can be used, and we don't need to develop from scratch.
There are generally three ways to traverse a collection: for loop, foreach loop, and iterator traversal. The latter two are essentially one kind, and both can be regarded as iterator traversal. Compared with for loop traversal, using iterators to traverse has three advantages:

  • The iterator mode encapsulates the complex data structure inside the collection. Developers don't need to know how to traverse, just use the iterator provided by the container;
  • The iterator mode separates the traversal operation of the collection object from the collection class and puts it into the iterator class, making the responsibilities of the two more single;
  • The iterator pattern makes it easier to add new traversal algorithms, more in line with the open-closed principle. In addition, since the iterators are all implemented from the same interface, it is easier to replace iterators during development based on the interface rather than the implementation of the program.

While traversing the collection elements through the iterator, adding or deleting elements in the collection may cause an element to be traversed repeatedly or not traversed. In response to this problem, there are two relatively straightforward solutions to avoid such unpredictable operating results. One is that elements are not allowed to be added or deleted during traversal, and the other is that traversal reports an error after adding or deleting elements. The first solution is more difficult to implement, because it is difficult to determine when the use of the iterator ends. The second solution is more reasonable, which is the solution adopted by the Java language. After adding and deleting elements, we choose the fail-fast solution, so that the traversal operation directly throws a runtime exception.

6. State mode

State patterns are generally used to implement state machines, and state machines are often used in system development such as games and workflow engines. A state machine is also called a finite state machine, which consists of three parts: state, event, and action. Among them, an event is also called a transition condition. Events trigger the transition of states and the execution of actions. However, the action is not necessary, and it is possible to just transfer the state without performing any action.

For the state machine, we summarize three implementation methods.
The first implementation is called the branching logic method. Use if-else or switch-case branch logic, refer to the state transition diagram, and literally translate each state transition into code as it is. For simple state machines, this implementation is the simplest and most straightforward, and is the first choice.

The second implementation is called look-up table method. For a state machine with many states and complex state transitions, the look-up table method is more suitable. Representing the state transition diagram through a two-dimensional array can greatly improve the readability and maintainability of the code.

The third way to achieve it is to use the state pattern. For a state machine with few states and relatively simple state transition, but the business logic contained in the action triggered by the event may be more complex, we prefer this implementation method.

7. Visitor pattern

The visitor pattern allows one or more operations to be applied to a set of objects. The design intention is to decouple the operations and the objects themselves, keep the class with a single responsibility, satisfy the principle of opening and closing, and deal with the complexity of the code.
For the visitor mode, the main difficulty of learning lies in the code implementation. The main reason why the code implementation is more complicated is that function overloading is statically bound in most object-oriented programming languages. That is to say, which overloaded function of the class to call is determined by the declared type of the parameter during compilation, not by the actual type of the parameter at runtime. In addition, we also talked about Double Disptach. If a language supports Double Dispatch, there is no need for the Visitor pattern.
It is precisely because the code implementation is difficult to understand, so applying this pattern in the project will lead to poor readability of the code. If your colleagues don't understand this design pattern, they may not be able to read and maintain the code you write. So, don't use this mode unless you have to.

8. Memo mode

Memento mode is also called snapshot mode. Specifically, it captures the internal state of an object without violating the principle of encapsulation, and saves this state outside the object, so that the object can be restored to its previous state later. The definition of this pattern expresses two parts: one part is to store copies for later recovery; the other part is to perform object backup and recovery without violating the principle of encapsulation.
The application scenarios of the memo mode are also relatively clear and limited, mainly used for loss prevention, revocation, recovery, etc. It is very similar to what we usually call "backup". The main difference between the two is that the memo pattern is more focused on the design and implementation of the code, and the backup is more focused on the architecture design or product design.
For the backup of large objects, the storage space occupied by the backup will be relatively large, and the time for backup and restoration will be relatively long. In response to this problem, different business scenarios have different processing methods. For example, only back up the necessary recovery information, combined with the latest data for recovery; another example, combine full backup and incremental backup, low-frequency full backup, high-frequency incremental backup, and combine the two for recovery.

9. Command mode

The command mode is not commonly used in daily work, you just need to understand it a little bit.
When it comes to coding implementation, the core implementation method used in the command mode is to encapsulate functions into objects. We know that in most programming languages, functions cannot be passed as parameters to other functions, nor can they be assigned to variables. With the command pattern, we encapsulate functions into objects, so that functions can be used like objects.
The main function and application scenarios of the command mode are to control the execution of commands, such as asynchronous, delay, queue execution commands, undo and redo commands, store commands, record logs for commands, etc. This is the unique role of the command mode. The place.

10. Interpreter mode

An interpreter pattern defines a grammatical (or grammar) representation for a language and an interpreter for processing that grammar. In fact, the "language" here does not only refer to the Chinese, English, Japanese, French and other languages ​​that we usually speak. In a broad sense, as long as it is a carrier that can carry information, we can call it "language", for example, ancient knotting notes, Braille, dumb language, Morse code, etc.

In order to understand the information expressed by the "language", we must define the corresponding grammatical rules. In this way, writers can write "sentences" according to grammatical rules (the professional name should be "expressions"), and readers can read "sentences" according to grammatical rules, so that information can be transmitted correctly. The interpreter mode we are going to talk about is actually an interpreter used to interpret "sentences" according to grammatical rules.

The code implementation of the interpreter mode is more flexible, and there is no fixed template. As we said earlier, the application design pattern is mainly to deal with the complexity of the code, and the interpreter pattern is no exception. The core idea of ​​its code implementation is to split the work of grammar analysis into various small classes, so as to avoid large and comprehensive analysis classes. The general approach is to split the grammatical rules into some small independent units, then parse each unit, and finally merge them into the parsing of the whole grammatical rules.

11. Intermediary pattern

The design idea of ​​the intermediary pattern is very similar to that of the middle layer. By introducing the middle layer of the intermediary, the interaction relationship (or dependency relationship) between a group of objects is converted from many-to-many (network relationship) to one-to-many (star relationship). relation). Originally, an object needs to interact with n objects, but now it only needs to interact with one intermediary object, thereby minimizing the interaction between objects, reducing the complexity of the code, and improving the readability and maintainability of the code.

Both the observer pattern and the intermediary pattern are to realize the decoupling between participants and simplify the interaction relationship. The difference between the two lies in the application scenarios. In the application scenario of the observer pattern, the interaction between participants is relatively organized and generally one-way. A participant has only one identity, either the observer or the observed. In the application scenario of the intermediary model, the interaction relationship between participants is intricate, and they can be both the sender of the message and the receiver of the message at the same time.

class disscussion

After finally learning these 23 design patterns, what questions do you have about these design patterns? You can talk about it in the message area.

Guess you like

Origin blog.csdn.net/qq_32907491/article/details/131275536