Summary of 23 Design Patterns


title: Summary of 23 Design Patterns
date: 2022-12-30 16:53:46
tags:

  • Design pattern
    categories:
  • Design mode
    cover: https://cover.png
    feature: false

Article directory


For detailed knowledge about design patterns, see the following three articles:

This is mainly a summary of the above three articles, which is used to clarify the principles, concepts and application scenarios of the 23 design patterns, as well as the similarities and differences between them, distinguish the 23 design patterns, and have an overall understanding of them. See the above three articles for implementation. It is recommended to read the above three articles or have a certain understanding of design patterns before reading the content of this chapter, and make a summary of the knowledge, or you can click the link at the end of any section of this chapter to go directly to the corresponding detailed knowledge part

1. Creational

The creational design pattern mainly solves the problem of "creation of objects"

1.1 Singleton Design Pattern

1.1.1 Overview and Implementation

A class is only allowed to create one object (or instance), then this class is a singleton class, this design pattern is called singleton design pattern, referred to as singleton pattern

Application scenario:

  • Resolve Resource Access Violations
  • Represents a globally unique class

Method to realize:

1. Hungry Chinese style: does not support lazy loading

public class IdGenerator {
    
    
    private AtomicLong id = new AtomicLong(0);
    private static final IdGenerator instance = new IdGenerator();
    private IdGenerator() {
    
    }
    public static IdGenerator getInstance() {
    
    
        return instance;
    }
    public long getId() {
    
    
        return id.incrementAndGet();
    }
}

2. Lazy style: Delayed loading is supported, but you can see that a big lock (synchronzed) is added to getInstance()this method, resulting in low concurrency of this function, performance problems, and high concurrency not supported

public class IdGenerator {
    
    
    private AtomicLong id = new AtomicLong(0);
    private static IdGenerator instance;
    private IdGenerator() {
    
    }
    public static synchronized IdGenerator getInstance() {
    
    
        if (instance == null) {
    
    
            instance = new IdGenerator();
        }
        return instance;
    }
    public long getId() {
    
    
        return id.incrementAndGet();
    }
}

3. Double detection: supports both lazy loading and high concurrency

public class IdGenerator {
    
    
    private AtomicLong id = new AtomicLong(0);
    private static IdGenerator instance;
    private IdGenerator() {
    
    }
    public static IdGenerator getInstance() {
    
    
        if (instance == null) {
    
    
            synchronized(IdGenerator.class) {
    
     // 此处为类级别的锁
                if (instance == null) {
    
    
                    instance = new IdGenerator();
                }
            }
        }
        return instance;
    }
    public long getId() {
    
    
        return id.incrementAndGet();
    }
}

4. Static inner class: a simpler implementation method than double detection

public class IdGenerator {
    
    
    private AtomicLong id = new AtomicLong(0);
    private IdGenerator() {
    
    }
    private static class SingletonHolder{
    
    
        private static final IdGenerator instance = new IdGenerator();
    }
    public static IdGenerator getInstance() {
    
    
        return SingletonHolder.instance;
    }
    public long getId() {
    
    
        return id.incrementAndGet();
    }
}

5. Enumeration: Using the characteristics of the Java enumeration type itself, the thread safety of instance creation and the uniqueness of the instance are guaranteed

public class IdGenerator {
    
    
    private AtomicLong id = new AtomicLong(0);
    private IdGenerator() {
    
    }
    private static class SingletonHolder{
    
    
        private static final IdGenerator instance = new IdGenerator();
    }
    public static IdGenerator getInstance() {
    
    
        return SingletonHolder.instance;
    }
    public long getId() {
    
    
        return id.incrementAndGet();
    }
}

1.1.2 Multiple instances

"Singleton" means that a class can only create one object. Correspondingly, "multiple instances" means that a class can create multiple objects, but the number is limited, for example, only 3 objects can be created

public class BackendServer {
    
    
	private long serverNo;
	private String serverAddress;
	private static final int SERVER_COUNT = 3;
	private static final Map<Long, BackendServer> serverInstances = new HashMap<>();

	static {
    
    
		serverInstances.put(1L, new BackendServer(1L, "192.134.22.138:8080"));
		serverInstances.put(2L, new BackendServer(2L, "192.134.22.139:8080"));
		serverInstances.put(3L, new BackendServer(3L, "192.134.22.140:8080"));
	}
	private BackendServer(long serverNo, String serverAddress) {
    
    
		this.serverNo = serverNo;
		this.serverAddress = serverAddress;
	}
	public BackendServer getInstance(long serverNo) {
    
    
		return serverInstances.get(serverNo);
	}
	public BackendServer getRandomInstance() {
    
    
		Random r = new Random();
		int no = r.nextInt(SERVER_COUNT)+1;
		return serverInstances.get(no);
	}
}

There is another way to understand the multi-instance pattern: only one object of the same type can be created, and multiple objects of different types can be created. How to understand the "type" here?

In the following example, in the code, loggerName is the "type" mentioned above, the object instances obtained by the same loggerName are the same, and the object instances obtained by different loggerName are different

public class Logger {
    
    
	private static final ConcurrentHashMap<String, Logger> instances
	    = new ConcurrentHashMap<>();
	private Logger() {
    
    }
	public static Logger getInstance(String loggerName) {
    
    
		instances.putIfAbsent(loggerName, new Logger());
		return instances.get(loggerName);
	}
	public void log() {
    
    
            //...
	}
}
//l1==l2, l1!=l3
Logger l1 = Logger.getInstance("User.class");
Logger l2 = Logger.getInstance("User.class");
Logger l3 = Logger.getInstance("Order.class");

The understanding of this multi-instance mode is somewhat similar to the factory mode . It differs from the factory pattern in that:

  • The objects created by the multi-instance pattern are all objects of the same class
  • The factory pattern creates objects of different subclasses

In fact, it is somewhat similar to Flyweight mode. In addition, in fact, the enumeration type is also equivalent to the multi-instance mode, a type can only correspond to one object, and a class can create multiple objects

Details can be seen: Summary of the Beauty of Design Patterns (Creative Type)_Fan 223's Blog Singleton Mode

1.2 Factory Pattern (Factory Design Pattern)

In general, the factory pattern is divided into three more subdivided types: simple factory, factory method and abstract factory. However, in GoF's "Design Patterns", it regards the simple factory pattern as a special case of the factory method pattern, so the factory pattern is only divided into factory methods and abstract factories. In fact, the former classification method is more common

1.2.1 Simple Factory

As follows, different parser classes are created according to different suffix names. Every time createParser()the method , a new parser must be created, and this implementation method is called the first implementation method of the simple factory pattern

public class RuleConfigParserFactory {
    
    
	public static IRuleConfigParser createParser(String configFormat) {
    
    
		IRuleConfigParser parser = null;
		if ("json".equalsIgnoreCase(configFormat)) {
    
    
			parser = new JsonRuleConfigParser();
		} else if ("xml".equalsIgnoreCase(configFormat)) {
    
    
			parser = new XmlRuleConfigParser();
		} else if ("yaml".equalsIgnoreCase(configFormat)) {
    
    
			parser = new YamlRuleConfigParser();
		} else if ("properties".equalsIgnoreCase(configFormat)) {
    
    
			parser = new PropertiesRuleConfigParser();
		}
		return parser;
	}
}

If the parser can be reused, in order to save memory and object creation time, the parser can be created and cached in advance, as follows. This implementation method is called the second implementation method of the simple factory pattern

public class RuleConfigParserFactory {
    
    
	private static final Map<String, RuleConfigParser> cachedParsers = new HashMap<>();
	static {
    
    
		cachedParsers.put("json", new JsonRuleConfigParser());
		cachedParsers.put("xml", new XmlRuleConfigParser());
		cachedParsers.put("yaml", new YamlRuleConfigParser());
		cachedParsers.put("properties", new PropertiesRuleConfigParser());
	}
	public static IRuleConfigParser createParser(String configFormat) {
    
    
		if (configFormat == null || configFormat.isEmpty()) {
    
    
                        // 或抛出 IllegalArgumentException
			return null;
		}
		IRuleConfigParser parser = cachedParsers.get(configFormat.toLowerCase());
		return parser;
	}
}

1.2.2 Factory Method (Factory Method)

In the first implementation method of the simple factory pattern, there is a set of if branch logic. In fact, if there are not many if branches, it is completely acceptable to have if branches in the code

Polymorphism or design patterns can also be used to replace the if branch judgment logic, but it is not without any disadvantages. Although it improves the scalability of the code and is more in line with the principle of opening and closing, it also increases the number of classes and sacrifices Code readability. Here, according to the idea of ​​polymorphism, the above code is refactored

public interface IRuleConfigParserFactory {
    
    
	IRuleConfigParser createParser();
}
public class JsonRuleConfigParserFactory implements IRuleConfigParserFactory {
    
    
	@Override
	public IRuleConfigParser createParser() {
    
    
		return new JsonRuleConfigParser();
	}
}
public class XmlRuleConfigParserFactory implements IRuleConfigParserFactory {
    
    
	@Override
	public IRuleConfigParser createParser() {
    
    
		return new XmlRuleConfigParser();
	}
}
public class YamlRuleConfigParserFactory implements IRuleConfigParserFactory {
    
    
	@Override
	public IRuleConfigParser createParser() {
    
    
		return new YamlRuleConfigParser();
	}
}
public class PropertiesRuleConfigParserFactory implements IRuleConfigParserFactory {
    
    
	@Override
	public IRuleConfigParser createParser() {
    
    
		return new PropertiesRuleConfigParser();
	}
}

This is a typical code implementation of the factory method pattern. In this way, when adding a parser, you only need to add a Factory class that implements the IRuleConfigParserFactory interface. Therefore, the factory method pattern is more in line with the opening and closing principle than the simple factory pattern

But there are quite big problems in the use of these factory classes, as follows:

public class RuleConfigSource {
    
    
	public RuleConfig load(String ruleConfigFilePath) {
    
    
		String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
		IRuleConfigParserFactory parserFactory = null;

		if ("json".equalsIgnoreCase(ruleConfigFileExtension)) {
    
    
			parserFactory = new JsonRuleConfigParserFactory();
		} else if ("xml".equalsIgnoreCase(ruleConfigFileExtension)) {
    
    
			parserFactory = new XmlRuleConfigParserFactory();
		} else if ("yaml".equalsIgnoreCase(ruleConfigFileExtension)) {
    
    
			parserFactory = new YamlRuleConfigParserFactory();
		} else if ("properties".equalsIgnoreCase(ruleConfigFileExtension)) {
    
    
			parserFactory = new PropertiesRuleConfigParserFactory();
		} else {
    
    
			throw new InvalidRuleConfigException("Rule config file fo support")rmat is not

From the above code implementation, the creation logic of the factory class object is coupled into load()the function , which is very similar to the first implementation method of the simple factory pattern. How to solve this problem? You can create another simple factory for the factory class, that is, the factory of the factory, to create the factory class object

public class RuleConfigSource {
    
    
	public RuleConfig load(String ruleConfigFilePath) {
    
    
		String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
		IRuleConfigParserFactory parserFactory = RuleConfigParserFactoryMap.getParserFactory(ruleConfigFileExtension);
		if (parserFactory == null) {
    
    
			throw new InvalidRuleConfigException("Rule config file format is not support");
		}
		IRuleConfigParser parser = parserFactory.createParser();
		String configText = "";
		// 从ruleConfigFilePath文件中读取配置文本到configText中
		RuleConfig ruleConfig = parser.parse(configText);
		return ruleConfig;
	}
	private String getFileExtension(String filePath) {
    
    
		// ...解析文件名获取扩展名,比如rule.json,返回json
		return "json";
	}
}

// 因为工厂类只包含方法,不包含成员变量,完全可以复用,不需要每次都创建新的工厂类对象,
// 所以,简单工厂模式的第二种实现思路更加合适
public class RuleConfigParserFactoryMap {
    
     //工厂的工厂
	private static final Map<String, IRuleConfigParserFactory> cachedFactories = new HashMap<>();
	static {
    
    
		cachedFactories.put("json", new JsonRuleConfigParserFactory());
		cachedFactories.put("xml", new XmlRuleConfigParserFactory());
		cachedFactories.put("yaml", new YamlRuleConfigParserFactory());
		cachedFactories.put("properties", new PropertiesRuleConfigParserFactory())
	}
	public static IRuleConfigParserFactory getParserFactory(String type) {
    
    
		if (type == null || type.isEmpty()) {
    
    
			return null;
		}
		IRuleConfigParserFactory parserFactory = cachedFactories.get(type.toLowerCase());
		return parserFactory;
	}
}

1.2.3 Factory Method Pattern VS Simple Factory Pattern

When the creation logic of the object is more complex, not just a simple new one, but to combine other class objects and perform various initialization operations, it is recommended to use the factory method pattern to split the complex creation logic into multiple factories class, so that each factory class will not be too complicated. However, using the simple factory pattern and putting all the creation logic in a factory class will make the factory class very complicated

In addition, in some scenarios, if the object is not reusable, the factory class must return a different object every time. If you use the simple factory pattern to implement, you can only choose the first implementation method that contains if branch logic. If you still want to avoid annoying if-else branch logic, at this time, it is recommended to use the factory method pattern

1.2.4 Abstract Factory

In the simple factory and factory method above, the class has only one classification. However, if the class has two classification methods, such as the parser example above, if it can be classified according to the configuration file format or the parsed object (Rule rule configuration or System system configuration), it will correspond to the following These 8 parser classes

针对规则配置的解析器: 基于接口IRuleConfigParser
JsonRuleConfigParser
XmlRuleConfigParser
YamlRuleConfigParser
PropertiesRuleConfigParser
针对系统配置的解析器: 基于接口ISystemConfigParser
JsonSystemConfigParser
XmlSystemConfigParser
YamlSystemConfigParser
PropertiesSystemConfigParser

If you continue to use the factory method to implement, you need to write a factory class for each parser, that is, you need to write 8 factory classes. If you need to add a parser for business configuration (such as IBizConfigParser) in the future, you need to add 4 more factory classes accordingly. Too many classes can also make the system hard to maintain. How to solve this problem?

The abstract factory was born for this very special scenario. Instead of creating just one parser object, you can have a single factory responsible for creating multiple objects of different types (IRuleConfigParser, ISystemConfigParser, etc.). This can effectively reduce the number of factory classes

public interface IConfigParserFactory {
    
    
	IRuleConfigParser createRuleParser();
	ISystemConfigParser createSystemParser();
        // 此处可以扩展新的parser类型,比如IBizConfigParser
}
public class JsonConfigParserFactory implements IConfigParserFactory {
    
    
	@Override
	public IRuleConfigParser createRuleParser() {
    
    
		return new JsonRuleConfigParser();
	}
	@Override
	public ISystemConfigParser createSystemParser() {
    
    
		return new JsonSystemConfigParser();
	}
}
public class XmlConfigParserFactory implements IConfigParserFactory {
    
    
	@Override
	public IRuleConfigParser createRuleParser() {
    
    
		return new XmlRuleConfigParser();
	}
	@Override
	public ISystemConfigParser createSystemParser() {
    
    
		return new XmlSystemConfigParser();
	}
}
// 省略YamlConfigParserFactory和PropertiesConfigParserFactory代码

Details can be seen: Summary of the beauty of design patterns (creative articles)_Fan 223's Blog Factory Patterns

1.3 Builder/Builder/Generator Pattern (Builder Design Pattern)

In normal development, the most common way to create an object is to use the new keyword to call the constructor of the class to complete. But under what circumstances is this method not applicable, and it is necessary to use the builder mode to create objects?

Let’s first look at what is the invalid state of the object. As follows, a rectangular class is defined, which is created first and then set, which will cause the object to be in an invalid state before the first set

Rectangle r = new Rectange(); // r is invalid
r.setWidth(2); // r is invalid
r.setHeight(3); // r is valid

In order to avoid the existence of this invalid state, it is necessary to use the constructor to initialize all member variables at one time. If there are too many constructor parameters, the code will be poor in readability and usability. When using the constructor, it is easy to mistake the order of the parameters and pass in the wrong parameter values, resulting in very hidden bugs. At this time, you need to consider using the builder mode, first set the builder's variables, and then once again Create objects in a timely manner, so that the objects are always in a valid state

public class ResourcePoolConfig {
    
    
	private String name;
	private int maxTotal;
	private int maxIdle;
	private int minIdle;
	private ResourcePoolConfig(Builder builder) {
    
    
		this.name = builder.name;
		this.maxTotal = builder.maxTotal;
		this.maxIdle = builder.maxIdle;
		this.minIdle = builder.minIdle;
	}
	//...省略getter方法...
	// 将Builder类设计成了ResourcePoolConfig的内部类。
	// 也可以将Builder类设计成独立的非内部类ResourcePoolConfigBuilder。
	public static class Builder {
    
    
		private static final int DEFAULT_MAX_TOTAL = 8;
		private static final int DEFAULT_MAX_IDLE = 8;
		private static final int DEFAULT_MIN_IDLE = 0;
		private String name;
		private int maxTotal = DEFAULT_MAX_TOTAL;
		private int maxIdle = DEFAULT_MAX_IDLE;
		private int minIdle = DEFAULT_MIN_IDLE;
		public ResourcePoolConfig build() {
    
    
			// 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等
			if (StringUtils.isBlank(name)) {
    
    
				throw new IllegalArgumentException("...");
			}
			if (maxIdle > maxTotal) {
    
    
				throw new IllegalArgumentException("...");
			}
			if (minIdle > maxTotal || minIdle > maxIdle) {
    
    
				throw new IllegalArgumentException("...");
			}
			return new ResourcePoolConfig(this);
		}
		public Builder setName(String name) {
    
    
			if (StringUtils.isBlank(name)) {
    
    
				throw new IllegalArgumentException("...");
			}
			this.name = name;
			return this;
		}
		public Builder setMaxTotal(int maxTotal) {
    
    
			if (maxTotal <= 0) {
    
    
				throw new IllegalArgumentException("...");
			}
			this.maxTotal = maxTotal;
			return this;
		}
		public Builder setMaxIdle(int maxIdle) {
    
    
			if (maxIdle < 0) {
    
    
				throw new IllegalArgumentException("...");
			}
			this.maxIdle = maxIdle;
			return this;
		}
		public Builder setMinIdle(int minIdle) {
    
    
			if (minIdle < 0) {
    
    
				throw new IllegalArgumentException("...");
			}
			this.minIdle = minIdle;
			return this;
		}
	}
}

// 这段代码会抛出IllegalArgumentException,因为minIdle>maxIdle
ResourcePoolConfig config = new ResourcePoolConfig.Builder()
	.setName("dbconnectionpool")
	.setMaxTotal(16)
	.setMaxIdle(10)
	.setMinIdle(12)
	.build();

Details can be seen: Summary of the beauty of design patterns (creative articles )

1.3.1 How is it different from the factory pattern?

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. Builder mode is used to create a type of complex object, by setting different optional parameters, "customized" to create different objects

For example, when a customer walks into a restaurant to order food, we use the factory model to make different foods, such as pizza, burgers, and salads, according to different choices of users. For pizza, users can customize various ingredients, such as cheese, tomato, and cheese. We use the builder mode to make pizza according to different ingredients selected by users

1.4 Prototype Design Pattern

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

The concept should not be difficult to understand. Through the concept, you should roughly know the principle and usage of the design pattern. For details, see: Summary of the Beauty of Design Patterns (Creative Type)_Fan 223's Blog Prototype Mode

2. Structural

Structural design patterns mainly solve the problem of "combination or assembly of classes or objects"

2.1 Proxy Design Pattern

Without changing the code of the original class (or called the proxy class), add functions to the original class by introducing the proxy class . In the following example, login()the method to process the login logic

public interface IUserController {
    
    
	UserVo login(String telephone, String password);
}

public class UserController implements IUserController {
    
    
	//...省略其他属性和方法...
	public UserVo login(String telephone, String password) {
    
    
		// ... 省略login逻辑...
		//...返回UserVo数据...
	}
}

Suppose you want to expand additional functions without changing the method, such as collecting raw data requested by the interface, such as access time, processing time, etc., then the proxy mode comes in handy

As follows, the proxy class UserControllerProxy and the original class UserController implement the same interface IUserController. The UserController class is only responsible for business functionality. The proxy class UserControllerProxy is responsible for attaching other logic codes before and after the execution of the business code, and calls the original class to execute the business code through delegation

public class UserControllerProxy implements IUserController {
    
    
	private MetricsCollector metricsCollector;
	private UserController userController;

	public UserControllerProxy(UserController userController) {
    
    
		this.userController = userController;
		this.metricsCollector = new MetricsCollector();
	}
	@Override
	public UserVo login(String telephone, String password) {
    
    
		long startTimestamp = System.currentTimeMillis();
		// 委托
		UserVo userVo = userController.login(telephone, password);
		long endTimeStamp = System.currentTimeMillis();
		long responseTime = endTimeStamp - startTimestamp;
		RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp);
		metricsCollector.recordRequest(requestInfo);
		return userVo;
	}
}

// 因为原始类和代理类实现相同的接口,是基于接口而非实现编程
// 将UserController类对象替换为UserControllerProxy类对象,不需要改动太多代码
IUserController userController = new UserControllerProxy(new UserController())

If the original class does not define an interface, and the original class code is not developed and maintained by us (for example, it comes from a third-party class library), we cannot directly modify the original class and redefine an interface for it. For the extension of this kind of external class, the way of inheritance is generally adopted. Let the proxy class inherit the original class, and then extend the additional functions, as follows:

public class UserControllerProxy extends UserController {
    
    
	private MetricsCollector metricsCollector;
	public UserControllerProxy() {
    
    
		this.metricsCollector = new MetricsCollector();
	}
	public UserVo login(String telephone, String password) {
    
    
		long startTimestamp = System.currentTimeMillis();
		UserVo userVo = super.login(telephone, password);
		long endTimeStamp = System.currentTimeMillis();
		long responseTime = endTimeStamp - startTimestamp;
		RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp);
		metricsCollector.recordRequest(requestInfo);
		return userVo;
	}
}
// UserControllerProxy使用举例
UserController userController = new UserControllerProxy();

Application scenario:

  1. Development of non-functional requirements for business systems
  2. Apply in RPC
  3. apply in cache

Details can be seen: Summary of the Beauty of Design Patterns (Structural Type)_Fan 223's Blog Proxy Mode

2.2 Bridge/Bridge Mode (Bridge Design Pattern)

In GoF's "Design Patterns" book, the bridge pattern is defined as follows:

Decouple an abstraction from its implementation so that the two can vary independently.
Decouple abstraction and implementation so that they can vary independently

Understanding the three concepts of "abstract", "implementation" and "decoupling" in the definition is the key to understanding the bridge mode

  • Abstraction can be understood as a common conceptual connection that exists in multiple entities, which means ignoring some information and treating different entities as the same entity
  • Implementation, that is, the specific implementation given by the abstraction, there may be many different implementation methods
  • Decoupling, the so-called coupling, is a strong relationship between the behavior of two entities. And removing their strong association is the release of coupling, or decoupling. Here, decoupling refers to decoupling the coupling between abstraction and implementation, or changing the strong association between them into a weak association. Changing the inheritance relationship between two roles to an aggregation relationship is to change the strong association between them into a weak association, that is, use a combination/aggregation relationship instead of an inheritance relationship between abstraction and implementation

It is recommended to understand it by comparing it with an example. For details, see: Summary of the Beauty of Design Patterns (Structure Type)_Fan 223's Blog Bridging Mode Part

2.3 Decorator Design Pattern

The decorator mode is similar to the proxy mode, but in the proxy mode, the proxy class adds functions that have nothing to do with the original class, while in the decorator mode, the decorator class adds enhanced functions related to the original class

// 代理模式的代码结构(下面的接口也可以替换成抽象类
public interface IA {
    
    
	void f();
}
public class A impelements IA {
    
    
	public void f() {
    
    
		//...
	}
}
public class AProxy impements IA {
    
    
	private IA a;
	public AProxy(IA a) {
    
    
		this.a = a;
	}
	public void f() {
    
    
		// 新添加的代理逻辑
		a.f();
		// 新添加的代理逻辑
	}
}

// 装饰器模式的代码结构(下面的接口也可以替换成抽象类)
public interface IA {
    
    
	void f();
}
public class A impelements IA {
    
    
	public void f() {
    
    
		//...
	}
}
public class ADecorator impements IA {
    
    
	private IA a;
	public ADecorator(IA a) {
    
    
		this.a = a;
	}
	public void f() {
    
    
		// 功能增强代码
		a.f();
		// 功能增强代码
	}
}

Multiple decorations are also possible:

class Father {
    
    
    public void run() {
    
    
        System.out.println("Father run");
    }
}

class Son extends Father{
    
    
    public void run() {
    
    
        System.out.println("Son run");
    }
}

class ChildDecorator extends Father {
    
    
    protected Father father;

    public ChildDecorator(Father father) {
    
    
        this.father = father;
    }

    public void run() {
    
    
        father.run();
        System.out.println("ChildDecorator run");
    }
}

class Child1 extends ChildDecorator{
    
    

    public Child1(Father father) {
    
    
        super(father);
    }

    public void run() {
    
    
        father.run();
        System.out.println("Child1 run");
    }
}

class Child2 extends ChildDecorator {
    
    

    public Child2(Father father) {
    
    
        super(father);
    }

    public void run() {
    
    
        father.run();
        System.out.println("Child2 run");
    }
}
public static void main(String[] args) {
    
    
        Father son = new Son();
        Father child1 = new Child1(son);
        Child2 child2 = new Child2(child1);
        child2.run();
}

insert image description here

Details can be seen: Summary of the beauty of design patterns (structural articles)_Fan 223's blog Decorator mode part

2.4 Adapter Design Pattern

As the name suggests, this mode is used for adaptation. It 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.

1. Class adapter

// 类适配器: 基于继承
public interface ITarget {
    
    
	void f1();
	void f2();
	void fc();
}
public class Adaptee {
    
    
	public void fa() {
    
    
		//...
	}
	public void fb() {
    
    
		//...
	}
	public void fc() {
    
    
		//...
	}
}
public class Adaptor extends Adaptee implements ITarget {
    
    
	public void f1() {
    
    
		super.fa();
	}
	public void f2() {
    
    
		//...重新实现f2()...
	}
	// 这里fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点
}

2. Object Adapter

// 对象适配器:基于组合
public interface ITarget {
    
    
	void f1();
	void f2();
	void fc();
}
public class Adaptee {
    
    
	public void fa() {
    
    
		//...
	}
	public void fb() {
    
    
		//...
	}
	public void fc() {
    
    
		//...
	}
}
public class Adaptor implements ITarget {
    
    
	private Adaptee adaptee;
	public Adaptor(Adaptee adaptee) {
    
    
		this.adaptee = adaptee;
	}
	public void f1() {
    
    
		adaptee.fa(); //委托给Adaptee
	}
	public void f2() {
    
    
		//...重新实现f2()...
	}
	public void fc() {
    
    
		adaptee.fc();
	}
}

For these two implementation methods, in actual development, how to choose which one to use? There are two main criteria for judging, one is the number of Adaptee interfaces, and the other is the degree of fit between Adaptee and ITarget

If there are not many Adaptee interfaces, both implementations are fine. If there are many Adaptee interfaces, and most of the Adaptee and ITarget interface definitions are the same, then it is recommended to use a class adapter, because the Adapter reuses the interface of the parent class Adaptee, and the code amount of the Adaptor is less than the implementation of the object adapter. If there are many Adaptee interfaces, and most of the Adaptee and ITarget interface definitions are different, it is recommended to use an object adapter, because the composition structure is more flexible than inheritance

Application scenario:

  1. Encapsulate flawed interface design
  2. Unify the interface design of multiple classes
  3. Replace dependent external systems
  4. Compatible with old version interface
  5. Adapt to data in different formats

Details can be seen: Summary of the Beauty of Design Patterns (Structure Type)_Fan 223's blog Adapter pattern part

2.5 The difference between the four design patterns of proxy, bridge, decorator and adapter

Proxy, bridge, decorator, adapter, these four patterns are more commonly used structural design patterns. Their code structures are very similar. Generally speaking, they can all be called the Wrapper mode, that is, the original class is encapsulated twice through the Wrapper class

Although the code structure is similar, the intentions of these four design patterns are completely different, that is to say, the problems to be solved and the application scenarios are different, which is their main difference

  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 functions. This is the biggest difference between it and the decorator mode
  2. Bridge mode: The purpose of the bridge mode is to separate the interface part from the implementation part, so that they can be changed more easily and relatively independently
  3. Decorator mode: The decorator mode enhances the function of the original class without changing the interface of the original class, and supports the nesting of multiple decorators
  4. Adapter pattern: The adapter pattern is an afterthought remedial strategy. The adapter provides a different interface from the original class, while the proxy mode and decorator mode provide the same interface as the original class

2.6 Facade Design Pattern

In the GoF book "Design Patterns", the facade pattern is defined as follows:

Provide a unified interface to a set of interfaces in a subsystem. Facade Pattern defines a higher-level interface that makes the subsystem easier to use
. use

The concept is very simple. Suppose there is a system A that provides four interfaces a, b, c, and d. When system B completes a certain business function, it needs to call interfaces a, b, and d of system A. Using the facade mode, we provide a facade interface x that wraps the calls of a, b, and d interfaces for direct use by system B

Application scenario:

  1. Solve the problem of ease of use: the facade mode can be used to encapsulate the underlying implementation of the system, hide the complexity of the system, and provide a set of easier-to-use, higher-level interfaces
  2. Solve performance problems, reduce network communication costs and improve App client response speed by replacing multiple interface calls with one facade interface call
  3. Solving Distributed Transaction Problems

Details can be seen: Summary of the Beauty of Design Patterns (Structure Type)_Fan 223's Blog Facade Mode

2.7 Composite Design Pattern

The composition mode is completely different from the "composition relationship (to assemble two classes through composition)" in object-oriented design. The "combination mode" mentioned here is mainly used to process tree-structured data. The "data" here can be simply understood as a set of object collections

In the GoF book "Design Patterns", the composition pattern is defined as follows:

Compose objects into tree structure to represent part-whole hierarchies. Composite lets client treat individual objects and compositions of objects uniformly
. Composition allows the client (in many design pattern books, "client" refers to the user of the code) to unify the processing logic of individual objects and composite objects

For example, the organizational structure of a company contains two data types: department and employee. Among them, the department can contain sub-departments and employees. This is a nested structure that can be represented as a tree data structure. At this time, the combination mode can be used to design and implement

Compare this example with the definition of the composite pattern: "Organize a set of objects (employees and departments) into a tree structure to represent a 'part-whole' hierarchy (nested structure of departments and sub-departments) .The composition mode allows the client to unify the processing logic (recursive traversal) of a single object (employee) and a composite object (department)."

Details can be seen: Summary of the beauty of design patterns (structural articles)_Fan 223's Blog Combination Patterns

2.8 Flyweight Design Pattern

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 repeated objects in a system, if these repeated objects are immutable objects, you can use the flyweight pattern to design the objects as flyweights, and only keep one instance in memory for multiple places. code references. This can reduce the number of objects in memory and save memory. In fact, not only the same objects can be designed as flyweights, but for similar objects, the same parts (fields) in these objects can also be extracted and designed as flyweights, so that a large number of similar objects can refer to these flyweights

The "immutable object" in the definition means that once it is initialized through the constructor, its state (member variables or properties of the object) will not be modified again. Therefore, immutable objects cannot expose set()any methods that modify internal state. The reason why the flyweight is required to be an immutable object is because it will be shared and used by multiple codes, so as to prevent one piece of code from modifying the flyweight and affecting other codes that use it

The concept is actually very simple, that is, to reuse repeated objects or repeated parts of similar objects

Details can be seen: Summary of the Beauty of Design Patterns (Structural Type)_Fan 223's Blog

2.8.1 Flyweight mode vs singleton, cache, object pool

1. The difference between flyweight mode and singleton

In the singleton mode, a class can only create one object, while in the Flyweight mode, a class can create multiple objects, and each object is shared by multiple code references. In fact, the Flyweight pattern is somewhat similar to the variant of the singleton mentioned before: multiple instances

But to distinguish between the two design patterns, you can't just look at the code implementation, but look at the design intent, that is, the problem to be solved. Although there are many similarities between flyweight mode and multiple instances from the point of view of code implementation, they are completely different from the point of view of design intent. The Flyweight mode is used to reuse objects and save memory, while the multi-instance mode is used to limit the number of objects

2. The difference between flyweight mode and cache

In the implementation of the Flyweight pattern, the created objects are "cached" through the factory class. The "cache" here actually means "storage", which is different from the usual "database cache", "CPU cache" and "MemCache cache". The cache we usually talk about is mainly to improve access efficiency, not to reuse

3. The difference between flyweight mode and object pool

Object pools, connection pools (such as database connection pools), thread pools, etc. are also for reuse, so what is the difference between them and the Flyweight mode?

Many people may be familiar with connection pools and thread pools, but unfamiliar with object pools. Here is a brief explanation of object pools. In a programming language like C++, memory management is the responsibility of the programmer. In order to avoid memory fragmentation caused by frequent object creation and release, you can pre-apply for a continuous memory space, which is the object pool mentioned here. Every time an object is created, an idle object is directly taken out of the object pool for use. After the object is used, it is put back into the object pool for subsequent reuse instead of being released directly.

Although object pools, connection pools, thread pools, and Flyweight modes are all for reuse, if you carefully pick out the word "reuse", the pooling technologies such as object pools, connection pools, and thread pools "Reuse" and "reuse" in Flyweight mode are actually different concepts

"Reuse" in pooling technology can be understood as "reuse", the main purpose is to save time (such as taking a connection from the database pool without recreating it). At any time, each object, connection, and thread will not be used in multiple places, but will be exclusively used by one user. After the use is completed, it will be returned to the pool and reused by other users. "Reuse" in Flyweight mode can be understood as "shared use", which is shared by all users throughout the life cycle, the main purpose is to save space

3. Behavioral

The behavioral design pattern mainly solves the problem of "interaction between classes or objects"

3.1 Observer/Publish-Subscribe Pattern (Observer Design Pattern/Publish-Subscribe Design Pattern)

In the GoF's "Design Patterns" book, it is defined like this:


Define a one-to-many dependency between objects so that when one object changes state, all its dependencies are notified and updated automatically. Objects are automatically notified

In general, the dependent object is called the observable (Observable), and the dependent object is called the observer (Observer). However, in actual project development, the names of these two objects are relatively flexible, and there are various names, such as: Subject-Observer, Publisher-Subscriber, Producer-Consumer, EventEmitter-EventListener, Dispatcher-Listener. No matter how you call it, as long as the application scenario conforms to the definition just given, it can be regarded as the observer mode

In fact, the observer mode is a relatively abstract mode. According to different application scenarios and requirements, there are completely different implementation methods. Here is the most classic implementation method. This is also when talking about this mode. The most common implementation method given by many books or materials. The specific code is as follows:

public interface Subject {
    
    
	void registerObserver(Observer observer);
	void removeObserver(Observer observer);
	void notifyObservers(Message message);
}
public interface Observer {
    
    
	void update(Message message);
}
public class ConcreteSubject implements Subject {
    
    
	private List<Observer> observers = new ArrayList<Observer>();
	@Override
	public void registerObserver(Observer observer) {
    
    
		observers.add(observer);
	}
	@Override
	public void removeObserver(Observer observer) {
    
    
		observers.remove(observer);
	}
	@Override
	public void notifyObservers(Message message) {
    
    
		for (Observer observer : observers) {
    
    
			observer.update(message);
		}
	}
}
public class ConcreteObserverOne implements Observer {
    
    
	@Override
	public void update(Message message) {
    
    
		//TODO: 获取消息通知,执行自己的逻辑...
		System.out.println("ConcreteObserverOne is notified.");
	}
}
public class ConcreteObserverTwo implements Observer {
    
    
	@Override
	public void update(Message message) {
    
    
		//TODO: 获取消息通知,执行自己的逻辑...
		System.out.println("ConcreteObserverTwo is notified.");
	}
}
public class Demo {
    
    
	public static void main(String[] args) {
    
    
		ConcreteSubject subject = new ConcreteSubject();
		subject.registerObserver(new ConcreteObserverOne());
		subject.registerObserver(new ConcreteObserverTwo());
		subject.notifyObservers(new Message());
	}
}

The above code is regarded as the "template code" of the observer mode, which can only reflect the general design ideas. In real software development, there is no need to copy the template code above. There are various ways to implement the observer mode. The names of functions and classes will vary greatly according to different business scenarios. For example, the register function can also be called attach, and the remove function can also be called detach, etc. However, the ever-changing remains the same, and the design ideas are almost the same

The core concept of the Observer pattern is actually in its definition. Details can be seen: Summary of the beauty of design patterns (behavioral articles)_Fan 223's Blog Observer Mode

3.2 Template Method Design Pattern

In the GoF's "Design Patterns" book, it is defined like this:

Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure
. implemented in subclasses. The template method pattern allows subclasses to redefine certain steps in the 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. The code example is as follows:

public abstract class AbstractClass {
    
    
	public final void templateMethod() {
    
    
		//...
		method1();
		//...
		method2();
		//...
	}
	protected abstract void method1();
	protected abstract void method2();
}
public class ConcreteClass1 extends AbstractClass {
    
    
	@Override
	protected void method1() {
    
    
		//...
	}
	@Override
	protected void method2() {
    
    
		//...
	}
}
public class ConcreteClass2 extends AbstractClass {
    
    
	@Override
	protected void method1() {
    
    
		//...
	}
	@Override
	protected void method2() {
    
    
		//...
	}
}
AbstractClass demo = ConcreteClass1();
demo.templateMethod();
  • Function 1: Reuse
    The template pattern abstracts the invariable process of an algorithm templateMethod()into , and leaves the variable part method1()to method2()the subclasses ContreteClass1 and ContreteClass2 to implement. All subclasses can reuse the process code defined by the template method in the parent class
  • Function 2: Expansion
    The expansion mentioned here does not refer to the extensibility of the code, but the extensibility of the framework, which is somewhat similar to the inversion of control mentioned before. Based on this role, the template mode is often used in the development of the framework, allowing framework users to customize the functions of the framework without modifying the framework source code

If you have used Java Servlet, inheriting the HttpServlet class and then rewriting doGet()and doPost()is a typical template pattern, here is the extension of the framework. It encapsulates the execution process of the Servlet, and then leaves the variable doGet()and doPost()parts to the inherited subclasses for concrete implementation

public class HelloServlet extends HttpServlet {
    
    
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Exception {
    
    
		this.doPost(req, resp);
	}
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws Exception {
    
    
		resp.getWriter().write("Hello World.");
	}
}

3.2.1 Template Pattern VS Callbacks

From the perspective of application scenarios, the synchronous callback is almost the same as the template mode. They are all in a large algorithm skeleton, free to replace a certain step in it, and achieve the purpose of code reuse and expansion. The asynchronous callback is quite different from the template mode, more like the observer mode

From the perspective of code implementation, the callback and template patterns are completely different. The callback is implemented based on the composition relationship. Passing one object to another object is a relationship between objects; the template mode is implemented based on the inheritance relationship. The subclass overrides the abstract method of the parent class, which is a relationship between classes.

As mentioned earlier, composition is better than inheritance. In fact, there is no exception here. In terms of code implementation, the callback is more flexible than the template mode, which is mainly reflected in the following points:

  • In a language like Java that only supports single inheritance, a subclass written based on the template pattern has inherited a parent class and no longer has the ability to inherit
  • Callbacks can use anonymous classes to create callback objects without having to define classes in advance; while template patterns define different subclasses for different implementations
  • If multiple template methods are defined in a class, and each method has a corresponding abstract method, then even if only one of the template methods is used, the subclass must implement all the abstract methods. The callback is more flexible, you only need to inject the callback object into the template method used

Detailed knowledge such as callbacks can be found in: Summary of the Beauty of Design Patterns (Behavioral Type)_Fan 223's Blog Template Mode

3.3 Strategy Design Pattern

In the GoF book "Design Patterns", it is defined like this:

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it. Define a family of algorithms,
encapsulate each algorithm separately, so that they can replace each other. The strategy pattern can make the changes of the algorithm independent of the client that uses them (the client here refers to the code that uses the algorithm)

The factory pattern is to decouple the creation and use of objects, and the observer pattern is to decouple the observer and the observed. The strategy pattern is similar to the two and can also play a decoupling role. However, it decouples the three parts of strategy definition, creation, and use

3.3.1 Policy definition

The definition of a strategy class is relatively simple, including a strategy interface and a group of strategy classes that implement this interface. Because all strategy classes implement the same interface, client code can flexibly replace different strategies based on the interface rather than implementation programming. The sample code is as follows:

public interface Strategy {
    
    
	void algorithmInterface();
}
public class ConcreteStrategyA implements Strategy {
    
    
	@Override
	public void algorithmInterface() {
    
    
		//具体的算法...
	}
}
public class ConcreteStrategyB implements Strategy {
    
    
	@Override
	public void algorithmInterface() {
    
    
		//具体的算法...
	}
}

Generally speaking, if the policy class is stateless, does not contain member variables, and is just a pure algorithm implementation, such a policy object can be shared and used, and there is no need to create a new policy object every time it getStrategy()is . In view of this situation, you can use the implementation of the factory class above, create each policy object in advance, cache it in the factory class, and return it directly when you use it

On the contrary, if the policy class is stateful, according to the needs of the business scenario, it is hoped that each time from the factory method, the newly created policy object is obtained instead of the cached shareable policy object, then it needs to be as follows To implement the policy factory class

public class StrategyFactory {
    
    
	public static Strategy getStrategy(String type) {
    
    
		if (type == null || type.isEmpty()) {
    
    
			throw new IllegalArgumentException("type should not be empty.");
		}
		if (type.equals("A")) {
    
    
			return new ConcreteStrategyA();
		} else if (type.equals("B")) {
    
    
			return new ConcreteStrategyB();
		}
		return null;
	}
}

3.3.2 Policy creation

Because the strategy mode will contain a set of strategies, when using them, the type (type) is generally used to determine which strategy to create for use. In order to encapsulate the creation logic, the creation details need to be shielded from the client code. The logic of creating strategies based on type can be extracted and placed in the factory class. The sample code is as follows:

public class StrategyFactory {
    
    
	private static final Map<String, Strategy> strategies = new HashMap<>();
	static {
    
    
		strategies.put("A", new ConcreteStrategyA());
		strategies.put("B", new ConcreteStrategyB());
	}
	public static Strategy getStrategy(String type) {
    
    
		if (type == null || type.isEmpty()) {
    
    
			throw new IllegalArgumentException("type should not be empty.");
		}
		return strategies.get(type);
	}
}

3.3.3 Use of policies

The Strategy pattern contains a set of optional strategies, how does client code typically determine which strategy to use? The most common is to dynamically determine which strategy to use at runtime, which is also the most typical application scenario of the strategy pattern. The "runtime dynamic" here refers to not knowing which strategy will be used in advance, but to dynamically decide which strategy to use according to uncertain factors such as configuration, user input, and calculation results during the running of the program.

// 策略接口:EvictionStrategy
// 策略类:LruEvictionStrategy、FifoEvictionStrategy、LfuEvictionStrategy...
// 策略工厂:EvictionStrategyFactory
public class UserCache {
    
    
	private Map<String, User> cacheData = new HashMap<>();
	private EvictionStrategy eviction;
	public UserCache(EvictionStrategy eviction) {
    
    
		this.eviction = eviction;
	}
	//...
}
// 运行时动态确定,根据配置文件的配置决定使用哪种策略
public class Application {
    
    
	public static void main(String[] args) throws Exception {
    
    
		EvictionStrategy evictionStrategy = null;
		Properties props = new Properties();
		props.load(new FileInputStream("./config.properties"));
		String type = props.getProperty("eviction_type");
		evictionStrategy = EvictionStrategyFactory.getEvictionStrategy(type);
		UserCache userCache = new UserCache(evictionStrategy);
		//...
	}
}
// 非运行时动态确定,在代码中指定使用哪种策略
public class Application {
    
    
	public static void main(String[] args) {
    
    
		//...
		EvictionStrategy evictionStrategy = new LruEvictionStrategy();
		UserCache userCache = new UserCache(evictionStrategy);
		//...
	}
}

From the above code, it can also be seen that "non-runtime dynamic determination", that is, the usage method in the second Application, cannot take advantage of the strategy mode. In this application scenario, the strategy pattern actually degenerates into "object-oriented polymorphism" or "based on interface rather than implementing programming principles"

Details can be seen: Summary of the Beauty of Design Patterns (Behavior Type)_Fan 223's Blog Strategy Patterns

3.4 Chain Of Responsibility Design Pattern

In GoF's "Design Patterns", it is defined as follows:


Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it. Multiple receiving objects have a chance to handle the request. String these receiving objects into a chain, and pass the request along the chain until one of the receiving objects in the chain can handle it

In the Chain of Responsibility pattern, multiple processors (that is, the "receiving object" just mentioned in the definition) process the same request in sequence. 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

There are many ways to realize the chain of responsibility mode. Here are two more commonly used ones.

1. Linked list

public abstract class Handler {
    
    
	protected Handler successor = null;
	public void setSuccessor(Handler successor) {
    
    
		this.successor = successor;
	}
	public final void handle() {
    
    
		boolean handled = doHandle();
		if (successor != null && !handled) {
    
    
			successor.handle();
		}
	}
	protected abstract boolean doHandle();
}
public class HandlerA extends Handler {
    
    
	@Override
	protected boolean doHandle() {
    
    
		boolean handled = false;
		//...
		return handled;
	}
}
public class HandlerB extends Handler {
    
    
	@Override
	protected boolean doHandle() {
    
    
		boolean handled = false;
		//...
		return handled;
	}
}

public class HandlerChain {
    
    
	private Handler head = null;
	private Handler tail = null;
	public void addHandler(Handler handler) {
    
    
		handler.setSuccessor(null);
		if (head == null) {
    
    
			head = handler;
			tail = handler;
			return;
		}
		tail.setSuccessor(handler);
		tail = handler;
	}
	public void handle() {
    
    
		if (head != null) {
    
    
			head.handle();
		}
	}
}

// 使用举例
public class Application {
    
    
	public static void main(String[] args) {
    
    
		HandlerChain chain = new HandlerChain();
		chain.addHandler(new HandlerA());
		chain.addHandler(new HandlerB());
		chain.handle();
	}
}

2. The HandlerChain class uses an array instead of a linked list to save all processors, and needs handle()to call handle()the function of each processor in the function of HandlerChain in turn

public interface IHandler {
    
    
	boolean handle();
}
public class HandlerA implements IHandler {
    
    
	@Override
	public boolean handle() {
    
    
		boolean handled = false;
		//...
		return handled;
	}
}
public class HandlerB implements IHandler {
    
    
	@Override
	public boolean handle() {
    
    
		boolean handled = false;
                //...
		return handled;
	}
}
public class HandlerChain {
    
    
	private List<IHandler> handlers = new ArrayList<>();
	public void addHandler(IHandler handler) {
    
    
		this.handlers.add(handler);
	}
	public void handle() {
    
    
		for (IHandler handler : handlers) {
    
    
			boolean handled = handler.handle();
			if (handled) {
    
    
				break;
			}
		}
	}
}
// 使用举例
public class Application {
    
    
	public static void main(String[] args) {
    
    
		HandlerChain chain = new HandlerChain();
		chain.addHandler(new HandlerA());
		chain.addHandler(new HandlerB());
		chain.handle();
	}
}

In the definition given by GoF, if a processor in the processor chain can handle the request, it will not continue to pass the request down. In fact, there is another variant of the responsibility chain mode, that is, the request will be processed by all processors, and there is no midway termination. This variant also has two implementations: using a linked list to store the processor and using an array to store the processor, similar to the above two implementations, only need to be slightly modified

In fact, it can be understood by comparing interceptors and filter chains. For details, see: Summary of the Beauty of Design Patterns (Behavior Type)_Fan 223's Blog Chain of Responsibility Mode Part

3.5 State Design Pattern

In actual software development, the state pattern is not very commonly used, but it can play a big role in the scenarios where it can be used. From this point of view, it is a bit like the combination mode mentioned earlier. State patterns are generally used to implement state machines, and state machines are often used in system development such as games and workflow engines. However, there are many ways to realize the state machine. In addition to the state mode, the more commonly used methods are branch logic method and look-up table method.

Finite state machine, English translation is Finite State Machine, abbreviated as FSM, referred to as state machine. The state machine has three components: state (State), event (Event), action (Action). Among them, an event is also called a transition condition (Transition Condition). Events trigger the transition of states and the execution of actions. However, the action is not necessary, and it is possible to only transfer the state without performing any action

For example, "Super Mario", in the game, Mario can transform into a variety of forms, such as Small Mario (Small Mario), Super Mario (Super Mario), Fire Mario (Fire Mario), Cape Mario ( Cape Mario) and so on. Under different game plots, each form will transform into each other, and the points will be increased or decreased accordingly. For example, the initial form is Little Mario, and after eating mushrooms, it will become Super Mario and increase 100 points

In fact, the transformation of Mario's form is a state machine. Among them, the different forms of Mario are the "states" in the state machine, the game plot (such as eating mushrooms) is the "event" in the state machine, and the addition and subtraction of points are the "actions" in the state machine. For example, the event of eating mushrooms will trigger the state transfer: from Little Mario to Super Mario, and trigger the execution of actions (add 100 points)

The concept is not difficult to understand, and the details can be seen: Summary of the Beauty of Design Patterns (Behavior Type)_Fan 223's Blog State Mode Part

3.6 Iterator/Cursor Pattern (Iterator Design Pattern/Cursor Design Pattern)

The iterator pattern is used to traverse collection objects. The "collection objects" mentioned here can also be called "containers" and "aggregate objects", which are actually objects that contain a group of objects, such as arrays, linked lists, trees, graphs, and jump lists. 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 is used to traverse the container, so a complete iterator pattern generally involves two parts: the container and the container iterator. In order to achieve the purpose of programming based on interfaces rather than implementations, containers include container interfaces and container implementation classes, and iterators include iterator interfaces and iterator implementation classes. A simple class diagram is as follows:

insert image description here

The concept is also very simple, which can be understood in comparison with Java's Iterator iterator. Most other programming languages ​​also provide iterator classes for traversing containers. In normal development, it can be used directly, and it is almost impossible to write from scratch. an iterator

Details can be seen: Summary of the Beauty of Design Patterns (Behavior Type)_Fan 223's Blog Iterator Pattern

3.7 Visitor Design Pattern

In the GoF's "Design Patterns" book, it is defined like this:

Allows for one or more operation to be applied to a set of objects at runtime, decoupling the operations from the object structure.
Allows one or more operations to be applied to a set of objects, decoupling operations and the object itself

The visitor mode is not easy to understand here. It is recommended to go directly to the detailed description: Summary of the beauty of design patterns (behavioral articles)_Fan 223's Blog Visitor Mode

This involves a double dispatch problem, Double Dispatch. Since there is a Double Dispatch, there is a corresponding Single Dispatch

  • The so-called Single Dispatch refers to the method of which object is executed, which is determined according to the runtime type of the object; which method of the object is executed, which is determined according to the compile-time type of the method parameter
  • The so-called Double Dispatch refers to the method of which object to execute, which is determined according to the runtime type of the object; which method to execute the object is determined according to the runtime type of the method parameter

The language that supports double dispatch does not need the visitor mode. The visitor mode is mainly to solve the polymorphism problem of overloaded method parameters when Single Dispatch is polymorphic.

3.8 Memento/Snapshot (Snapshot) Mode (Memento Design Pattern)

In the book "Design Patterns" by GoF, the Memento pattern is defined as follows:

Captures and externalizes an object's internal state so that it can be restored later
, all without violating encapsulation. for the previous state

The definition of this mode mainly expresses two parts, one part is to store the copy for later recovery, and the other part is to backup and restore objects without violating the principle of encapsulation

The concept is also very simple to understand, and can be understood by comparing it with the usual backup. The application scenarios of the two are very similar, and they are both applied in scenarios such as loss prevention, recovery, and revocation. The difference between them 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 details, see: Summary of the Beauty of Design Patterns (Behavioral Type)_Fan 223's Blog Memo Patterns

3.9 Command Design Pattern

In the GoF's "Design Patterns" book, it is defined like this:

The command pattern encapsulates a request as an object, thereby letting us parameterize other objects with different requests, queue or log requests, and support undoable operations
. Other objects (inject different request dependencies into other objects), and can support queued execution of requests (commands), logging, revocation, etc. (additional control) functions

When it comes to coding implementation, the core implementation method used in the command mode is to encapsulate functions into objects. The C language supports function pointers, and functions can be passed as variables. However, in most programming languages, functions cannot be passed as arguments to other functions, nor can they be assigned to variables. With the command pattern, functions can be encapsulated into objects. Specifically, design a class that contains this function, instantiate an object and pass it around, so that the function can be used like an object. From an implementation point of view, it is similar to the aforementioned callback

After the function is encapsulated into an object, the object can be stored for easy control and execution. Therefore, the main function and application scenarios of the command mode are to control the execution of commands, such as asynchronous, delay, queuing commands, undoing and redoing commands, storing commands, recording logs for commands, etc. This is what the command mode can do A place to play a unique role

Details can be seen: Summary of the Beauty of Design Patterns (Behavioral Type)_Fan 223's Blog Command Mode Part

3.9.1 Command Mode VS Strategy Mode

Seeing the above definition, you may feel that the command mode is very similar to the strategy mode and factory mode, so what is the difference between them? Not only that, but I feel that many of the previous models are very similar. Do you have similar feelings?

In fact, each design pattern should be composed of two parts: the first part is the application scenario, that is, what kind of problems this pattern can solve; the second part is the solution, that is, the design idea and specific code implementation of this pattern. However, code implementation does not have to be included in the pattern. If you only focus on the solution part, or even the code implementation, you will have the illusion that most of the patterns look similar

In fact, the main difference between design patterns lies in the design intent, which is the application scenario. Simply looking at design ideas or code implementation, some patterns are indeed very similar, such as strategy pattern and factory pattern

When talking about the strategy pattern, I mentioned that the strategy pattern includes three parts: the definition, creation and use of the strategy. From the code structure, it is very similar to the factory pattern. The difference between them is that the strategy pattern focuses on the specific application scenario of "strategy" or "algorithm", which is used to solve the problem of selecting different strategies from a set of strategies according to the runtime state, while the factory pattern focuses on the creation process of encapsulated objects. Here The object of is not limited by any business scenario, it can be a strategy, but it can also be other things. From the design intent, these two modes are completely different things

Let's look at the difference between the command mode and the strategy mode. You may think that the execution logic of commands can also be regarded as a strategy, so is it a strategy pattern? In fact, there is a slight difference between the two

In the strategy pattern, different strategies have the same purpose, different implementations, and can be replaced with each other. For example, BubbleSort and SelectionSort are both used to implement sorting, but one is implemented using the bubble sort algorithm, and the other is implemented using the selection sort algorithm. In the command mode, different commands have different purposes, correspond to different processing logic, and are not interchangeable with each other.

3.10 Interpreter Design Pattern

In the GoF book "Design Patterns", it is defined like this:

Interpreter pattern is used to define a grammatical representation for a language and provides an interpreter to deal with this grammar
.

There are many concepts that are rarely touched in normal development, such as "language", "grammar" and "interpreter". In fact, the "language" here does not only refer to the usual Chinese, English, Japanese, French and other languages. In a broad sense, as long as it is a carrier that can carry information, it can be called "language", for example, ancient knotting notes, Braille, dumb language, Morse code, etc.

In order to understand the information expressed by the "language", it is necessary to 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 is actually an interpreter used to interpret "sentences" according to grammatical rules

Suppose we define a new calculation "language" for addition, subtraction, multiplication and division, the grammar rules are as follows:

  • Operators only include addition, subtraction, multiplication, and division, and have no concept of precedence
  • In the expression (that is, the "sentence" mentioned above), write the number first, and then write the operator, separated by spaces
  • According to the sequence, take out two numbers and an operator calculation result, put the result back into the head position of the number, repeat the above process until there is only one number left, and this number is the final calculation result of the expression

For example, an expression such as "8 3 2 4 - + *" is processed according to the above grammatical rules, the number "8 3" and the "-" operator are taken out, and 5 is calculated, so the expression becomes "5 2 4+*". Then, take out the " 5 2 " and " + " operators, calculate 7, and the expression becomes " 7 4 * ". Finally, take out the "7 4" and "*" operators, and the final result is 28. What processes this result is the interpreter

The concept should not be difficult to understand, as you can see in detail: Summary of the Beauty of Design Patterns (Behavior Type)_Fan 223's blog Interpreter mode part

3.11 Mediator Design Pattern

In the book "Design Patterns" in the GoF, it is defined like this:


Mediator pattern defines a separate (mediator) object that encapsulates the interaction between a set of objects and the objects delegate their interaction to a mediator object instead of interacting with each other directly. An interaction between a set of objects. Delegate the interaction between the set of objects to intermediary objects to avoid direct interaction between objects

When talking about "how to decouple code", one of the methods is to introduce an intermediate layer. In fact, 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). Originally, an object needs to interact with n objects, but now it only needs to interact with an intermediary object, thereby minimizing the interaction between objects, reducing the complexity of the code, and improving the readability and maintainability of the code

A comparison diagram of object interaction relationship is drawn as follows. Among them, the interaction diagram on the right is the result of optimizing the interaction relationship on the left by using the intermediary mode. From the figure, it can be seen intuitively that the interaction relationship on the right is clearer and more concise.

insert image description here

When it comes to the intermediary model, there is a more classic example that has to be said, that is, air traffic control

In order for the aircraft to fly without interfering with each other, each aircraft needs to know the position of other aircraft at all times, which requires communication with other aircraft at all times. The communication network formed by aircraft communication will be extremely complicated. At this time, by introducing an intermediary such as "tower", let each aircraft only communicate with the tower, send its own position to the tower, and the tower is responsible for the route scheduling of each aircraft. This greatly simplifies the communication network

Details can be seen: Summary of the Beauty of Design Patterns (Behavior Type)_Fan 223's Blog Intermediary Mode Part

3.11.1 Intermediary Pattern VS Observer Pattern

When we talked about the observer mode, we mentioned that there are many ways to implement the observer mode. Although the classic implementation method cannot completely decouple the observer and the observed, the observer needs to be registered with the observed, and the state update of the observed needs to call the observer's update()method . However, in the cross-process implementation, the message queue can be used to achieve complete decoupling. Both the observer and the observed only need to interact with the message queue. The observer does not know the existence of the observed at all, and the observed does not know at all. aware of the existence of the observer

The intermediary mode is also to decouple the interaction between objects, and all participants only interact with the intermediary. The message queue in the observer mode is somewhat similar to the "intermediary" in the intermediary mode, and the observer and the observed in the observer mode are somewhat similar to the "participants" in the intermediary mode. Here comes the question: what is the difference between the intermediary mode and the observer mode? When to choose to use the mediation pattern? When to choose to use the observer pattern?

In the observer mode, although a participant can be both an observer and an observed person, in most cases, the interaction relationship is often one-way, and a participant is either an observer or an observer. The observed person will not have both identities. That is to say, in the application scenario of the observer mode, the interaction relationship between the participants is more organized

The intermediary model is just the opposite. Only when the interaction between participants is intricate and the maintenance cost is high, the mediation model should be considered. After all, the application of the intermediary pattern will bring certain side effects, and it may produce large and complex god classes. In addition, if the state of a participant changes, the operations performed by other participants have a certain sequence requirement. At this time, the intermediary model can use the intermediary class to implement the sequential order by calling the methods of different participants. control, and the observer mode cannot achieve such order requirements

Guess you like

Origin blog.csdn.net/ACE_U_005A/article/details/128497950