Summary of the beauty of design patterns (structural articles)


title: Summary of the Beauty of Design Patterns (Structural Type)
date: 2022-12-21 09:59:11
tags:

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


See the first four articles:

1. Proxy Design Pattern

1.1 Principle Analysis

The principle and code implementation of the Proxy Design Pattern are not difficult to grasp. It introduces additional functions to the original class by introducing the proxy class without changing the code of the original class (or called the proxy class). In the following example, this is a performance counter used to collect raw data of interface requests, such as access time, processing time, etc.

public class UserController {
    
    
	//...省略其他属性和方法...
	private MetricsCollector metricsCollector; // 依赖注入
	public UserVo login(String telephone, String password) {
    
    
		long  = System.currentTimeMillis();
		// ... 省略login逻辑...
		long endTimeStamp = System.currentTimeMillis();
		long responseTime = endTimeStamp - startTimestamp;
		RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp);
		metricsCollector.recordRequest(requestInfo);
		//...返回UserVo数据...
	}
	public UserVo register(String telephone, String password) {
    
    
		long startTimestamp = System.currentTimeMillis();
		// ... 省略register逻辑...
		long endTimeStamp = System.currentTimeMillis();
		long responseTime = endTimeStamp - startTimestamp;
		RequestInfo requestInfo = new RequestInfo("register", responseTime, startTimestamp);
		metricsCollector.recordRequest(requestInfo);
		//...返回UserVo数据...
	}
}

Obviously, there are two problems with the above writing. First, the performance counter framework code invades the business code and is highly coupled with the business code. If this framework needs to be replaced in the future, the replacement cost will be relatively high. Second, the code for collecting interface requests has nothing to do with business code, so it should not be placed in a class. The best business class has more single responsibilities and only focuses on business processing

In order to decouple the framework code and business code, the proxy mode comes in handy. 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. The specific code implementation is as follows:

public interface IUserController {
    
    
	UserVo login(String telephone, String password);
	UserVo register(String telephone, String password);
}
public class UserController implements IUserController {
    
    
	//...省略其他属性和方法...
	@Override
	public UserVo login(String telephone, String password) {
    
    
		//...省略login逻辑...
		//...返回UserVo数据...
	}
	@Override
	public UserVo register(String telephone, String password) {
    
    
		//...省略register逻辑...
		//...返回UserVo数据...
	}
}
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;
	}
	@Override
	public UserVo register(String telephone, String password) {
    
    
		long startTimestamp = System.currentTimeMillis();
		UserVo userVo = userController.register(telephone, password);
		long endTimeStamp = System.currentTimeMillis();
		long responseTime = endTimeStamp - startTimestamp;
		RequestInfo requestInfo = new RequestInfo("register", responseTime, startTimestamp);
		metricsCollector.recordRequest(requestInfo);
		return userVo;
	}
}

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

Referring to the design idea based on interfaces rather than implementing programming, when replacing original class objects with proxy class objects, in order to minimize code changes, in the code implementation of the proxy mode just now, the proxy class and the original class need to 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 (for example, it comes from a third-party class library), we cannot directly modify the original class and redefine an interface for it. In this case, how to implement the proxy mode?

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. The specific code is 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;
	}
	public UserVo register(String telephone, String password) {
    
    
		long startTimestamp = System.currentTimeMillis();
		UserVo userVo = super.register(telephone, password);
		long endTimeStamp = System.currentTimeMillis();
		long responseTime = endTimeStamp - startTimestamp;
		RequestInfo requestInfo = new RequestInfo("register", responseTime, startTimestamp);
		metricsCollector.recordRequest(requestInfo);
		return userVo;
	}
}
//UserControllerProxy使用举例
UserController userController = new UserControllerProxy();

1.2 Dynamic proxy

However, there are still some problems with the code implementation just now. On the one hand, it is necessary to reimplement all the methods in the original class in the proxy class, and attach similar code logic to each method. On the other hand, if there is more than one class of additional functionality to be added, a proxy class needs to be created for each class

If there are 50 original classes to which additional functionality will be added, then 50 corresponding proxy classes will be created. This will cause the number of classes in the project to multiply, increasing the cost of code maintenance. Moreover, the code in each proxy class is a bit like template-style "repetitive" code, which also increases unnecessary development costs. How to solve this problem?

A dynamic proxy can be used to solve this problem. The so-called dynamic proxy (Dynamic Proxy) is not to write a proxy class for each original class in advance, but to dynamically create a proxy class corresponding to the original class at runtime, and then replace the original class with the proxy class in the system. So how to implement dynamic proxy?

If you are familiar with the Java language, implementing a dynamic proxy is a very simple matter. Because the Java language itself has provided the syntax of the dynamic proxy (in fact, the bottom layer of the dynamic proxy relies on the reflection syntax of Java), the code is as follows. Among them, MetricsCollectorProxy, as a dynamic proxy class, dynamically creates a proxy class for each class that needs to collect interface request information

public class MetricsCollectorProxy {
    
    
	private MetricsCollector metricsCollector;
	public MetricsCollectorProxy() {
    
    
		this.metricsCollector = new MetricsCollector();
	}
	public Object createProxy(Object proxiedObject) {
    
    
		Class<?>[] interfaces = proxiedObject.getClass().getInterfaces();
		DynamicProxyHandler handler = new DynamicProxyHandler(proxiedObject);
		return Proxy.newProxyInstance(proxiedObject.getClass().getClassLoader(), interfaces
	}
	private class DynamicProxyHandler implements InvocationHandler {
    
    
		private Object proxiedObject;
		public DynamicProxyHandler(Object proxiedObject) {
    
    
			this.proxiedObject = proxiedObject;
		}
		@Override
		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    
			long startTimestamp = System.currentTimeMillis();
			Object result = method.invoke(proxiedObject, args);
			long endTimeStamp = System.currentTimeMillis();
			long responseTime = endTimeStamp - startTimestamp;
			String apiName = proxiedObject.getClass().getName() + ":" + method.getName();
			RequestInfo requestInfo = new RequestInfo(apiName, responseTime, startTimestamp);
			metricsCollector.recordRequest(requestInfo);
			return result;
		}
	}
}
//MetricsCollectorProxy使用举例
MetricsCollectorProxy proxy = new MetricsCollectorProxy();
IUserController userController = (IUserController) proxy.createProxy(new userController());

In fact, the underlying implementation principle of Spring AOP is based on dynamic proxy. The user configures which classes need to create proxies, and defines which additional functions to execute before and after executing the business code of the original class. Spring creates dynamic proxy objects for these classes and replaces the original class objects in the JVM. The method of the original class that was originally executed in the code is replaced by the method of executing the proxy class, which realizes the purpose of adding additional functions to the original class

1.3 Application scenarios

1. Development of non-functional requirements for business systems

One of the most commonly used application scenarios of the proxy mode is to develop some non-functional requirements in the business system, such as: monitoring, statistics, authentication, current limiting, transactions, idempotence, and logs. Decouple these additional functions from business functions, and put them in the proxy class for unified processing, so that programmers only need to focus on business development

If you are familiar with the Java language and the Spring development framework, this part of the work can be done in the Spring AOP aspect. The underlying implementation principle of Spring AOP is based on dynamic proxy

2. Application of proxy mode in RPC

In fact, the RPC framework can also be seen as a proxy pattern, which is called a remote proxy in GoF's "Design Patterns". Through the remote proxy, details such as network communication and data encoding and decoding are hidden. When the client uses the RPC service, it is like using a local function, without knowing the details of the interaction with the server. In addition, developers of RPC services only need to develop business logic, just like developing functions for local use, and do not need to pay attention to the details of interaction with the client

3. Application of proxy mode in cache

Suppose you want to develop a caching function for an interface request. For some interface requests, if the input parameters are the same, the cached result will be returned directly within the set expiration time without re-processing the logic. For example, to meet the needs of obtaining users' personal information, two interfaces can be developed, one supports caching and the other supports real-time query. For requirements that require real-time data, let it call the real-time query interface, and for requirements that do not require real-time data, let it call the interface that supports caching. So how to implement the caching function of the interface request?

The simplest implementation method is as mentioned above. For each query requirement that needs to support caching, two different interfaces are developed, one supports caching and the other supports real-time query. However, this obviously increases the development cost, and it will make the code look very bloated (the number of interfaces is doubled), and it is not convenient for the centralized management of cache interfaces (adding and deleting cache interfaces), centralized configuration (such as configuring each interface cache expiration time)

For these problems, the proxy mode can come in handy, to be precise, it should be a dynamic proxy. If it is developed based on the Spring framework, the function of interface caching can be completed in the AOP aspect. When the application starts, the interface that needs to support caching and the corresponding caching policy (such as expiration time) are loaded from the configuration file. When the request comes, intercept the request in the AOP aspect. If the request contains a field that supports caching (such as http://...?..&cached=true), it will be obtained from the cache (memory cache or Redis cache, etc.) data returned directly

2. Bridge Design Pattern

2.1 Principle Analysis

There are two different ways of understanding this pattern. Of course, the "purest" way to understand this is the definition of the bridge pattern in GoF's "Design Patterns". After all, these 23 classic design patterns were originally summarized in this book. In GoF's "Design Patterns", the bridge pattern is defined as follows: "Decouple an abstraction from its implementation so that the two can vary independently." Translated into Chinese is: "Decouple abstraction and implementation so that they can change independently."

Regarding the bridge mode, there is another way of understanding in many books and materials: "A class has two (or more) dimensions that change independently, and through combination, these two (or more) dimensions can be Expand independently." Replace the inheritance relationship with the composition relationship to avoid the exponential explosion of the inheritance hierarchy. This way of understanding is very similar to the design principle of "composition is better than inheritance" mentioned earlier, so here we focus on GoF's way of understanding

The definition given by GoF is very short. Based on this sentence alone, it is estimated that few people can understand what it means. So, let's explain it through the example of JDBC driver. The JDBC driver is a classic application of the bridge mode. Let's take a look at how to use the JDBC driver to query the database. The specific code is as follows:

Class.forName("com.mysql.jdbc.Driver");//加载及注册JDBC驱动程序
String url = "jdbc:mysql://localhost:3306/sample_db?user=root&password=your_password"
Connection con = DriverManager.getConnection(url);
Statement stmt = con.createStatement()String query = "select * from test";
ResultSet rs=stmt.executeQuery(query);
while(rs.next()) {
    
    
    rs.getString(1);
    rs.getInt(2);
}

If you want to replace the MySQL database with an Oracle database, you only need to replace com.mysql.jdbc.Driver in the first line of code with oracle.jdbc.driver.OracleDriver. Of course, there is also a more flexible implementation method. You can write the Driver class that needs to be loaded into the configuration file. When the program starts, it will be automatically loaded from the configuration file. In this way, you do not need to modify the code when switching databases. Just modify the configuration file

Whether it is changing the code or changing the configuration, in the project, switching from one database to another requires only a small amount of code changes, or no code changes at all. How is such an elegant database switching achieved?

From the code of the com.mysql.jdbc.Driver class, some relevant source codes are extracted, as follows:

package com.mysql.jdbc;
import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    
    
	static {
    
    
		try {
    
    
			java.sql.DriverManager.registerDriver(new Driver());
		} catch (SQLException E) {
    
    
			throw new RuntimeException("Can't register driver!");
		}
	}
	/**
	* Construct a new driver and register it with DriverManager
	* @throws SQLException if a database error occurs.
	*/
	public Driver() throws SQLException {
    
    
		// Required for Class.forName().newInstance()
	}
}

Combined with the code implementation of com.mysql.jdbc.Driver, it can be found that when the statement Class.forName(“com.mysql.jdbc.Driver”) is executed, two things are actually done. The first thing is to ask the JVM to find and load the specified Driver class, and the second thing is to execute the static code of the class, that is, to register the MySQL Driver to the DriverManager class

Let's take a look at what the DriverManager class is for. The specific code is as follows. After registering the specific Driver implementation class (for example, com.mysql.jdbc.Driver) to DriverManager, all subsequent calls to the JDBC interface will be delegated to the specific Driver implementation class for execution. The Driver implementation classes all implement the same interface (java.sql.Driver), which is why the Driver can be switched flexibly

public class DriverManager {
    
    
	private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
	//...
	static {
    
    
		loadInitialDrivers();
		println("JDBC DriverManager initialized");
	}
	//...
	public static synchronized void registerDriver(java.sql.Driver driver) throws NullPointerException {
    
    
		if (driver != null) {
    
    
			registeredDrivers.addIfAbsent(new DriverInfo(driver));
		} else {
    
    
			throw new NullPointerException();
		}
	}
	public static Connection getConnection(String url, String user, String password) {
    
    
		java.util.Properties info = new java.util.Properties();
		if (user != null) {
    
    
			info.put("user", user);
		}
		if (password != null) {
    
    
			info.put("password", password);
		}
		return (getConnection(url, info, Reflection.getCallerClass()));
	}
	//...
}

The bridge pattern is defined as "decoupling abstraction and implementation so that they can vary independently". Then 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

So in the case of JDBC, what is "abstract"? What is "realization"?

In fact, JDBC itself is equivalent to "abstract". Note that the "abstract" mentioned here does not refer to "abstract classes" or "interfaces", but a set of abstracted "class libraries" that have nothing to do with specific databases. The specific Driver (for example, com.mysql.jdbc.Driver) is equivalent to "implementation". Note that the "implementation" mentioned here does not refer to the "implementation class of the interface", but a set of "class libraries" related to specific databases. JDBC and Driver are independently developed and assembled together through the composition relationship between objects. All logical operations of JDBC are ultimately entrusted to Driver for execution

insert image description here

2.2 Application examples

An example of an API interface monitoring alarm was mentioned before: different types of alarms are triggered according to different alarm rules. The alarm supports multiple notification channels, including: email, SMS, WeChat, and automated voice calls. There are several types of urgency for notifications, including: SEVERE (severe), URGENCY (urgent), NORMAL (normal), TRIVIAL (not important). Different urgency levels correspond to different notification channels. For example, SERVE (serious) level messages will be notified to relevant personnel through "automatic voice calls". Let's first look at the simplest and most direct implementation. The code looks like this:

public enum NotificationEmergencyLevel {
    
    
	SEVERE, URGENCY, NORMAL, TRIVIAL
}
public class Notification {
    
    
	private List<String> emailAddresses;
	private List<String> telephones;
	private List<String> wechatIds;
	public Notification() {
    
    }

	public void setEmailAddress(List<String> emailAddress) {
    
    
		this.emailAddresses = emailAddress;
	}
	public void setTelephones(List<String> telephones) {
    
    
		this.telephones = telephones;
	}
	public void setWechatIds(List<String> wechatIds) {
    
    
		this.wechatIds = wechatIds;
	}

	public void notify(NotificationEmergencyLevel level, String message) {
    
    
		if (level.equals(NotificationEmergencyLevel.SEVERE)) {
    
    
			//...自动语音电话
		} else if (level.equals(NotificationEmergencyLevel.URGENCY)) {
    
    
			//...发微信
		} else if (level.equals(NotificationEmergencyLevel.NORMAL)) {
    
    
			//...发邮件
		} else if (level.equals(NotificationEmergencyLevel.TRIVIAL)) {
    
    
			//...发邮件
		}
	}
}
//在API监控告警的例子中,我们如下方式来使用Notification类:
public class ErrorAlertHandler extends AlertHandler {
    
    
	public ErrorAlertHandler(AlertRule rule, Notification notification) {
    
    
		super(rule, notification);
	}
	@Override
	public void check(ApiStatInfo apiStatInfo) {
    
    
		if (apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi())) {
    
    
			notification.notify(NotificationEmergencyLevel.SEVERE, "...");
		}
	}
}

One of the most obvious problems with the code implementation of the Notification class is that it has a lot of if-else branching logic. In fact, if the code in each branch is not complicated, and there is no possibility of infinite expansion in the later stage (adding more if-else branch judgments), then such a design problem is not big, and there is no need to abandon if-else branching logic

However, the Notification code obviously does not meet this condition. Because the code logic in each if-else branch is more complicated, all the logic for sending notifications are piled up in the Notification class. The more code in a class, the harder it is to read and modify, and the higher the cost of maintenance. Many design patterns are trying to split huge classes into smaller classes, and then assemble them together through a more reasonable structure

For the code of Notification, the sending logic of different channels is separated to form an independent message sending class (MsgSender related class). Among them, the Notification class is equivalent to abstraction, and the MsgSender class is equivalent to implementation. The two can be developed independently and combined arbitrarily through a combination relationship (that is, a bridge). The so-called arbitrary combination means that the corresponding relationship between messages of different urgency and transmission channels is not fixed in the code, but can be specified dynamically (for example, by reading the configuration to obtain the corresponding relationship)

public interface MsgSender {
    
    
	void send(String message);
}
public class TelephoneMsgSender implements MsgSender {
    
    
	private List<String> telephones;
	public TelephoneMsgSender(List<String> telephones) {
    
    
		this.telephones = telephones;
	}
	@Override
	public void send(String message) {
    
    
		//...
	}
}
public class EmailMsgSender implements MsgSender {
    
    
	// 与TelephoneMsgSender代码结构类似,所以省略...
}
public class WechatMsgSender implements MsgSender {
    
    
	// 与TelephoneMsgSender代码结构类似,所以省略...
}
public abstract class Notification {
    
    
	protected MsgSender msgSender;
	public Notification(MsgSender msgSender) {
    
    
		this.msgSender = msgSender;
	}
	public abstract void notify(String message);
}

public class SevereNotification extends Notification {
    
    
	public SevereNotification(MsgSender msgSender) {
    
    
		super(msgSender);
	}
	@Override
	public void notify(String message) {
    
    
		msgSender.send(message);
	}
}
public class UrgencyNotification extends Notification {
    
    
	// 与SevereNotification代码结构类似,所以省略...
}
public class NormalNotification extends Notification {
    
    
	// 与SevereNotification代码结构类似,所以省略...
}
public class TrivialNotification extends Notification {
    
    
	// 与SevereNotification代码结构类似,所以省略...
}

3. Decorator Pattern (Decorator Design Pattern)

3.1 Java IO classes

The Java IO class library is very large and complex, with dozens of classes responsible for reading and writing IO data. If the Java IO class is classified, it can be divided into four categories from the following two dimensions. Specifically as follows:

insert image description here

Based on these four parent classes, Java IO has extended many subclasses for different reading and writing scenarios. Specifically as follows:

insert image description here

If you want to open the file test.txt, read data from it. Among them, InputStream is an abstract class, and FileInputStream is a subclass specially used to read file streams. BufferedInputStream is a data reading class that supports caching, which can improve the efficiency of data reading

InputStream in = new FileInputStream("/user/wangzheng/test.txt");
InputStream bin = new BufferedInputStream(in);
byte[] data = new byte[128];
while (bin.read(data) != -1) {
    
    
	//...
}

At first glance at the above code, you will feel that the usage of Java IO is more troublesome. You need to create a FileInputStream object first, and then pass it to the BufferedInputStream object for use. Why doesn't Java IO design a BufferedFileInputStream class that inherits FileInputStream and supports caching? In this way, as in the following code, you can directly create a BufferedFileInputStream class object, open the file to read data, isn't it easier to use?

InputStream bin = new BufferedFileInputStream("/user/wangzheng/test.txt");
byte[] data = new byte[128];
while (bin.read(data) != -1) {
    
    
    //...
}

3.2 Design Scheme Based on Inheritance

If InputStream has only one subclass FileInputStream, it is acceptable to design a grandson class BufferedFileInputStream on the basis of FileInputStream, after all, the inheritance structure is relatively simple. But in fact, there are many subclasses that inherit InputStream. Need to subclass each InputStream, and then continue to derive subclasses that support cached reading

In addition to supporting cache reading, if we need to enhance other aspects of the function, such as the DataInputStream class below, it supports reading data according to basic data types (int, boolean, long, etc.)

FileInputStream in = new FileInputStream("/user/wangzheng/test.txt");
DataInputStream din = new DataInputStream(in);
int data = din.readInt();

In this case, if you continue to implement it in the way of inheritance, you need to continue to derive DataFileInputStream, DataPipedInputStream and other classes. If you need a class that supports both caching and reading data according to the basic type, then you need to continue to derive n more classes such as BufferedDataFileInputStream, BufferedDataPipedInputStream, etc. This is just adding two enhancements. If you need to add more enhancements, it will lead to a combination explosion, the class inheritance structure becomes extremely complicated, and the code is neither easy to expand nor maintain

3.3 Design scheme based on decorator pattern

This design idea of ​​Java IO is shown here, and the code is simplified:

public abstract class InputStream {
    
    
	//...
	public int read(byte b[]) throws IOException {
    
    
		return read(b, 0, b.length);
	}
	public int read(byte b[], int off, int len) throws IOException {
    
    
		//...
	}
	public long skip(long n) throws IOException {
    
    
		//...
	}
	public int available() throws IOException {
    
    
		return 0;
	}
	public void close() throws IOException {
    
    }
	public synchronized void mark(int readlimit) {
    
    }
	public synchronized void reset() throws IOException {
    
    
		throw new IOException("mark/reset not supported");
	}
	public boolean markSupported() {
    
    
		return false;
	}
}
public class BufferedInputStream extends InputStream {
    
    
	protected volatile InputStream in;
	protected BufferedInputStream(InputStream in) {
    
    
		this.in = in;
	}
	//...实现基于缓存的读数据接口...
}
public class DataInputStream extends InputStream {
    
    
	protected volatile InputStream in;
	protected DataInputStream(InputStream in) {
    
    
		this.in = in;
	}
	//...实现读取基本类型数据的接口
}

After reading the above code, you may ask, is the decorator pattern simply "replacing inheritance with composition"? of course not. From the design of Java IO, the decorator pattern has two special features compared to the simple composition relationship

1. The decorator class and the original class inherit the same parent class, so that multiple decorator classes can be "nested" on the original class

For example, the following piece of code nests two decorator classes for FileInputStream: BufferedInputStream and DataInputStream, allowing it to support both cached reading and reading data according to basic data types

InputStream in = new FileInputStream("/user/wangzheng/test.txt");
InputStream bin = new BufferedInputStream(in);
DataInputStream din = new DataInputStream(bin);
int data = din.readInt();

2. The decorator class is an enhancement to the function, which is also an important feature of the decorator pattern application scenario

In fact, there are many design patterns that conform to the code structure of "composition relationship", such as the proxy pattern, bridge pattern, and the current decorator pattern mentioned earlier. Although their code structures are similar, the intent of each design pattern is different. Take the similar proxy mode and decorator mode as an example. 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();
		// 功能增强代码
	}
}

In fact, if you look at the source code of JDK, you will find that BufferedInputStream and DataInputStream are not inherited from InputStream, but another class called FilterInputStream. Then what is the design intent for introducing such a class?

Look again at the code for the BufferedInputStream class. InputStream is an abstract class rather than an interface, and most of its functions (such as read(), available()) have default implementations. It stands to reason that only those functions that need to increase the caching function need to be re-implemented in the BufferedInputStream class. That's it, other functions inherit the default implementation of InputStream. But in practice, it doesn't work

Even for functions that do not need to increase the caching function, BufferedInputStream still has to reimplement it, simply wrapping the function call to the InputStream object. A specific code example is as follows:

public class BufferedInputStream extends InputStream {
    
    
	protected volatile InputStream in;
	protected BufferedInputStream(InputStream in) {
    
    
		this.in = in;
	}
	// f()函数不需要增强,只是重新调用一下InputStream in对象的f()
	public void f() {
    
    
		in.f();
	}
}

If it is not reimplemented, the BufferedInputStream class will not be able to delegate the task of finally reading data to the passed InputStream object. Because assuming we don’t rewrite the method f(), and then internally call the passed InputStream object f(), when using f()the method only the topmost f()method will be called. If there are multiple decorators, there will be problems, and the chain will be interrupted ( The overall call process is actually a chain call). Similarly, if we rewrite f()the method , but internally forget to call f()the method of the InputStream object passed in, that is, the last InputStream object, chain interruption will also occur

In fact, DataInputStream also has the same problem as BufferedInputStream. In order to avoid code duplication, Java IO abstracts a decorator parent class FilterInputStream, the code implementation is as follows. All decorator classes of InputStream (BufferedInputStream, DataInputStream) inherit from this decorator parent class. In this way, the decorator class only needs to implement the method it needs to enhance, and other methods inherit the default implementation of the decorator parent class

public class FilterInputStream extends InputStream {
    
    
	protected volatile InputStream in;

	protected FilterInputStream(InputStream in) {
    
    
		this.in = in;
	}
	public int read() throws IOException {
    
    
		return in.read();
	}
	public int read(byte b[]) throws IOException {
    
    
		return read(b, 0, b.length);
	}
	public int read(byte b[], int off, int len) throws IOException {
    
    
		return in.read(b, off, len);
	}
	public long skip(long n) throws IOException {
    
    
		return in.skip(n);
	}
	public int available() throws IOException {
    
    
		return in.available();
	}
	public void close() throws IOException {
    
    
		in.close();
	}
	public synchronized void mark(int readlimit) {
    
    
		in.mark(readlimit);
	}
	public synchronized void reset() throws IOException {
    
    
		in.reset();
	}
	public boolean markSupported() {
    
    
		return in.markSupported();
	}
}

Here is a source code of BufferedInputStream's read()method call, omitting the logic part

public synchronized int read() throws IOException {
    
    
        // 省略其他
        fill();
}

private void fill() throws IOException {
    
    
        // 省略其他
        int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
}

private InputStream getInIfOpen() throws IOException {
    
    
        // in 就是通过构造函数传递进来的 InputStream 对象
        InputStream input = in;
        if (input == null)
            throw new IOException("Stream closed");
        return input;
}

It can be seen that after BufferedInputStream rewrites read()the method , getInIfOpen()it obtains the passed InputStream in through this method, and then in.read()calls read()the method of the InputStream object passed in through . But from this method, the package call read()of is completely useless

public class FilterInputStream extends InputStream {
    
    
	protected volatile InputStream in;

	protected FilterInputStream(InputStream in) {
    
    
		this.in = in;
	}
	public int read() throws IOException {
    
    
		return in.read();
	}
	public int read(byte b[]) throws IOException {
    
    
		return read(b, 0, b.length);
	}
	public int read(byte b[], int off, int len) throws IOException {
    
    
		return in.read(b, off, len);
	}
        // 省略其他
}

But as mentioned above, if there is no rewriting method, the call must be wrapped, otherwise there will be a chain break. For code reuse, scalability, including avoiding missed calls, FilterInputStream here implements the wrapped calls of all InputStream methods. If the subclass (such as BufferedInputStream) is rewritten, use the subclass, if the subclass is not rewritten, use FilterInputStream (this is actually inherited knowledge)

A complete example is as follows:

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

4. Adapter Design Pattern

4.1 Principle and Implementation

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. For this mode, there is an example that is often used to explain it, that is, the USB adapter acts as an adapter, and the two incompatible interfaces can work together through the adapter.

The principle is very simple, let's look at its code implementation. 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 specific code implementation is as follows. Among them, ITarget represents the interface definition to be transformed into. Adaptee is a set of interfaces that are not compatible with the definition of ITarget interface. Adaptor converts Adaptee into a set of interfaces that conform to the definition of ITarget interface.

// 类适配器: 基于继承
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,这是跟对象适配器最大的不同点
}
// 对象适配器:基于组合
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

4.2 Application scenarios

Generally speaking, the adapter mode can be regarded as a "compensation mode" to remedy design defects. Applying this model is considered a "helpless move". If in the early stage of design, the problem of interface incompatibility can be coordinated and avoided, then this mode will have no chance of application

1. Package defective interface design

Assuming that the dependent external system has defects in interface design (such as containing a large number of static methods), the introduction will affect the testability of our own code. In order to isolate design defects, it is hoped to re-encapsulate the interface provided by the external system to abstract a better interface design. At this time, the adapter mode can be used

public class CD {
    
     //这个类来自外部sdk,我们无权修改它的代码
	//...
	public static void staticFunction1() {
    
    
		//...
	}
	public void uglyNamingFunction2() {
    
    
		//...
	}
	public void tooManyParamsFunction3(int paramA, int paramB, ...) {
    
    
		//...
	}
	public void lowPerformanceFunction4() {
    
    
		//...
	}
}

// 使用适配器模式进行重构
public interface ITarget {
    
    
	void function1();
	void function2();
	void fucntion3(ParamsWrapperDefinition paramsWrapper);
	void function4();
	//...
}
// 注意:适配器类的命名不一定非得末尾带Adaptor
public class CDAdaptor extends CD implements ITarget {
    
    
	//...
	public void function1() {
    
    
		super.staticFunction1();
	}
	public void function2() {
    
    
		super.uglyNamingFucntion2();
	}
	public void function3(ParamsWrapperDefinition paramsWrapper) {
    
    
		super.tooManyParamsFunction3(paramsWrapper.getParamA(), ...);
	}
	public void function4() {
    
    
		//...reimplement it...
	}
}

2. Unify the interface design of multiple classes

The realization of a function depends on multiple external systems (or classes). Through the adapter mode, adapt their interfaces to a unified interface definition, and then use polymorphic features to reuse code logic

Assuming that our system needs to filter sensitive words in the text content entered by users, in order to improve the recall rate of filtering, we have introduced a variety of third-party sensitive word filtering systems to filter the content entered by users in turn to filter out as many sensitive words as possible. word. However, the filtering interface provided by each system is different. This means that it is impossible to reuse a set of logic to call various systems. At this time, you can use the adapter mode to adapt the interfaces of all systems to a unified interface definition, so that the code that calls sensitive word filtering can be reused

// A敏感词过滤系统提供的接口
public class ASensitiveWordsFilter {
    
    
	//text是原始文本,函数输出用***替换敏感词之后的文本
	public String filterSexyWords(String text) {
    
    
		// ...
	}
	public String filterPoliticalWords(String text) {
    
    
		// ...
	}
}
// B敏感词过滤系统提供的接口
public class BSensitiveWordsFilter {
    
    
	public String filter(String text) {
    
    
		//...
	}
}
// C敏感词过滤系统提供的接口
public class CSensitiveWordsFilter {
    
    
	public String filter(String text, String mask) {
    
    
		//...
	}
}
// 未使用适配器模式之前的代码:代码的可测试性、扩展性不好
public class RiskManagement {
    
    
	private ASensitiveWordsFilter aFilter = new ASensitiveWordsFilter();
	private BSensitiveWordsFilter bFilter = new BSensitiveWordsFilter();
	private CSensitiveWordsFilter cFilter = new CSensitiveWordsFilter();
	public String filterSensitiveWords(String text) {
    
    
		String maskedText = aFilter.filterSexyWords(text);
		maskedText = aFilter.filterPoliticalWords(maskedText);
		maskedText = bFilter.filter(maskedText);
		maskedText = cFilter.filter(maskedText, "***");
		return maskedText;
	}
}
// 使用适配器模式进行改造
public interface ISensitiveWordsFilter {
    
     // 统一接口定义
	String filter(String text);
}
public class ASensitiveWordsFilterAdaptor implements ISensitiveWordsFilter {
    
    
	private ASensitiveWordsFilter aFilter;
	public String filter(String text) {
    
    
		String maskedText = aFilter.filterSexyWords(text);
		maskedText = aFilter.filterPoliticalWords(maskedText);
		return maskedText;
	}
}
//...省略BSensitiveWordsFilterAdaptor、CSensitiveWordsFilterAdaptor...

// 扩展性更好,更加符合开闭原则,如果添加一个新的敏感词过滤系统,
// 这个类完全不需要改动;而且基于接口而非实现编程,代码的可测试性更好。
public class RiskManagement {
    
    
	private List<ISensitiveWordsFilter> filters = new ArrayList<>();
	public void addSensitiveWordsFilter(ISensitiveWordsFilter filter) {
    
    
		filters.add(filter);
	}
	public String filterSensitiveWords(String text) {
    
    
		String maskedText = text;
		for (ISensitiveWordsFilter filter : filters) {
    
    
			maskedText = filter.filter(maskedText);
		}
		return maskedText;
	}
}

3. Replace dependent external systems

When replacing an external system that a project depends on with another external system, using the adapter pattern can reduce code changes. A specific code example is as follows:

// 外部系统A
public interface IA {
    
    
	//...
	void fa();
}
public class A implements IA {
    
    
	//...
	public void fa() {
    
    
		//...
	}
}
// 在我们的项目中,外部系统A的使用示例
public class Demo {
    
    
	private IA a;
	public Demo(IA a) {
    
    
		this.a = a;
	}
	//...
}
Demo d = new Demo(new A());

// 将外部系统A替换成外部系统B
public class BAdaptor implemnts IA {
    
    
	private B b;
	public BAdaptor(B b) {
    
    
		this.b= b;
	}
	public void fa() {
    
    
		//...
		b.fb();
	}
}
// 借助BAdaptor,Demo的代码中,调用IA接口的地方都无需改动,
// 只需要将BAdaptor如下注入到Demo即可。
Demo d = new Demo(new BAdaptor(new B()));

4. Compatible with the old version interface

When upgrading the version, for some interfaces that are to be discarded, they are not deleted directly, but are temporarily reserved and marked as deprecated, and the internal implementation logic is delegated to a new interface implementation. The advantage of this is that it allows projects that use it to have a transition period, rather than forcing code changes

JDK1.0 contains a class Enumeration that traverses collection containers. JDK2.0 refactored this class, renamed it Iterator class, and optimized its code implementation. But considering that if Enumeration is directly deleted from JDK2.0, if the project using JDK1.0 is switched to JDK2.0, the code will fail to compile. In order to avoid this from happening, it is necessary to modify all the places where Enumeration is used in the project to use Iterator

A single project to replace Enumeration to Iterator is barely acceptable. However, there are too many projects developed in Java, and it is obviously unreasonable for all projects to compile and report errors without code modification after a JDK upgrade. This is what is often referred to as an incompatible upgrade. In order to be compatible with the old code that uses a lower version of JDK, you can temporarily keep the Enumeration class and replace its implementation with a direct call to Itertor. A code example is as follows:

public class Collections {
    
    
	public static Emueration emumeration(final Collection c) {
    
    
		return new Enumeration() {
    
    
			Iterator i = c.iterator();
			public boolean hasMoreElments() {
    
    
				return i.hashNext();
			}
			public Object nextElement() {
    
    
				return i.next():
			}
		}
	}
}

5. Adapt to data in different formats

As mentioned earlier, the adapter mode is mainly used for interface adaptation. In fact, it can also be used for adaptation between data in different formats. For example, unify credit data in different formats pulled from different credit systems into the same format for easy storage and use. For another example, in Java can Arrays.asList()also be regarded as a data adapter, which converts the data of the array type into the collection container type

List stooges = Arrays.asList(“Larry”, “Moe”, “Curly”);

4.3 Application of Adapter Pattern in Java Logging

There are many logging frameworks in Java, and they are often used to print log information in project development. Among them, log4j, logback, and JUL (java.util.logging) provided by JDK and JCL (Jakarta Commons Logging) provided by Apache are more commonly used.

Most logging frameworks provide similar functions, such as printing logs according to different levels (debug, info, warn, error...), but they do not implement a unified interface. This may be mainly due to historical reasons. Unlike JDBC, it has formulated an interface specification for database operations from the very beginning.

If you just develop a project for your own use, you can use any log framework, just choose one of log4j and logback. However, if you develop a component, framework, class library, etc. that are integrated into other systems, then the choice of logging framework is not so random

For example, a certain component used in the project uses log4j to print logs, while our project itself uses logback. After introducing components into the project, our project is equivalent to having two sets of log printing frameworks. Each logging framework has its own unique configuration. Therefore, we need to write different configuration files for each log framework (for example, the file address of the log storage, the format of the print log). If multiple components are introduced and each component uses a different log framework, the management of the log itself becomes very complicated. So, in order to solve this problem, we need a unified log printing framework

If you are doing Java development, then the Slf4j log framework is definitely no stranger. It is equivalent to the JDBC specification and provides a set of unified interface specifications for printing logs. However, it only defines the interface and does not provide a specific implementation. It needs to be used with other log frameworks (log4j, logback...)

Not only that, Slf4j appeared later than JUL, JCL, log4j and other log frameworks, so it is impossible for these log frameworks to sacrifice version compatibility and transform the interface to conform to the Slf4j interface specification. Slf4j also considers this problem in advance, so it not only provides a unified interface definition, but also provides adapters for different logging frameworks. The interfaces of different log frameworks are encapsulated twice, and adapted into a unified Slf4j interface definition. A specific code example is as follows:

// slf4j统一的接口定义
package org.slf4j;
public interface Logger {
    
    
	public boolean isTraceEnabled();
	public void trace(String msg);
	public void trace(String format, Object arg);
	public void trace(String format, Object arg1, Object arg2);
	public void trace(String format, Object[] argArray);
	public void trace(String msg, Throwable t);
	public boolean isDebugEnabled();
	public void debug(String msg);
	public void debug(String format, Object arg);
	public void debug(String format, Object arg1, Object arg2)
	public void debug(String format, Object[] argArray)
	public void debug(String msg, Throwable t);
	//...省略info、warn、error等一堆接口
}

// log4j日志框架的适配器
// Log4jLoggerAdapter实现了LocationAwareLogger接口,
// 其中LocationAwareLogger继承自Logger接口,
// 也就相当于Log4jLoggerAdapter实现了Logger接口。
package org.slf4j.impl;
public final class Log4jLoggerAdapter extends MarkerIgnoringBase
	implements LocationAwareLogger, Serializable {
    
    
	final transient org.apache.log4j.Logger logger; // log4j
	public boolean isDebugEnabled() {
    
    
		return logger.isDebugEnabled();
	}
	public void debug(String msg) {
    
    
		logger.log(FQCN, Level.DEBUG, msg, null);
	}
	public void debug(String format, Object arg) {
    
    
		if (logger.isDebugEnabled()) {
    
    
			FormattingTuple ft = MessageFormatter.format(format, arg);
			logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());
		}
	}
	public void debug(String format, Object arg1, Object arg2) {
    
    
		if (logger.isDebugEnabled()) {
    
    
			FormattingTuple ft = MessageFormatter.format(format, arg1, arg2);
			logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());
		}
	}
	public void debug(String format, Object[] argArray) {
    
    
		if (logger.isDebugEnabled()) {
    
    
			FormattingTuple ft = MessageFormatter.arrayFormat(format, argArray);
			logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());
		}
	}
	public void debug(String msg, Throwable t) {
    
    
		logger.log(FQCN, Level.DEBUG, msg, t);
	}
	//...省略一堆接口的实现...
}

Therefore, when developing business systems or developing frameworks and components, we uniformly use the interface provided by Slf4j to write the code for printing logs. Which log framework implementation (log4j, logback...) to use can be dynamically specified ( Using Java's SPI technology), you only need to import the corresponding SDK into the project

If some old projects do not use Slf4j, but directly use such as JCL to print logs, what should we do if we want to replace them with other logging frameworks, such as log4j? In fact, Slf4j not only provides adapters from other log frameworks to Slf4j, but also provides reverse adapters, that is, adaptations from Slf4j to other log frameworks. You can switch JCL to Slf4j first, and then switch Slf4j to log4j. After two adapter conversions, log4j can be successfully switched to logback

4.4 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

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

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

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

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

5. Facade Design Pattern

If your usual work involves interface development, have you encountered any problems with interface granularity?

In order to ensure the reusability (or versatility) of the interface, we need to design the interface as fine-grained as possible and have a single responsibility. However, if the granularity of the interface is too small, when the user of the interface develops a business function, It will result in the need to call n more fine-grained interfaces to complete. The caller will definitely complain that the interface is not easy to use

On the contrary, if the granularity of the interface is designed too large, an interface returns n more data and needs to do more than n things, the interface will not be universal enough and the reusability will not be good. Interfaces cannot be reused, so different interfaces need to be developed to meet the business needs of different callers, which will lead to infinite expansion of the system interface. So how to solve the contradiction between interface reusability (universality) and ease of use?

5.1 Principle and Implementation

Facade mode, also called appearance mode, the full English name is 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

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

At this time, there may be such doubts that it is not a big problem for system B to directly call a, b, and d. Why do we need to provide an interface x that wraps a, b, and d?

Assume that system A just mentioned is a backend server, and system B is an App client. The App client obtains data through the interface provided by the backend server. The communication between the App and the server is through the mobile network, and the network communication takes a lot of time. In order to improve the response speed of the App, we need to minimize the number of network communications between the App and the server.

Assume that to complete a certain business function (such as displaying a certain page information), it is necessary to call the three interfaces a, b, and d in sequence. Due to the characteristics of its own business, it does not support calling these three interfaces concurrently.

If you find that the response speed of the App client is relatively slow, after troubleshooting, you will find that it is because too many interfaces call too much network communication. For this situation, you can use the facade mode to let the back-end server provide an interface x that wraps the three interface calls of a, b, and d. The App client calls the interface x once to obtain all the desired data, reducing the number of network communications from 3 times to 1 time, which improves the response speed of the App

The example given here is just one of the intentions of applying the facade pattern, which is to solve performance problems. In fact, in different application scenarios, the intention of using the facade pattern is also different

5.2 Application scenarios

In the definition given by GoF, it is mentioned that "the facade mode makes the subsystem easier to use". In fact, in addition to solving the problem of usability, it can also solve many other problems. In addition, it should be emphasized that the "subsystem (subsystem)" in the definition of the facade pattern can also be understood in many ways. It can be either a complete system or more fine-grained classes or modules

1. Solve the usability problem

The facade pattern can be used to encapsulate the underlying implementation of the system, hide the complexity of the system, and provide a set of simpler, easier-to-use, higher-level interfaces. For example, the Linux system call function can be regarded as a kind of "facade". It is a set of "special" programming interfaces exposed to developers by the Linux operating system, which encapsulates the underlying more basic Linux kernel calls. For another example, the Linux Shell command can actually be regarded as a facade mode application. It continues to encapsulate system calls, providing more friendly and simple commands, allowing us to interact with the operating system directly by executing commands

Many design principles, ideas, and patterns are interlinked, and they are expressions of the same principle from different angles. In fact, from the perspective of hiding the complexity of implementation and providing an easier-to-use interface, the facade pattern is somewhat similar to Dimit's law (the principle of least knowledge) and the principle of interface isolation: two interactive systems only expose limited the necessary interface. In addition, the facade mode is somewhat similar to the design idea of ​​encapsulation and abstraction mentioned earlier, providing a more abstract interface and encapsulating the underlying implementation details

2. Solve performance problems

Regarding the use of facade mode to solve performance problems, I have already talked about it. By replacing multiple interface calls with one facade interface call, network communication costs are reduced and the response speed of the App client is improved. Now let's discuss such a question: From the perspective of code implementation, how should we organize facade interfaces and non-facade interfaces?

If there are not many facade interfaces, it can be put together with non-facade interfaces without special marking, and can be used as ordinary interfaces. If there are many facade interfaces, a new layer can be abstracted on top of the existing interfaces, and the facade interfaces can be specially placed to distinguish them from the original interface layer in terms of class and package naming. If there are too many facade interfaces, and many of them span multiple subsystems, you can put the facade interface into a new subsystem

3. Solve distributed transaction problems

In a financial system, there are two business domain models, user and wallet. These two business domain models both expose a series of interfaces, such as interfaces for adding, deleting, modifying and checking users, and interfaces for adding, deleting, modifying and checking wallets. Suppose there is such a business scenario: when a user registers, not only will the user be created (in the User table of the database), but also a wallet will be created for the user (in the Wallet table of the database)

For such a simple business requirement, it can be completed by sequentially calling the user creation interface and the wallet creation interface. However, user registration needs to support transactions, that is to say, the two operations of creating users and wallets either succeed or both fail, and one cannot succeed and the other fail

It is difficult to support two interface calls to be executed in one transaction, which involves distributed transaction issues. Although it can be solved by introducing a distributed transaction framework or an after-event compensation mechanism, the code implementation is relatively complicated. The simplest solution is to use the database transaction or the transaction provided by the Spring framework (if it is Java language), and execute the two SQL operations of creating a user and creating a wallet in one transaction. This requires two SQL operations to be completed in one interface. Therefore, we can learn from the idea of ​​the facade mode and design a new interface that wraps these two operations, so that the new interface can execute two SQL operations in one transaction.

6. Composite Design Pattern

The composition mode is completely different from the "composition relationship (to assemble two classes through composition)" in the object-oriented design mentioned earlier. 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

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

6.1 Principle and Implementation

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

Suppose we have such a requirement: design a class to represent the directory in the file system, which can conveniently realize the following functions:

  • Dynamically add and delete subdirectories or files under a certain directory
  • Count the number of files in the specified directory
  • Count the total size of files in the specified directory

The skeleton code is shown below, and the core logic has not been implemented. In the following code implementation, the files and directories are represented by the FileSystemNode class uniformly, and are distinguished by the isFile attribute

public class FileSystemNode {
    
    
	private String path;
	private boolean isFile;
	private List<FileSystemNode> subNodes = new ArrayList<>();
	public FileSystemNode(String path, boolean isFile) {
    
    
		this.path = path;
		this.isFile = isFile;
	}
	public int countNumOfFiles() {
    
    
		// TODO:...
	}
	public long countSizeOfFiles() {
    
    
		// TODO:...
	}
	public String getPath() {
    
    
		return path;
	}
	public void addSubNode(FileSystemNode fileOrDir) {
    
    
		subNodes.add(fileOrDir);
	}
	public void removeSubNode(FileSystemNode fileOrDir) {
    
    
		int size = subNodes.size();
		int i = 0;
		for (; i < size; ++i) {
    
    
			if (subNodes.get(i).getPath().equalsIgnoreCase(fileOrDir.getPath())) {
    
    
				break;
			}
		}
		if (i < size) {
    
    
			subNodes.remove(i);
		}
	}
}

countNumOfFiles()And countSizeOfFiles()these two functions, in fact, this is the recursive traversal algorithm of the tree. For files, you can directly return the number of files (return 1) or size. For a directory, traverse each subdirectory or file in the directory, recursively calculate their number or size, and then sum, which is the number and size of files in this directory

public int countNumOfFiles() {
    
    
	if (isFile) {
    
    
		return 1;
	}
	int numOfFiles = 0;
	for (FileSystemNode fileOrDir : subNodes) {
    
    
		numOfFiles += fileOrDir.countNumOfFiles();
	}
	return numOfFiles;
}
public long countSizeOfFiles() {
    
    
	if (isFile) {
    
    
		File file = new File(path);
		if (!file.exists()) return 0;
		return file.length();
	}
	long sizeofFiles = 0;
	for (FileSystemNode fileOrDir : subNodes) {
    
    
		sizeofFiles += fileOrDir.countSizeOfFiles();
	}
	return sizeofFiles;
}

Purely from the perspective of function realization, there is no problem with the above code, and the desired function has been realized. However, if you are developing a large-scale system, from the perspective of scalability (files or directories may correspond to different operations), business modeling (files and directories are two concepts from business), code readability (files and directories Differentiated treatment is more in line with people's cognition of business), it is best to differentiate the design of files and directories, defined as two classes of File and Directory

public abstract class FileSystemNode {
    
    
	protected String path;
	public FileSystemNode(String path) {
    
    
		this.path = path;
	}
	public abstract int countNumOfFiles();
	public abstract long countSizeOfFiles();
	public String getPath() {
    
    
		return path;
	}
}
public class File extends FileSystemNode {
    
    
	public File(String path) {
    
    
		super(path);
	}
	@Override
	public int countNumOfFiles() {
    
    
		return 1;
	}
	@Override
	public long countSizeOfFiles() {
    
    
		java.io.File file = new java.io.File(path);
		if (!file.exists()) return 0;
		return file.length();
	}
}
public class Directory extends FileSystemNode {
    
    
	private List<FileSystemNode> subNodes = new ArrayList<>();
	public Directory(String path) {
    
    
		super(path);
	}
	@Override
	public int countNumOfFiles() {
    
    
		int numOfFiles = 0;
		for (FileSystemNode fileOrDir : subNodes) {
    
    
			numOfFiles += fileOrDir.countNumOfFiles();
		}
		return numOfFiles;
	}
	@Override
	public long countSizeOfFiles() {
    
    
		long sizeofFiles = 0;
		for (FileSystemNode fileOrDir : subNodes) {
    
    
			sizeofFiles += fileOrDir.countSizeOfFiles();
		}
		return sizeofFiles;
	}
	public void addSubNode(FileSystemNode fileOrDir) {
    
    
		subNodes.add(fileOrDir);
	}
	public void removeSubNode(FileSystemNode fileOrDir) {
    
    
		int size = subNodes.size();
		int i = 0;
		for (; i < size; ++i) {
    
    
			if (subNodes.get(i).getPath().equalsIgnoreCase(fileOrDir.getPath())) {
    
    
				break;
			}
		}
		if (i < size) {
    
    
			subNodes.remove(i);
		}
	}
}

The file and directory classes are designed, now let's see how to use them to represent the directory tree structure in a file system. A specific code example is as follows:

public class Demo {
    
    
	public static void main(String[] args) {
    
    
		/**
		* /
		* /wz/
		* /wz/a.txt
		* /wz/b.txt
		* /wz/movies/
		* /wz/movies/c.avi
		* /xzg/
		* /xzg/docs/
		* /xzg/docs/d.txt
		*/
		Directory fileSystemTree = new Directory("/");
		Directory node_wz = new Directory("/wz/");
		Directory node_xzg = new Directory("/xzg/");
		fileSystemTree.addSubNode(node_wz);
		fileSystemTree.addSubNode(node_xzg);
		File node_wz_a = new File("/wz/a.txt");
		File node_wz_b = new File("/wz/b.txt");
		Directory node_wz_movies = new Directory("/wz/movies/");
		node_wz.addSubNode(node_wz_a);
		node_wz.addSubNode(node_wz_b);
		node_wz.addSubNode(node_wz_movies);
		File node_wz_movies_c = new File("/wz/movies/c.avi");
		node_wz_movies.addSubNode(node_wz_movies_c);
		Directory node_xzg_docs = new Directory("/xzg/docs/");
		node_xzg.addSubNode(node_xzg_docs);
		File node_xzg_docs_d = new File("/xzg/docs/d.txt");
		node_xzg_docs.addSubNode(node_xzg_docs_d);
		System.out.println("/ files num:" + fileSystemTree.countNumOfFiles());
		System.out.println("/wz/ files num:" + node_wz.countNumOfFiles());
	}
}

In contrast to this example, let's look at the definition of the combination mode again: "Organize a set of objects (files and directories) into a tree structure to represent a 'part-whole' hierarchy (the nesting of directories and subdirectories) structure). The combination mode allows the client to unify the processing logic (recursive traversal) of a single object (file) and a combination object (directory)." In fact, the design idea of ​​the combination mode just mentioned is not so much a design Mode, rather, is an abstraction of data structures and algorithms for business scenarios. Among them, data can be represented as a data structure such as a tree, and business requirements can be realized through a recursive traversal algorithm on the tree

6.2 Application scenarios

Suppose you are developing an OA system (office automation system). The company's organizational structure contains two data types, department and employee. Among them, a department can contain sub-departments and employees. The table structure in the database is as follows:

insert image description here

Now it is hoped to build a personnel structure diagram of the entire company (department, sub-department, employee affiliation) in memory, and provide an interface to calculate the salary cost of the department (the salary sum of all employees belonging to this department)

Departments contain sub-departments and employees, which is a nested structure that can be represented as a tree data structure. The need to calculate the salary expenditure of each department can also be realized through the traversal algorithm on the tree. So, from this point of view, this application scenario can be designed and implemented using the composite pattern

The sample code is as follows, where HumanResource is the parent class abstracted from Department and Employee to unify the processing logic of salary. The code in the demo is responsible for reading data from the database and building the organizational chart in memory

public abstract class HumanResource {
    
    
	protected long id;
	protected double salary;
	public HumanResource(long id) {
    
    
		this.id = id;
	}
	public long getId() {
    
    
		return id;
	}
	public abstract double calculateSalary();
}
public class Employee extends HumanResource {
    
    
	public Employee(long id, double salary) {
    
    
		super(id);
		this.salary = salary;
	}
	@Override
	public double calculateSalary() {
    
    
		return salary;
	}
}
public class Department extends HumanResource {
    
    
	private List<HumanResource> subNodes = new ArrayList<>();
	public Department(long id) {
    
    
		super(id);
	}
	@Override
	public double calculateSalary() {
    
    
		double totalSalary = 0;
		for (HumanResource hr : subNodes) {
    
    
			totalSalary += hr.calculateSalary();
		}
		this.salary = totalSalary;
		return totalSalary;
	}
	public void addSubNode(HumanResource hr) {
    
    
		subNodes.add(hr);
	}
}
// 构建组织架构的代码
public class Demo {
    
    
	private static final long ORGANIZATION_ROOT_ID = 1001;
	private DepartmentRepo departmentRepo; // 依赖注入
	private EmployeeRepo employeeRepo; // 依赖注入
	public void buildOrganization() {
    
    
		Department rootDepartment = new Department(ORGANIZATION_ROOT_ID);
		buildOrganization(rootDepartment);
	}
	private void buildOrganization(Department department) {
    
    
		List<Long> subDepartmentIds = departmentRepo.getSubDepartmentIds(department);
		for (Long subDepartmentId : subDepartmentIds) {
    
    
		Department subDepartment = new Department(subDepartmentId);
			department.addSubNode(subDepartment);
			buildOrganization(subDepartment);
		}
		List<Long> employeeIds = employeeRepo.getDepartmentEmployeeIds(department.getId());
		for (Long employeeId : employeeIds) {
    
    
		double salary = employeeRepo.getEmployeeSalary(employeeId);
			department.addSubNode(new Employee(employeeId, salary));
		}
	}
}

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)."

7. Flyweight Design Pattern

7.1 Principle and Implementation

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

Suppose we are developing a board game (such as chess). There are thousands of "rooms" in a game hall, and each room corresponds to a chess game. The chess game should save the data of each chess piece, such as: chess piece type (general, phase, soldier, cannon, etc.), chess piece color (red square, black square), and the position of the chess piece in the game. Using these data, a complete board can be displayed to the player. The specific code is as follows. Among them, the ChessPiece class represents a chess piece, and the ChessBoard class represents a chess game, which stores the information of 30 chess pieces in chess

public class ChessPiece {
    
    //棋子
	private int id;
	private String text;
	private Color color;
	private int positionX;
	private int positionY;
	public ChessPiece(int id, String text, Color color, int positionX, int positionY) {
    
    
		this.id = id;
		this.text = text;
		this.color = color;
		this.positionX = positionX;
		this.positionY = positionX;
	}
	public static enum Color {
    
    
		RED, BLACK
	}
	// ...省略其他属性和getter/setter方法...
}
public class ChessBoard {
    
    //棋局
	private Map<Integer, ChessPiece> chessPieces = new HashMap<>();
	public ChessBoard() {
    
    
		init();
	}
	private void init() {
    
    
		chessPieces.put(1, new ChessPiece(1, "車", ChessPiece.Color.BLACK, 0, 0));
		chessPieces.put(2, new ChessPiece(2,"馬", ChessPiece.Color.BLACK, 0, 1));
		//...省略摆放其他棋子的代码...
	}
	public void move(int chessPieceId, int toPositionX, int toPositionY) {
    
    
		//...省略...
	}
}

In order to record the current chess situation in each room, a ChessBoard chess object needs to be created for each room. Because there are thousands of rooms in the game hall (in fact, there are many game halls with millions of people online at the same time), saving so many game objects will consume a lot of memory. Is there any way to save memory?

At this time, the flyweight mode can come in handy. Like the implementation just now, there will be a large number of similar objects in memory. The id, text, and color of these similar objects are all the same, except for positionX and positionY. In fact, we can separate the id, text, and color attributes of chess pieces, design them as independent classes, and use them as flyweights for multiple boards to reuse. In this way, the chessboard only needs to record the position information of each chess piece. The specific code implementation is as follows:

// 享元类
public class ChessPieceUnit {
    
    
	private int id;
	private String text;
	private Color color;
	public ChessPieceUnit(int id, String text, Color color) {
    
    
		this.id = id;
		this.text = text;
		this.color = color;
	}
	public static enum Color {
    
    
		RED, BLACK
	}
	// ...省略其他属性和getter方法...
}
public class ChessPieceUnitFactory {
    
    
	private static final Map<Integer, ChessPieceUnit> pieces = new HashMap<>();
	static {
    
    
		pieces.put(1, new ChessPieceUnit(1, "車", ChessPieceUnit.Color.BLACK));
		pieces.put(2, new ChessPieceUnit(2,"馬", ChessPieceUnit.Color.BLACK));
		//...省略摆放其他棋子的代码...
	}
	public static ChessPieceUnit getChessPiece(int chessPieceId) {
    
    
		return pieces.get(chessPieceId);
	}
}
public class ChessPiece {
    
    
	private ChessPieceUnit chessPieceUnit;
	private int positionX;
	private int positionY;
	public ChessPiece(ChessPieceUnit unit, int positionX, int positionY) {
    
    
		this.chessPieceUnit = unit;
		this.positionX = positionX;
		this.positionY = positionY;
	}
	// 省略getter、setter方法
}
public class ChessBoard {
    
    
	private Map<Integer, ChessPiece> chessPieces = new HashMap<>();
	public ChessBoard() {
    
    
		init();
	}
	private void init() {
    
    
		chessPieces.put(1, new ChessPiece(
		                    ChessPieceUnitFactory.getChessPiece(1), 0,0));
		chessPieces.put(1, new ChessPiece(
		                    ChessPieceUnitFactory.getChessPiece(2), 1,0));
		//...省略摆放其他棋子的代码...
	}
	public void move(int chessPieceId, int toPositionX, int toPositionY) {
    
    
		//...省略...
	}
}

In the above code implementation, the factory class is used to cache ChessPieceUnit information (that is, id, text, color). The ChessPieceUnit obtained through the factory class is the Flyweight. All ChessBoard objects share these 30 ChessPieceUnit objects (since there are only 30 pieces in chess). Before using the Flyweight mode, to record 10,000 chess games, it is necessary to create a ChessPieceUnit object with 300,000 (30*10,000) pieces. Using the flyweight mode, only 30 flyweight objects need to be created for all chess games to share, which greatly saves memory

Summarize its code structure. In fact, the code implementation is very simple, mainly through the factory mode. In the factory class, a Map is used to cache the created enjoyment objects to achieve the purpose of reuse.

7.2 Application in text editor

Think of the text editor mentioned here as Word for Office. However, in order to simplify the requirements background, it is assumed that this text editor only implements text editing functions, and does not include complex editing functions such as pictures and tables. For the simplified text editor, to represent a text file in memory, only two parts of information, text and format, need to be recorded. Among them, the format includes information such as the font, size, and color of the text.

Although in actual document writing, the format of the text is generally set according to the text type (title, body...), the title is one format, the body is another format, and so on. However, it is theoretically possible to format each word in a text file differently. In order to achieve such flexible formatting, and the code implementation is not too complicated, each text is treated as an independent object, and its formatting information is contained in it. A specific code example is as follows:

public class Character {
    
    //文字
	private char c;
	private Font font;
	private int size;
	private int colorRGB;
	public Character(char c, Font font, int size, int colorRGB) {
    
    
		this.c = c;
		this.font = font;
		this.size = size;
		this.colorRGB = colorRGB;
	}
}
public class Editor {
    
    
	private List<Character> chars = new ArrayList<>();
	public void appendCharacter(char c, Font font, int size, int colorRGB) {
    
    
		Character character = new Character(c, font, size, colorRGB);
		chars.add(character);
	}
}

In the text editor, every time a character is typed, appendCharacter()the method to create a new Character object and save it in the chars array. If there are tens of thousands, hundreds of thousands, or hundreds of thousands of characters in a text file, then so many Character objects must be stored in memory. Is there a way to save a little memory?

In fact, in a text file, there will not be too many font formats used. After all, it is unlikely that someone will set each text into a different format. Therefore, for the font format, it can be designed as a flyweight, so that different texts can be shared and used. According to this design idea, refactor the above code. The refactored code looks like this:

public class CharacterStyle {
    
    
	private Font font;
	private int size;
	private int colorRGB;
	public CharacterStyle(Font font, int size, int colorRGB) {
    
    
		this.font = font;
		this.size = size;
		this.colorRGB = colorRGB;
	}
	@Override
	public boolean equals(Object o) {
    
    
		CharacterStyle otherStyle = (CharacterStyle) o;
		return font.equals(otherStyle.font)
		       && size == otherStyle.size
		       && colorRGB == otherStyle.colorRGB;
	}
}
public class CharacterStyleFactory {
    
    
	private static final List<CharacterStyle> styles = new ArrayList<>();
	public static CharacterStyle getStyle(Font font, int size, int colorRGB) {
    
    
		CharacterStyle newStyle = new CharacterStyle(font, size, colorRGB);
		for (CharacterStyle style : styles) {
    
    
			if (style.equals(newStyle)) {
    
    
				return style;
			}
		}
		styles.add(newStyle);
		return newStyle;
	}
}
public class Character {
    
    
	private char c;
	private CharacterStyle style;
	public Character(char c, CharacterStyle style) {
    
    
		this.c = c;
		this.style = style;
	}
}
public class Editor {
    
    
	private List<Character> chars = new ArrayList<>();
	public void appendCharacter(char c, Font font, int size, int colorRGB) {
    
    
		Character character = new Character(c, CharacterStyleFactory.getStyle(font, size, colorRGB);
		chars.add(character);
	}
}

7.3 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

7.4 Application in Java Integer

First look at the following piece of code, and think about what kind of results this code will output:

Integer i1 = 56;
Integer i2 = 56;
Integer i3 = 129;
Integer i4 = 129;
System.out.println(i1 == i2);
System.out.println(i3 == i4);

If you are not familiar with the Java language, you may think that the values ​​of i1 and i2 are both 56, and the values ​​of i3 and i4 are both 129. The values ​​of i1 and i2 are equal, and the values ​​of i3 and i4 are equal, so the output result should be two true. This kind of analysis is wrong. To correctly analyze the above code, you need to figure out the following two questions:

  • How to determine whether two Java objects are equal (that is, the meaning of the "==" operator in the code)?
  • What is Autoboxing and Unboxing?

Java provides corresponding wrapper types for primitive data types. Specifically as follows:

insert image description here

The so-called autoboxing is to automatically convert the basic data type into a wrapper type. The so-called automatic unboxing is to automatically convert the wrapper type into a basic data type. A specific code example is as follows:

Integer i = 56; //自动装箱
int j = i; //自动拆箱

The value 56 is the basic data type int. When it is assigned to a wrapper type (Integer) variable, the automatic boxing operation is triggered to create an object of Integer type and assigned to the variable i. Its bottom layer is equivalent to executing the following statement:

Integer i = 59; 底层执行了: Integer i = Integer.valueOf(59);

Conversely, when the variable i of the wrapper type is assigned to the variable j of the basic data type, an automatic unboxing operation is triggered, and the data in i is taken out and assigned to j. Its bottom layer is equivalent to executing the following statement:

int j = i; 底层执行了: int j = i.intValue();

After clarifying the automatic boxing and automatic unboxing, let's look at how to determine whether two objects are equal? However, before that, we must first figure out how Java objects are stored in memory. For example:

User a = new User(123, 23); // id=123, age=23

The value stored in a is the memory address of the User object, which is shown as a pointing to the User object in the figure.

insert image description here

When using "==" to determine whether two objects are equal, it is actually judging whether the addresses stored in the two local variables are the same, in other words, judging whether the two local variables point to the same object

Look at the previous code again:

Integer i1 = 56;
Integer i2 = 56;
Integer i3 = 129;
Integer i4 = 129;
System.out.println(i1 == i2);
System.out.println(i3 == i4);

The first 4 lines of assignment statements will trigger the autoboxing operation, that is, an Integer object will be created and assigned to the four variables i1, i2, i3, and i4. According to the explanation just now, although i1 and i2 store the same value, both of which are 56, they point to different Integer objects, so when "==" is used to determine whether they are the same, false will be returned. Similarly, i3==i4the judgment statement will also return false

However, the above analysis is still wrong. The answer is not two false, but one true and one false. Seeing this may be a bit confusing. In fact, this is precisely because Integer uses the Flyweight pattern to reuse objects, which leads to such running results. When valueOf()creating , if the value of the Integer object to be created is between -128 and 127, it will be returned directly from the IntegerCache class, otherwise it will be created by calling the new method. It is clearer to look at the code. The specific code of valueOf()the function is as follows:

public static Integer valueOf(int i) {
    
    
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

In fact, the IntegerCache here is equivalent to the factory class that generates flyweight objects mentioned earlier, but the name is not xxxFactory. Let's look at its specific code implementation. This class is an internal class of Integer, and you can also view the JDK source code by yourself:

/**
* Cache to support the object identity semantics of autoboxing for values betw
* -128 and 127 (inclusive) as required by JLS.
*
* The cache is initialized on first usage. The size of the cache
* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
* During VM initialization, java.lang.Integer.IntegerCache.high property
* may be set and saved in the private system properties in the
* sun.misc.VM class.
*/
private static class IntegerCache {
    
    
	static final int low = -128;
	static final int high;
	static final Integer cache[];
	static {
    
    
		// high value may be configured by property
		int h = 127;
		String integerCacheHighPropValue =
		    sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high")
		if (integerCacheHighPropValue != null) {
    
    
			try {
    
    
				int i = parseInt(integerCacheHighPropValue);
				i = Math.max(i, 127);
				// Maximum array size is Integer.MAX_VALUE
				h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
			} catch( NumberFormatException nfe) {
    
    
				// If the property cannot be parsed into an int, ignore it.
			}
		}
		high = h;
		cache = new Integer[(high - low) + 1];
		int j = low;
		for(int k = 0; k < cache.length; k++)
			cache[k] = new Integer(j++);
		// range [-128, 127] must be interned (JLS7 5.1.7)
		assert IntegerCache.high >= 127;
	}
	private IntegerCache() {
    
    }
}

Why does IntegerCache only cache integer values ​​between -128 and 127?

In the code implementation of IntegerCache, when this class is loaded, the cached flyweight objects will be created in one go. After all, there are too many integer values, and it is impossible to pre-create all the integer values ​​in the IntegerCache class. This will not only occupy too much memory, but also make the loading time of the IntegerCache class too long. Therefore, you can only choose to cache the most commonly used integer values ​​for most applications, that is, the size of one byte (data between -128 and 127)

In fact, JDK also provides methods to allow us to customize the maximum value of the cache, there are the following two ways. If you analyze the JVM memory usage of the application and find that the data between -128 and 255 takes up more memory, you can use the following method to adjust the maximum value of the cache from 127 to 255. However, note here that the JDK does not provide a way to set the minimum value

//方法一:
-Djava.lang.Integer.IntegerCache.high=255
//方法二:
-XX:AutoBoxCacheMax=255

In fact, in addition to the Integer type, other wrapper types, such as Long, Short, Byte, etc., also use the flyweight mode to cache data between -128 and 127. For example, the LongCache flyweight factory class and valueOf()function are as follows:

private static class LongCache {
    
    
	private LongCache() {
    
    }
	static final Long cache[] = new Long[-(-128) + 127 + 1];
	static {
    
    
		for(int i = 0; i < cache.length; i++)
			cache[i] = new Long(i - 128);
	}
}
public static Long valueOf(long l) {
    
    
	final int offset = 128;
	if (l >= -128 && l <= 127) {
    
     // will cache
		return LongCache.cache[(int)l + offset];
	}
	return new Long(l);
}

In normal development, for the following three methods of creating integer objects, the latter two are preferred:

Integer a = new Integer(123);
Integer a = 123;
Integer a = Integer.valueOf(123);

The first creation method does not use IntegerCache, and the latter two creation methods can use IntegerCache cache to return shared objects to save memory. To give an extreme example, suppose the program needs to create 10,000 Integer objects between -128 and 127. Using the first creation method, you need to allocate memory space for 10,000 Integer objects; using the latter two creation methods, you only need to allocate memory space for up to 256 Integer objects

7.5 Application in Java String

Similarly, let's look at a piece of code first. What is the output of this code?

String s1 = "小争哥";
String s2 = "小争哥";
String s3 = new String("小争哥");
System.out.println(s1 == s2);
System.out.println(s1 == s3);

The result of running the above code is: one true, one false. Similar to the design idea of ​​the Integer class, the String class uses the flyweight pattern to reuse the same string constant (that is, the "little brother" in the code). JVM will specially open up a storage area to store string constants, this storage area is called "string constant pool". The memory storage structure corresponding to the above code is as follows:

insert image description here

However, the design of the Flyweight mode of the String class is slightly different from that of the Integer class. The objects to be shared in the Integer class are created at one time when the class is loaded. However, for strings, there is no way to know in advance which string constants to share, so there is no way to create them in advance. It can only be stored in the constant pool when a certain string constant is used for the first time. When you use it later, you can directly refer to what already exists in the constant pool, and you don’t need to recreate it

In fact, Flyweight mode is not friendly to JVM garbage collection. Because the enjoyment factory class has always saved the reference to the enjoyment object, which leads to the fact that the enjoyment object will not be automatically recycled by the JVM garbage collection mechanism without any code use. Therefore, in some cases, if the object's life cycle is short and will not be used intensively, using the flyweight pattern may waste more memory. Therefore, unless it has been verified online that using the Flyweight mode can really save memory a lot, otherwise, don’t overuse this mode. Introducing a complex design mode for a little bit of memory saving is not worth the candle

Guess you like

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