设计模式 - 创建型
# 设计模式 - 创建型
来源:极客时间《设计模式之美》 (opens new window)专栏
创建型模式主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。其中,单例模式用来创建全局唯一的对象。工厂模式用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。建造者模式是用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。原型模式针对创建成本比较大的对象,利用对已有对象进行复制的方式进行创建,以达到节省创建时间的目的。
# 1. 单例模式
单例设计模式(Singleton Design Pattern),一个类只允许创建一个对象(或者实例),那这个类就是单例类,这种设计模式就叫作单例设计模式,简称单例模式。
# 1.1 为什么要使用单例模式
# 1.1.1 处理资源访问冲突
实现了一个往文件中打印日志的 Logger 类,代码如下:
public class Logger {
private FileWriter writer;
public Logger() {
File file = new File("/Users/wangzheng/log.txt");
writer = new FileWriter(file, true); //true表示追加写入
}
public void log(String message) {
writer.write(message);
}
}
// Logger类的应用示例:
public class UserController {
private Logger logger = new Logger();
public void login(String username, String password) {
// ...省略业务逻辑代码...
logger.log(username + " logined!");
}
}
public class OrderController {
private Logger logger = new Logger();
public void create(OrderVo order) {
// ...省略业务逻辑代码...
logger.log("Created an order: " + order.toString());
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
在 Web 容器的 Servlet 多线程环境下,如果两个 Servlet 线程同时分别执行 login() 和 create() 两个函数,并且同时写日志到 log.txt 文件中,那就有可能存在日志信息互相覆盖的情况。
最先想到的解决方式就是通过加锁的方式:给 log() 函数加互斥锁(Java 中可以通过 synchronized 的关键字),同一时刻只允许一个线程调用执行 log() 函数。代码如下:
public void log(String message) {
synchronized(this) {
writer.write(mesasge);
}
}
2
3
4
5
synchronized 无法解决该问题,原因是这种锁是一个对象级别的锁,一个对象在不同的线程下同时调用 log() 函数,会被强制要求顺序执行。但是,不同的对象之间并不共享同一把锁。在不同的线程下,通过不同的对象调用执行 log() 函数,锁并不会起作用,仍然有可能存在写入日志互相覆。
实际上给 log() 函数加互斥锁是多此一举,因为 FileWriter 本身就是线程安全的,它的内部实现中本身就加了对象级别的锁,外层再加对象级别的锁多此一举。因为不同的 Logger 对象不共享 FileWriter 对象,所以,FileWriter 对象级别的锁也解决不了数据写入互相覆盖的问题。
解决方式:
把对象级别的锁,换成类级别的锁。让所有的对象都共享同一把锁。这样就避免了不同对象之间同时调用 log() 函数,而导致的日志覆盖问题。代码如下:
public void log(String message) { synchronized(Logger.class) { // 类级别的锁 writer.write(mesasge); } }
1
2
3
4
5其他方式:分布式锁、并发队列(比如 Java 中的 BlockingQueue),但实现起来有点复杂。
更简单的方式即单例模式
单例模式相对于类级别锁的好处:不用创建多个 Logger 对象,一方面节省内存空间;另一方面节省系统文件句柄(对于操作系统来说,文件句柄也是一种资源,不能随便浪费)。代码如下:
public class Logger { private FileWriter writer; private static final Logger instance = new Logger(); private Logger() { File file = new File("/Users/wangzheng/log.txt"); writer = new FileWriter(file, true); //true表示追加写入 } public static Logger getInstance() { return instance; } public void log(String message) { writer.write(mesasge); } } // Logger类的应用示例: public class UserController { public void login(String username, String password) { // ...省略业务逻辑代码... Logger.getInstance().log(username + " logined!"); } } public class OrderController { public void create(OrderVo order) { // ...省略业务逻辑代码... Logger.getInstance().log("Created a order: " + order.toString()); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 1.1.2 表示全局唯一类
从业务概念上,如果有些数据在系统中只应保存一份,那就比较适合设计为单例类。比如,配置信息类、唯一递增 ID 号码生成器(果程序中有两个对象,那就会存在生成重复 ID 的情况)。
import java.util.concurrent.atomic.AtomicLong;
public class IdGenerator {
// AtomicLong是一个Java并发库中提供的一个原子变量类型,
// 它将一些线程不安全需要加锁的复合操作封装为了线程安全的原子操作,
// 比如下面会用到的incrementAndGet().
private AtomicLong id = new AtomicLong(0);
private static final IdGenerator instance = new IdGenerator();
private IdGenerator() {}
public static IdGenerator getInstance() {
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
// IdGenerator使用举例
long id = IdGenerator.getInstance().getId();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 1.2 如何实现一个单例
需关注如下几点:
- 构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例;
- 考虑对象创建时的线程安全问题;
- 考虑是否支持延迟加载;
- 考虑 getInstance() 性能是否高(是否加锁)
# 1.2.1 饿汉式
在类加载的时候,instance 静态实例就已经创建并初始化好了,所以,instance 实例的创建过程是线程安全的。不支持懒加载。
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static final IdGenerator instance = new IdGenerator();
private IdGenerator() {}
public static IdGenerator getInstance() {
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
2
3
4
5
6
7
8
9
10
11
# 1.2.2 懒汉式
懒汉式相对于饿汉式的优势是支持延迟加载。缺点在于 getInstance 需要加锁(synchronzed),导致函数并发度很低。如果频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶颈,这种实现方式就不可取了。
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static IdGenerator instance;
private IdGenerator() {}
public static synchronized IdGenerator getInstance() {
if (instance == null) {
instance = new IdGenerator();
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1.2.3 双重检查
双重检查是既支持延迟加载、又支持高并发的单例实现方式。
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static IdGenerator instance;
private IdGenerator() {}
public static IdGenerator getInstance() {
if (instance == null) {
synchronized(IdGenerator.class) { // 此处为类级别的锁
if (instance == null) {
instance = new IdGenerator();
}
}
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
实际上,上述实现方式存在问题:CPU 指令重排序可能导致在 IdGenerator 类的对象被关键字 new 创建并赋值给 instance 之后,还没来得及初始化(执行构造函数中的代码逻辑),就被另一个线程使用了。这样,另一个线程就使用了一个没有完整初始化的 IdGenerator 类的对象。要解决这个问题,只需要给 instance 成员变量添加 volatile 关键字来禁止指令重排序即可。
# 1.2.4 静态内部类
比双重检测更加简单的实现方法,那就是利用 Java 的静态内部类。它有点类似饿汉式,但又能做到了延迟加载。
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private IdGenerator() {}
private static class SingletonHolder{
private static final IdGenerator instance = new IdGenerator();
}
public static IdGenerator getInstance() {
return SingletonHolder.instance;
}
public long getId() {
return id.incrementAndGet();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SingletonHolder 是一个静态内部类,当外部类 IdGenerator 被加载的时候,并不会创建 SingletonHolder 实例对象。只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。
# 1.2.5 枚举
最简单的实现方式,通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。
public enum IdGenerator {
INSTANCE;
private AtomicLong id = new AtomicLong(0);
public long getId() {
return id.incrementAndGet();
}
}
2
3
4
5
6
7
8
# 1.3 单例模式存在的问题
在项目中使用单例,都是用它来表示一些全局唯一类,比如配置信息类、连接池类、ID 生成器类。通过类似 IdGenerator.getInstance().getId() 这样的方法来调用。但这种使用方法有点类似硬编码(hard code),会带来诸多问题。
单例对 OOP 特性的支持不友好
单例这种设计模式对于其中的抽象、继承、多态都支持得不好。
IdGenerator 的使用方式违背了基于接口而非实现的设计原则(即 OOP 的抽象特性)。如果未来希望针对不同的业务采用不同的 ID 生成算法。比如,订单 ID 和用户 ID 采用不同的 ID 生成器来生成。为了应对需求变化,需要修改所有用到 IdGenerator 类的地方,这样代码的改动就会比较大。
除此之外,代码设计时单例类一般不会被继承和实现多态,也就相当于损失了可以应对未来需求变化的扩展性。
单例会隐藏类之间的依赖关系
单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用。如果代码比较复杂,这种调用关系就会非常隐蔽。
单例对代码的扩展性不友好
单例类只能有一个对象实例。如果未来需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动。
如数据库连接池,为避免慢 SQL 影响到其他 SQL 的执行,慢 SQL 独享一个数据库连接池。所以数据库连接池、线程池这类的资源池,最好还是不要设计成单例类。实际上一些开源的数据库连接池、线程池也确实没有设计成单例类。
单例对代码的可测试性不友好
单例类这种硬编码式的使用方式,导致无法实现 mock 替换。
如果单例类持有成员变量(比如 IdGenerator 中的 id 成员变量),那它实际上相当于一种全局变量,被所有的代码共享。全局变量不易测试。
单例不支持有参数的构造函数
单例不支持有参数的构造函数,比如创建一个连接池的单例对象,没法通过参数来指定连接池的大小。
推荐的解决方式:将参数放到另外一个全局变量中(如配置类)。
public class Config { public static final int PARAM_A = 123; public static final int PARAM_B = 245; } public class Singleton { private static Singleton instance = null; private final int paramA; private final int paramB; private Singleton() { this.paramA = Config.PARAM_A; this.paramB = Config.PARAM_B; } public synchronized static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 1.4 单例的代替方案
为了保证全局唯一,除了使用单例,还可以用静态方法来实现。但也不能解决之前提到的问题。
// 静态方法实现方式
public class IdGenerator {
private static AtomicLong id = new AtomicLong(0);
public static long getId() {
return id.incrementAndGet();
}
}
// 使用举例
long id = IdGenerator.getId();
2
3
4
5
6
7
8
9
10
类对象的全局唯一性可以通过多种不同的方式来保证。既可以通过单例模式来强制保证,也可以通过工厂模式、IOC 容器(比如 Spring IOC 容器)来保证,还可以通过程序员自己来保证(自己在编写代码的时候自己保证不要创建两个类对象)。
# 1.5 实现分布式单例模式
# 1.5.1 如何理解单例模式中的唯一性
单例模式定义中提到,“一个类只允许创建唯一一个对象”,对象的唯一性的作用范围是指进程内只允许创建一个对象,即单例模式创建的对象是进程唯一的,这也就说,单例类中对象在进程间是不唯一的。
# 1.5.2 如何实现线程唯一的单例
“进程唯一”指的是进程内唯一,进程间不唯一。类比一下,“线程唯一”指的是线程内唯一,线程间可以不唯一。实际上,“进程唯一”还代表了线程内、线程间都唯一,这也是“进程唯一”和“线程唯一”的区别之处。
假设 IdGenerator 是一个线程唯一的单例类。在线程 A 内可以创建一个单例对象 a。因为线程内唯一,在线程 A 内就不能再创建新的 IdGenerator 对象了,而线程间可以不唯一,所以,在另外一个线程 B 内还可以重新创建一个新的单例对象 b。
实现方式:通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样就可以做到不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java 语言本身提供了 ThreadLocal 工具类,可以更加轻松地实现线程唯一单例。不过,ThreadLocal 底层实现原理也是基于下面代码中所示的 HashMap。
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static final ConcurrentHashMap<Long, IdGenerator> instances
= new ConcurrentHashMap<>();
private IdGenerator() {}
public static IdGenerator getInstance() {
Long currentThreadId = Thread.currentThread().getId();
instances.putIfAbsent(currentThreadId, new IdGenerator());
return instances.get(currentThreadId);
}
public long getId() {
return id.incrementAndGet();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 1.5.3 如何实现集群唯一单例
“进程唯一”指的是进程内唯一、进程间不唯一。“线程唯一”指的是线程内唯一、线程间不唯一。集群相当于多个进程构成的一个集合,“集群唯一”就相当于是进程内唯一、进程间也唯一。也就是说,不同的进程间共享同一个对象,不能创建同一个类的多个对象。
实现思路:需要把单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。为了保证任何时刻,在进程间都只有一份对象存在,一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使用完这个对象之后,还需要显式地将对象从内存中删除,并且释放对对象的加锁。
大致代码如下:
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static IdGenerator instance;
private static SharedObjectStorage storage = FileSharedObjectStorage(/*入参省略,比如文件地址*/);
private static DistributedLock lock = new DistributedLock();
private IdGenerator() {}
public synchronized static IdGenerator getInstance()
if (instance == null) {
lock.lock();
instance = storage.load(IdGenerator.class);
}
return instance;
}
public synchroinzed void freeInstance() {
storage.save(this, IdGeneator.class);
instance = null; //释放对象
lock.unlock();
}
public long getId() {
return id.incrementAndGet();
}
}
// IdGenerator使用举例
IdGenerator idGeneator = IdGenerator.getInstance();
long id = idGenerator.getId();
idGenerator.freeInstance();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 1.5.4 如何实现一个多例模式
“单例”指的是,一个类只能创建一个对象。对应地,“多例”指的就是,一个类可以创建多个对象,但是个数是有限制的,比如只能创建 3 个对象。
public class BackendServer {
private long serverNo;
private String serverAddress;
private static final int SERVER_COUNT = 3;
private static final Map<Long, BackendServer> serverInstances = new HashMap<>();
static {
serverInstances.put(1L, new BackendServer(1L, "192.134.22.138:8080"));
serverInstances.put(2L, new BackendServer(2L, "192.134.22.139:8080"));
serverInstances.put(3L, new BackendServer(3L, "192.134.22.140:8080"));
}
private BackendServer(long serverNo, String serverAddress) {
this.serverNo = serverNo;
this.serverAddress = serverAddress;
}
public BackendServer getInstance(long serverNo) {
return serverInstances.get(serverNo);
}
public BackendServer getRandomInstance() {
Random r = new Random();
int no = r.nextInt(SERVER_COUNT)+1;
return serverInstances.get(no);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
实际上,对于多例模式,还有一种理解方式:同一类型的只能创建一个对象,不同类型的可以创建多个对象。
例如:在代码中,logger name 就是刚刚说的“类型”,同一个 logger name 获取到的对象实例是相同的,不同的 logger name 获取到的对象实例是不同的。
public class Logger {
private static final ConcurrentHashMap<String, Logger> instances
= new ConcurrentHashMap<>();
private Logger() {}
public static Logger getInstance(String loggerName) {
instances.putIfAbsent(loggerName, new Logger());
return instances.get(loggerName);
}
public void log() {
//...
}
}
//l1==l2, l1!=l3
Logger l1 = Logger.getInstance("User.class");
Logger l2 = Logger.getInstance("User.class");
Logger l3 = Logger.getInstance("Order.class");
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
这种多例模式的理解方式有点类似工厂模式。它跟工厂模式的不同之处是,多例模式创建的对象都是同一个类的对象,而工厂模式创建的是不同子类的对象;它还有点类似享元模式;枚举类型也相当于多例模式,一个类型只能对应一个对象,一个类可以创建多个对象。
# 2. 工厂模式
工厂模式(Factory Design Pattern)。一般情况下,工厂模式分为三种更加细分的类型:简单工厂、工厂方法和抽象工厂。需要搞清楚应用场景:什么时候该用工厂模式?相对于直接 new 来创建对象,用工厂模式来创建究竟有什么好处呢?
# 2.1 简单工厂(Simple Factory)
例子:根据配置文件的后缀(json、xml、yaml、properties),选择不同的解析器(JsonRuleConfigParser、XmlRuleConfigParser……),将存储在文件中的配置解析成内存对象 RuleConfig。原始代码如下:
public class RuleConfigSource {
public RuleConfig load(String ruleConfigFilePath) {
String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
IRuleConfigParser parser = null;
if ("json".equalsIgnoreCase(ruleConfigFileExtension)) {
parser = new JsonRuleConfigParser();
} else if ("xml".equalsIgnoreCase(ruleConfigFileExtension)) {
parser = new XmlRuleConfigParser();
} else if ("yaml".equalsIgnoreCase(ruleConfigFileExtension)) {
parser = new YamlRuleConfigParser();
} else if ("properties".equalsIgnoreCase(ruleConfigFileExtension)) {
parser = new PropertiesRuleConfigParser();
} else {
throw new InvalidRuleConfigException(
"Rule config file format is not supported: " + ruleConfigFilePath);
}
String configText = "";
//从ruleConfigFilePath文件中读取配置文本到configText中
RuleConfig ruleConfig = parser.parse(configText);
return ruleConfig;
}
private String getFileExtension(String filePath) {
//...解析文件名获取扩展名,比如rule.json,返回json
return "json";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
为了让代码逻辑更加清晰,可读性更好,且让类的职责更加单一,将创建 parser 的代码剥离到一个独立的类中,即实现简单工厂模式:
public class RuleConfigParserFactory {
public static IRuleConfigParser createParser(String configFormat) {
IRuleConfigParser parser = null;
if ("json".equalsIgnoreCase(configFormat)) {
parser = new JsonRuleConfigParser();
} else if ("xml".equalsIgnoreCase(configFormat)) {
parser = new XmlRuleConfigParser();
} else if ("yaml".equalsIgnoreCase(configFormat)) {
parser = new YamlRuleConfigParser();
} else if ("properties".equalsIgnoreCase(configFormat)) {
parser = new PropertiesRuleConfigParser();
}
return parser;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
上面代码每次调用 RuleConfigParserFactory 的 createParser() 的时候,都要创建一个新的 parser。如果 parser 可以复用,为了节省内存和对象创建的时间,可以将 parser 事先创建好缓存起来。当调用 createParser() 函数的时候,从缓存中取出 parser 对象直接使用。简单工厂的第二种实现方式:
public class RuleConfigParserFactory {
private static final Map<String, RuleConfigParser> cachedParsers = new HashMap<>();
static {
cachedParsers.put("json", new JsonRuleConfigParser());
cachedParsers.put("xml", new XmlRuleConfigParser());
cachedParsers.put("yaml", new YamlRuleConfigParser());
cachedParsers.put("properties", new PropertiesRuleConfigParser());
}
public static IRuleConfigParser createParser(String configFormat) {
if (configFormat == null || configFormat.isEmpty()) {
return null;//返回null还是IllegalArgumentException全凭你自己说了算
}
IRuleConfigParser parser = cachedParsers.get(configFormat.toLowerCase());
return parser;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
尽管简单工厂模式的代码实现中,有多处 if 分支判断逻辑,违背开闭原则,但权衡扩展性和可读性,这样的代码实现在大多数情况下(比如,不需要频繁地添加 parser,也没有太多的 parser)是没有问题的。
# 2.2 工厂方法(Factory Method)
如果非要将 if 分支逻辑去掉,比较经典的处理方法是利用多态,同时创建一个 RuleConfigParserFactoryMap(工厂的工厂),通过RuleConfigParserFactoryMap 来获取工厂类对象。当需要添加新的规则配置解析器的时候,只需要创建新的 parser 类和 parser factory 类,并且在 RuleConfigParserFactoryMap 类中,将新的 parser factory 对象添加到 cachedFactories 中即可。代码的改动非常少,基本上符合开闭原则。
// 使用多态
public interface IRuleConfigParserFactory {
IRuleConfigParser createParser();
}
public class JsonRuleConfigParserFactory implements IRuleConfigParserFactory {
@Override
public IRuleConfigParser createParser() {
return new JsonRuleConfigParser();
}
}
public class XmlRuleConfigParserFactory implements IRuleConfigParserFactory {
@Override
public IRuleConfigParser createParser() {
return new XmlRuleConfigParser();
}
}
public class YamlRuleConfigParserFactory implements IRuleConfigParserFactory {
@Override
public IRuleConfigParser createParser() {
return new YamlRuleConfigParser();
}
}
public class PropertiesRuleConfigParserFactory implements IRuleConfigParserFactory {
@Override
public IRuleConfigParser createParser() {
return new PropertiesRuleConfigParser();
}
}
//工厂类再创建一个简单工厂,也就是工厂的工厂,用来创建工厂类对象
public class RuleConfigParserFactoryMap {
private static final Map<String, IRuleConfigParserFactory> cachedFactories = new HashMap<>();
static {
cachedFactories.put("json", new JsonRuleConfigParserFactory());
cachedFactories.put("xml", new XmlRuleConfigParserFactory());
cachedFactories.put("yaml", new YamlRuleConfigParserFactory());
cachedFactories.put("properties", new PropertiesRuleConfigParserFactory());
}
public static IRuleConfigParserFactory getParserFactory(String type) {
if (type == null || type.isEmpty()) {
return null;
}
IRuleConfigParserFactory parserFactory = cachedFactories.get(type.toLowerCase());
return parserFactory;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 2.3 抽象工厂(Abstract Factory)
抽象工厂模式的应用场景比较特殊,不常用。比如在规则配置解析例子中,解析器类只会根据配置文件格式(Json、Xml、Yaml……)来分类。但如果类有两种分类方式,比如既可以按照配置文件格式来分类,也可以按照解析的对象(Rule 规则配置还是 System 系统配置)来分类,那就会对应下面这 8 个 parser 类。
针对规则配置的解析器:基于接口IRuleConfigParser
JsonRuleConfigParser
XmlRuleConfigParser
YamlRuleConfigParser
PropertiesRuleConfigParser
针对系统配置的解析器:基于接口ISystemConfigParser
JsonSystemConfigParser
XmlSystemConfigParser
YamlSystemConfigParser
PropertiesSystemConfigParser
2
3
4
5
6
7
8
9
10
11
抽象工厂就是针对这种非常特殊的场景而诞生的。可以让一个工厂负责创建多个不同类型的对象(IRuleConfigParser、ISystemConfigParser 等),而不是只创建一种 parser 对象。这样就可以有效地减少工厂类的个数。
public interface IConfigParserFactory {
IRuleConfigParser createRuleParser();
ISystemConfigParser createSystemParser();
//此处可以扩展新的parser类型,比如IBizConfigParser
}
public class JsonConfigParserFactory implements IConfigParserFactory {
@Override
public IRuleConfigParser createRuleParser() {
return new JsonRuleConfigParser();
}
@Override
public ISystemConfigParser createSystemParser() {
return new JsonSystemConfigParser();
}
}
public class XmlConfigParserFactory implements IConfigParserFactory {
@Override
public IRuleConfigParser createRuleParser() {
return new XmlRuleConfigParser();
}
@Override
public ISystemConfigParser createSystemParser() {
return new XmlSystemConfigParser();
}
}
// 省略YamlConfigParserFactory和PropertiesConfigParserFactory代码
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 2.4 根据场景合理选择工厂模式
当创建逻辑比较复杂,是一个“大工程”的时候,就考虑使用工厂模式,封装对象的创建过程,将对象的创建和使用相分离。
第一种情况:类似规则配置解析的例子,代码中存在 if-else 分支判断,动态地根据不同的类型创建不同的对象。针对这种情况,就考虑使用工厂模式,将这一大坨 if-else 创建对象的代码抽离出来,放到工厂类中。
第二种情况:尽管不需要根据不同的类型创建不同的对象,但是,单个对象本身的创建过程比较复杂,比如前面提到的要组合其他类对象,做各种初始化操作。在这种情况下也可以考虑使用工厂模式,将对象的创建过程封装到工厂类中。
对于第一种情况,当每个对象的创建逻辑都比较简单的时候,推荐使用简单工厂模式,将多个对象的创建逻辑放到一个工厂类中。当每个对象的创建逻辑都比较复杂的时候,为了避免设计一个过于庞大的简单工厂类,推荐使用工厂方法模式,将创建逻辑拆分得更细,每个对象的创建逻辑独立到各自的工厂类中。同理,对于第二种情况,因为单个对象本身的创建逻辑就比较复杂,所以建议使用工厂方法模式。
工厂模式它的作用无外乎下面这四个。这也是判断要不要使用工厂模式的最本质的参考标准。
- 封装变化:创建逻辑有可能变化,封装成工厂类之后,创建逻辑的变更对调用者透明。
- 代码复用:创建代码抽离到独立的工厂类之后可以复用。
- 隔离复杂性:封装复杂的创建逻辑,调用者无需了解如何创建对象。
- 控制复杂度:将创建代码抽离出来,让原本的函数或类职责更单一,代码更简洁。
# 2.5 设计实现一个 Dependency Injection 框架
创建对象的“大工程”,依赖注入框架,或者叫依赖注入容器(Dependency Injection Container),简称 DI 容器。
工厂模式与 DI 容器区别
DI 容器底层最基本的设计思路就是基于工厂模式的。DI 容器相当于一个大的工厂类,负责在程序启动的时候,根据配置(要创建哪些类对象,每个类对象的创建需要依赖哪些其他类对象)事先创建好对象。当应用程序需要使用某个类对象的时候,直接从容器中获取即可。正是因为它持有一堆对象,所以这个框架才被称为“容器”。
一个工厂类只负责某个类对象或者某一组相关类对象(继承自同一抽象类或者接口的子类)的创建,而 DI 容器负责的是整个应用中所有类对象的创建(还包括配置的解析、对象生命周期的管理)。
DI 容器的核心功能
一个简单的 DI 容器的核心功能一般有三个:配置解析、对象创建和对象生命周期管理。
配置解析:将需要由 DI 容器来创建的类对象和创建类对象的必要信息(使用哪个构造函数以及对应的构造函数参数都是什么等等),放到配置文件中。容器读取配置文件,根据配置文件提供的信息来创建对象。下面是一个典型的 Spring 容器的配置文件:
public class RateLimiter { private RedisCounter redisCounter; public RateLimiter(RedisCounter redisCounter) { this.redisCounter = redisCounter; } public void test() { System.out.println("Hello World!"); } //... } public class RedisCounter { private String ipAddress; private int port; public RedisCounter(String ipAddress, int port) { this.ipAddress = ipAddress; this.port = port; } //... } 配置文件beans.xml: <beans> <bean id="rateLimiter" class="com.xzg.RateLimiter"> <constructor-arg ref="redisCounter"/> </bean> <bean id="redisCounter" class="com.xzg.redisCounter"> <constructor-arg type="String" value="127.0.0.1"> <constructor-arg type="int" value=1234> </bean> </beans>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32对象创建:DI 容器的具体实现的时候,通过“反射”机制,在程序运行的过程中动态地加载类、创建对象,不需要事先在代码中写死要创建哪些对象。
对象生命周期:在 Spring 框架中可以通过配置 scope 属性,来区分这两种不同类型的对象。scope=prototype 表示返回新创建的对象,scope=singleton 表示返回单例对象。还可以配置对象是否支持懒加载。如果 lazy-init=true,对象在真正被使用到的时候(比如:BeansFactory.getBean(“userService”))才被被创建;如果 lazy-init=false,对象在应用启动的时候就事先创建好。还可以配置对象的 init-method 和 destroy-method 方法,比如 init-method=loadProperties(),destroy-method=updateConfigFile()。DI 容器在创建好对象之后,会主动调用 init-method 属性指定的方法来初始化对象。在对象被最终销毁之前,DI 容器会主动调用 destroy-method 属性指定的方法来做一些清理工作,比如释放数据库连接池、关闭文件。
实现一个简单 DI 容器
用 Java 语言来实现一个简单的 DI 容器,核心逻辑只需要包括这样两个部分:配置文件解析、根据配置文件通过“反射”语法来创建对象。
最小原型设计
// 配置文件beans.xml <beans> <bean id="rateLimiter" class="com.xzg.RateLimiter"> <constructor-arg ref="redisCounter"/> </bean> <bean id="redisCounter" class="com.xzg.redisCounter" scope="singleton" lazy-init="true"> <constructor-arg type="String" value="127.0.0.1"> <constructor-arg type="int" value=1234> </bean> </beans>
1
2
3
4
5
6
7
8
9
10
11public class Demo { public static void main(String[] args) { ApplicationContext applicationContext = new ClassPathXmlApplicationContext( "beans.xml"); RateLimiter rateLimiter = (RateLimiter) applicationContext.getBean("rateLimiter"); rateLimiter.test(); //... } }
1
2
3
4
5
6
7
8
9提供执行入口
ClassPathXmlApplicationContext 负责组装 BeansFactory 和 BeanConfigParser 两个类,串联执行流程:从 classpath 中加载 XML 格式的配置文件,通过 BeanConfigParser 解析为统一的 BeanDefinition 格式,然后,BeansFactory 根据 BeanDefinition 来创建对象。
public interface ApplicationContext { Object getBean(String beanId); } public class ClassPathXmlApplicationContext implements ApplicationContext { private BeansFactory beansFactory; private BeanConfigParser beanConfigParser; public ClassPathXmlApplicationContext(String configLocation) { this.beansFactory = new BeansFactory(); this.beanConfigParser = new XmlBeanConfigParser(); loadBeanDefinitions(configLocation); } private void loadBeanDefinitions(String configLocation) { InputStream in = null; try { in = this.getClass().getResourceAsStream("/" + configLocation); if (in == null) { throw new RuntimeException("Can not find config file: " + configLocation); } List<BeanDefinition> beanDefinitions = beanConfigParser.parse(in); beansFactory.addBeanDefinitions(beanDefinitions); } finally { if (in != null) { try { in.close(); } catch (IOException e) { // TODO: log error } } } } @Override public Object getBean(String beanId) { return beansFactory.getBean(beanId); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39配置文件解析
配置文件解析主要包含 BeanConfigParser 接口和 XmlBeanConfigParser 实现类,负责将配置文件解析为 BeanDefinition 结构,以便 BeansFactory 根据这个结构来创建对象。
public interface BeanConfigParser { List<BeanDefinition> parse(InputStream inputStream); List<BeanDefinition> parse(String configContent); } public class XmlBeanConfigParser implements BeanConfigParser { @Override public List<BeanDefinition> parse(InputStream inputStream) { String content = null; // TODO:... return parse(content); } @Override public List<BeanDefinition> parse(String configContent) { List<BeanDefinition> beanDefinitions = new ArrayList<>(); // TODO:... return beanDefinitions; } } public class BeanDefinition { private String id; private String className; private List<ConstructorArg> constructorArgs = new ArrayList<>(); private Scope scope = Scope.SINGLETON; private boolean lazyInit = false; // 省略必要的getter/setter/constructors public boolean isSingleton() { return scope.equals(Scope.SINGLETON); } public static enum Scope { SINGLETON, PROTOTYPE } public static class ConstructorArg { private boolean isRef; private Class type; private Object arg; // 省略必要的getter/setter/constructors } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48核心工程类设计
BeansFactory 创建对象用到的主要技术点就是 Java 中的反射语法:一种动态加载类和创建对象的机制。
public class BeansFactory { private ConcurrentHashMap<String, Object> singletonObjects = new ConcurrentHashMap<>(); private ConcurrentHashMap<String, BeanDefinition> beanDefinitions = new ConcurrentHashMap<>(); public void addBeanDefinitions(List<BeanDefinition> beanDefinitionList) { for (BeanDefinition beanDefinition : beanDefinitionList) { this.beanDefinitions.putIfAbsent(beanDefinition.getId(), beanDefinition); } for (BeanDefinition beanDefinition : beanDefinitionList) { if (beanDefinition.isLazyInit() == false && beanDefinition.isSingleton()) { createBean(beanDefinition); } } } public Object getBean(String beanId) { BeanDefinition beanDefinition = beanDefinitions.get(beanId); if (beanDefinition == null) { throw new NoSuchBeanDefinitionException("Bean is not defined: " + beanId); } return createBean(beanDefinition); } @VisibleForTesting protected Object createBean(BeanDefinition beanDefinition) { if (beanDefinition.isSingleton() && singletonObjects.contains(beanDefinition.getId())) { return singletonObjects.get(beanDefinition.getId()); } Object bean = null; try { Class beanClass = Class.forName(beanDefinition.getClassName()); List<BeanDefinition.ConstructorArg> args = beanDefinition.getConstructorArgs(); if (args.isEmpty()) { bean = beanClass.newInstance(); } else { Class[] argClasses = new Class[args.size()]; Object[] argObjects = new Object[args.size()]; for (int i = 0; i < args.size(); ++i) { BeanDefinition.ConstructorArg arg = args.get(i); if (!arg.getIsRef()) { argClasses[i] = arg.getType(); argObjects[i] = arg.getArg(); } else { BeanDefinition refBeanDefinition = beanDefinitions.get(arg.getArg()); if (refBeanDefinition == null) { throw new NoSuchBeanDefinitionException("Bean is not defined: " + arg.getArg()); } argClasses[i] = Class.forName(refBeanDefinition.getClassName()); argObjects[i] = createBean(refBeanDefinition); } } bean = beanClass.getConstructor(argClasses).newInstance(argObjects); } } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) { throw new BeanCreationFailureException("", e); } if (bean != null && beanDefinition.isSingleton()) { singletonObjects.putIfAbsent(beanDefinition.getId(), bean); return singletonObjects.get(beanDefinition.getId()); } return bean; } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# 3. 建造者模式
Builder 模式是比较常用的创建型设计模式,中文翻译为建造者模式或者构建者模式,也有人叫它生成器模式。
# 3.1 为什么需要建造者模式
创建一个对象最常见的方式是使用 new 关键字调用类的构造函数类完成,那什么情况下要使用建造者模式来创建对象?
假设有这样一道设计面试题:需要定义一个资源池配置类 ResourcePoolConfig。这里的资源池可以简单理解为线程池、连接池、对象池等。在这个资源池配置类中,有以下几个是可配置项。请你编写代码实现这个 ResourcePoolConfig 类。
public class ResourcePoolConfig {
private static final int DEFAULT_MAX_TOTAL = 8;
private static final int DEFAULT_MAX_IDLE = 8;
private static final int DEFAULT_MIN_IDLE = 0;
private String name;
private int maxTotal = DEFAULT_MAX_TOTAL;
private int maxIdle = DEFAULT_MAX_IDLE;
private int minIdle = DEFAULT_MIN_IDLE;
public ResourcePoolConfig(String name, Integer maxTotal, Integer maxIdle, Integer minIdle) {
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("name should not be empty.");
}
this.name = name;
if (maxTotal != null) {
if (maxTotal <= 0) {
throw new IllegalArgumentException("maxTotal should be positive.");
}
this.maxTotal = maxTotal;
}
if (maxIdle != null) {
if (maxIdle < 0) {
throw new IllegalArgumentException("maxIdle should not be negative.");
}
this.maxIdle = maxIdle;
}
if (minIdle != null) {
if (minIdle < 0) {
throw new IllegalArgumentException("minIdle should not be negative.");
}
this.minIdle = minIdle;
}
}
//...省略getter方法...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
现在,ResourcePoolConfig 只有 4 个可配置项,但如果可配置项逐渐增多,变成了 8 个、10 个,甚至更多,那继续沿用现在的设计思路,构造函数的参数列表会变得很长,代码在可读性和易用性上都会变差。在使用构造函数的时候就容易搞错各参数的顺序,传递进错误的参数值,导致非常隐蔽的 bug。
解决这个问题的办法就是用 set() 函数来给成员变量赋值,以替代冗长的构造函数。其中配置项 name 是必填的,所以把它放到构造函数中设置,强制创建类对象的时候就要填写。其他配置项 maxTotal、maxIdle、minIdle 都不是必填的,所以通过 set() 函数来设置,让使用者自主选择填写或者不填写。
public class ResourcePoolConfig {
private static final int DEFAULT_MAX_TOTAL = 8;
private static final int DEFAULT_MAX_IDLE = 8;
private static final int DEFAULT_MIN_IDLE = 0;
private String name;
private int maxTotal = DEFAULT_MAX_TOTAL;
private int maxIdle = DEFAULT_MAX_IDLE;
private int minIdle = DEFAULT_MIN_IDLE;
public ResourcePoolConfig(String name) {
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("name should not be empty.");
}
this.name = name;
}
public void setMaxTotal(int maxTotal) {
if (maxTotal <= 0) {
throw new IllegalArgumentException("maxTotal should be positive.");
}
this.maxTotal = maxTotal;
}
public void setMaxIdle(int maxIdle) {
if (maxIdle < 0) {
throw new IllegalArgumentException("maxIdle should not be negative.");
}
this.maxIdle = maxIdle;
}
public void setMinIdle(int minIdle) {
if (minIdle < 0) {
throw new IllegalArgumentException("minIdle should not be negative.");
}
this.minIdle = minIdle;
}
//...省略getter方法...
}
// ResourcePoolConfig使用举例
ResourcePoolConfig config = new ResourcePoolConfig("dbconnectionpool");
config.setMaxTotal(16);
config.setMaxIdle(8);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
通过构造函数设置必填项,通过 set() 方法设置可选配置项,就能实现设计需求。如果把问题的难度再加大点,比如,还需要解决下面这三个问题,那现在的设计思路就不能满足了。
- 首先 name 是必填的,所以把它放到构造函数中,强制创建对象的时候就设置。如果必填的配置项有很多,把这些必填配置项都放到构造函数中设置,那构造函数就又会出现参数列表很长的问题。如果把必填项也通过 set() 方法设置,那校验这些必填项是否已经填写的逻辑就无处安放。
- 假设配置项之间有一定的依赖关系,比如,如果用户设置了 maxTotal、maxIdle、minIdle 其中一个,就必须显式地设置另外两个;或者配置项之间有一定的约束条件,比如,maxIdle 和 minIdle 要小于等于 maxTotal。如果继续使用现在的设计思路,那这些配置项之间的依赖关系或者约束条件的校验逻辑就无处安放。
- 如果希望 ResourcePoolConfig 类对象是不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值。要实现这个功能就不能在 ResourcePoolConfig 类中暴露 set() 方法。
为了解决这些问题,建造者模式就派上用场了。可以把校验逻辑放置到 Builder 类中,先创建建造者,并且通过 set() 方法设置建造者的变量值,然后在使用 build() 方法真正创建对象之前,做集中的校验,校验通过之后才会创建对象。除此之外把 ResourcePoolConfig 的构造函数改为 private 私有权限。这样就只能通过建造者来创建 ResourcePoolConfig 类对象。并且 ResourcePoolConfig 没有提供任何 set() 方法,这样创建出来的对象就是不可变对象了。
public class ResourcePoolConfig {
private String name;
private int maxTotal;
private int maxIdle;
private int minIdle;
private ResourcePoolConfig(Builder builder) {
this.name = builder.name;
this.maxTotal = builder.maxTotal;
this.maxIdle = builder.maxIdle;
this.minIdle = builder.minIdle;
}
//...省略getter方法...
//我们将Builder类设计成了ResourcePoolConfig的内部类。
//我们也可以将Builder类设计成独立的非内部类ResourcePoolConfigBuilder。
public static class Builder {
private static final int DEFAULT_MAX_TOTAL = 8;
private static final int DEFAULT_MAX_IDLE = 8;
private static final int DEFAULT_MIN_IDLE = 0;
private String name;
private int maxTotal = DEFAULT_MAX_TOTAL;
private int maxIdle = DEFAULT_MAX_IDLE;
private int minIdle = DEFAULT_MIN_IDLE;
public ResourcePoolConfig build() {
// 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("...");
}
if (maxIdle > maxTotal) {
throw new IllegalArgumentException("...");
}
if (minIdle > maxTotal || minIdle > maxIdle) {
throw new IllegalArgumentException("...");
}
return new ResourcePoolConfig(this);
}
public Builder setName(String name) {
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("...");
}
this.name = name;
return this;
}
public Builder setMaxTotal(int maxTotal) {
if (maxTotal <= 0) {
throw new IllegalArgumentException("...");
}
this.maxTotal = maxTotal;
return this;
}
public Builder setMaxIdle(int maxIdle) {
if (maxIdle < 0) {
throw new IllegalArgumentException("...");
}
this.maxIdle = maxIdle;
return this;
}
public Builder setMinIdle(int minIdle) {
if (minIdle < 0) {
throw new IllegalArgumentException("...");
}
this.minIdle = minIdle;
return this;
}
}
}
// 这段代码会抛出IllegalArgumentException,因为minIdle>maxIdle
ResourcePoolConfig config = new ResourcePoolConfig.Builder()
.setName("dbconnectionpool")
.setMaxTotal(16)
.setMaxIdle(10)
.setMinIdle(12)
.build();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
使用建造者模式创建对象,还能避免对象存在无效状态。比如定义了一个长方形类,如果不使用建造者模式,采用先创建后 set 的方式,那就会导致在第一个 set 之后,对象处于无效状态。
Rectangle r = new Rectange(); // r is invalid
r.setWidth(2); // r is invalid
r.setHeight(3); // r is valid
2
3
为了避免这种无效状态的存在,就需要使用构造函数一次性初始化好所有的成员变量。如果构造函数参数过多,就需要考虑使用建造者模式,先设置建造者的变量,然后再一次性地创建对象,让对象一直处于有效状态。实际上,如果并不是很关心对象是否有短暂的无效状态,也不是太在意对象是否是可变的。比如对象只是用来映射数据库读出来的数据,那直接暴露 set() 方法来设置类的成员变量值是完全没问题的。而且,使用建造者模式来构建对象,代码实际上是有点重复的,ResourcePoolConfig 类中的成员变量,要在 Builder 类中重新再定义一遍。
# 3.2 与工厂模式的区别
建造者模式是让建造者类来负责对象的创建工作。工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”地创建不同的对象。
网上有一个经典的例子很好地解释了两者的区别:顾客走进一家餐馆点餐,我们利用工厂模式,根据用户不同的选择,来制作不同的食物,比如披萨、汉堡、沙拉。对于披萨来说,用户又有各种配料可以定制,比如奶酪、西红柿、起司,我们通过建造者模式根据用户选择的不同配料来制作披萨。
# 4. 原型模式
# 4.1 原型模式的原理与应用
如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式(Prototype Design Pattern),简称原型模式。
实际上,创建对象包含的申请内存、给成员变量赋值这一过程,对于大部分业务系统来说这点时间完全是可以忽略。应用一个复杂的模式,只得到一点点的性能提升,这就是所谓的过度设计。
但如果对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者需要从 RPC、网络、数据库、文件系统等非常慢速的 IO 中读取,这种情况下就可以利用原型模式,从其他已有对象中直接拷贝得到,而不用每次在创建新对象的时候,都重复执行这些耗时的操作。
通过例子来理解:
假设数据库中存储了大约 10 万条“搜索关键词”信息,每条信息包含关键词、关键词被搜索的次数、信息最近被更新的时间等。系统 A 在启动的时候会加载这份数据到内存中,用于处理某些其他的业务需求。为了方便快速地查找某个关键词对应的信息可以给关键词建立一个散列表索引。对于 Java 语言,可以直接使用语言中提供的 HashMap 容器来实现。其中,HashMap 的 key 为搜索关键词,value 为关键词详细信息(比如搜索次数)。
不过还有另外一个系统 B,专门用来分析搜索日志,定期(比如间隔 10 分钟)批量地更新数据库中的数据,并且标记为新的数据版本。比如,在下面的示例图中,对 v2 版本的数据进行更新,得到 v3 版本的数据。这里假设只有更新和新添关键词,没有删除关键词的行为。
为了保证系统 A 中数据的实时性(不一定非常实时,但数据也不能太旧),系统 A 需要定期根据数据库中的数据,更新内存中的索引和数据。
可以使用增量更新的方式,从数据库中取出更新时间大于上次的所有搜索关键词,也就是找出当前版本与最新版本数据的“差集”,然后针对差集中的每个关键词进行处理,如果已存在就更新;如果不存在就将插入。
public class Demo {
private ConcurrentHashMap<String, SearchWord> currentKeywords = new ConcurrentHashMap<>();
private long lastUpdateTime = -1;
public void refresh() {
// 从数据库中取出更新时间>lastUpdateTime的数据,放入到currentKeywords中
List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime);
long maxNewUpdatedTime = lastUpdateTime;
for (SearchWord searchWord : toBeUpdatedSearchWords) {
if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) {
maxNewUpdatedTime = searchWord.getLastUpdateTime();
}
if (currentKeywords.containsKey(searchWord.getKeyword())) {
currentKeywords.replace(searchWord.getKeyword(), searchWord);
} else {
currentKeywords.put(searchWord.getKeyword(), searchWord);
}
}
lastUpdateTime = maxNewUpdatedTime;
}
private List<SearchWord> getSearchWords(long lastUpdateTime) {
// TODO: 从数据库中取出更新时间>lastUpdateTime的数据
return null;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
现在有一个特殊的要求:任何时刻,系统 A 中的所有数据都必须是同一个版本的,要么都是版本 a,要么都是版本 b,不能有的是版本 a,有的是版本 b。那刚刚的更新方式就不能满足这个要求了。除此之外还要求:在更新内存数据的时候,系统 A 不能处于不可用状态,也就是不能停机更新数据。
实际上,也不难。把正在使用的数据的版本定义为“服务版本”,当要更新内存中的数据的时候,并不是直接在服务版本(假设是版本 a 数据 currentKeywords)上更新,而是重新创建另一个版本数据(假设是版本 b 数据 newKeywords),等新的版本数据建好之后,再一次性地将服务版本从版本 a 切换到版本 b。这样既保证了数据一直可用,又避免了中间状态的存在。
按照这个思路存在一个问题,即获取新的版本的方式如果从数据库中重新获取,需要将这 10 万条数据从数据库中读出,然后计算哈希值。这个过程显然是比较耗时。为了提高效率,原型模式就派上用场了。
为了提高效率,如果拷贝 currentKeywords 数据到 newKeywords 中,然后从数据库中只捞出新增或者有更新的关键词,更新到 newKeywords 中。而相对于 10 万条数据来说,每次新增或者更新的关键词个数是比较少的,所以,这种策略大大提高了数据更新的效率。
public class Demo {
private HashMap<String, SearchWord> currentKeywords=new HashMap<>();
private long lastUpdateTime = -1;
public void refresh() {
// 原型模式就这么简单,拷贝已有对象的数据,更新少量差值
HashMap<String, SearchWord> newKeywords = (HashMap<String, SearchWord>) currentKeywords.clone();
// 从数据库中取出更新时间>lastUpdateTime的数据,放入到newKeywords中
List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime);
long maxNewUpdatedTime = lastUpdateTime;
for (SearchWord searchWord : toBeUpdatedSearchWords) {
if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) {
maxNewUpdatedTime = searchWord.getLastUpdateTime();
}
if (newKeywords.containsKey(searchWord.getKeyword())) {
SearchWord oldSearchWord = newKeywords.get(searchWord.getKeyword());
oldSearchWord.setCount(searchWord.getCount());
oldSearchWord.setLastUpdateTime(searchWord.getLastUpdateTime());
} else {
newKeywords.put(searchWord.getKeyword(), searchWord);
}
}
lastUpdateTime = maxNewUpdatedTime;
currentKeywords = newKeywords;
}
private List<SearchWord> getSearchWords(long lastUpdateTime) {
// TODO: 从数据库中取出更新时间>lastUpdateTime的数据
return null;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 4.2 原型模式的实现方式:深拷贝和浅拷贝
容易发现上面代码中对象拷贝使用了浅拷贝,即 newKeywords 和 currentKeywords 共享 SearchWord 对象。
HashMap<String, SearchWord> newKeywords = (HashMap<String, SearchWord>) currentKeywords.clone();
当修改 newKeywords 更新 SearchWord 对象的时候,就会导致 currentKeywords 中指向的 SearchWord,有的是老版本的,有的是新版本的,就没法满足之前的需求:currentKeywords 中的数据在任何时刻都是同一个版本的,不存在介于老版本与新版本之间的中间状态。
解决方式:将浅拷贝替换为深拷贝。
第一种方法:递归拷贝对象、对象的引用对象以及引用对象的引用对象,直到要拷贝的对象只包含基本数据类型数据,没有引用对象为止。即将上面浅拷贝代码替换为:
HashMap<String, SearchWord> newKeywords = new HashMap<>();
for (HashMap.Entry<String, SearchWord> e : currentKeywords.entrySet()) {
SearchWord searchWord = e.getValue();
SearchWord newSearchWord = new SearchWord(
searchWord.getKeyword(), searchWord.getCount(), searchWord.getLastUpdateTime());
newKeywords.put(e.getKey(), newSearchWord);
}
2
3
4
5
6
7
第二种方法:先将对象序列化,然后再反序列化成新的对象。
public Object deepCopy(Object object) {
ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream oo = new ObjectOutputStream(bo);
oo.writeObject(object);
ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
ObjectInputStream oi = new ObjectInputStream(bi);
return oi.readObject();
}
2
3
4
5
6
7
8
9
10
这两种深度拷贝的方式都要比浅拷贝耗时、耗内存空间。其实可以先采用浅拷贝的方式创建 newKeywords,对于需要更新的 SearchWord 对象,再使用深度拷贝的方式创建一份新的对象,替换 newKeywords 中的老对象。毕竟需要更新的数据是很少的。这种方式即利用了浅拷贝节省时间、空间的优点,又能保证 currentKeywords 中的中数据都是老版本的数据。代码如下:
public class Demo {
private HashMap<String, SearchWord> currentKeywords=new HashMap<>();
private long lastUpdateTime = -1;
public void refresh() {
// Shallow copy
HashMap<String, SearchWord> newKeywords = (HashMap<String, SearchWord>) currentKeywords.clone();
// 从数据库中取出更新时间>lastUpdateTime的数据,放入到newKeywords中
List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime);
long maxNewUpdatedTime = lastUpdateTime;
for (SearchWord searchWord : toBeUpdatedSearchWords) {
if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) {
maxNewUpdatedTime = searchWord.getLastUpdateTime();
}
if (newKeywords.containsKey(searchWord.getKeyword())) {
newKeywords.remove(searchWord.getKeyword());
}
newKeywords.put(searchWord.getKeyword(), searchWord);
}
lastUpdateTime = maxNewUpdatedTime;
currentKeywords = newKeywords;
}
private List<SearchWord> getSearchWords(long lastUpdateTime) {
// TODO: 从数据库中取出更新时间>lastUpdateTime的数据
return null;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31