4 工厂&建造者&原型模式


本文将介绍工厂模式、建造者模式、原型模式
## 工厂模式【常用】
工厂模式很重要,后面的很多架构设计都是工厂模式联合其它设计模式使用。
1. 一般情况下,工厂模式分为三种更加细分的类型:简单工厂、工厂方法和抽象工厂。不过,在 GoF 的《设计模式》一书中,将简单工厂模式看作是工厂方法模式的一种特例,所以工厂模式只被分成了工厂方法和抽象工厂两类。实际上,前面一种分类方法更加常见。因此,下面的介绍沿用第一种分类方法。 2. 在这三种细分的工厂模式中,简单工厂、工厂方法原理比较简单,在实际的项目中也比较常用。而抽象工厂的原理稍微复杂点,在实际的项目中相对也不常用。所以重点是前两种工厂模式。对于抽象工厂,稍微了解一下即可。 3. 除此之外,下面叙述的重点也不是原理和实现,因为这些都很简单,重点还是搞清楚应用场景:什么时候该用工厂模式?相对于直接 `new`来创建对象,用工厂模式来创建究竟有什么好处呢?

简单工厂(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";
  	}
}

为了让代码的逻辑更加清晰、可读性更好,将功能独立的代码块封装成函数。按照这个设计思路,可以将代码中涉及parser创建的部分逻辑剥离出来,抽象成createParser()函数。重构之后的代码如下所示:

public RuleConfig load(String ruleConfigFilePath) {
	String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
    IRuleConfigParser parser = createParser(ruleConfigFileExtension);
    if (parser == null) {
      	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";
}

private 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;
}

为了让类的职责更加单一(设计模式原则中的单一职责)、代码更加清晰,还可以进一步将createParser()函数剥离到一个独立的类中,让这个类只负责对象的创建。而这个类就是现在要讲的简单工厂模式类。具体的代码如下所示:

// 简单工厂模式的第一种实现方法
public class RuleConfigSource {
  	public RuleConfig load(String ruleConfigFilePath) {
		String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
		IRuleConfigParser parser = RuleConfigParserFactory.createParser(ruleConfigFileExtension);
		if (parser == null) {
			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";
  	}
}

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;
  	}
}
  1. 在上面的代码实现中,每次调用RuleConfigParserFactorycreateParser()的时候,都要创建一个新的parser
  2. 实际上,如果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;
  	}
}
  1. 对于上面两种简单工厂模式的实现方法,如果要添加新的parser,那势必要改动到RuleConfigParserFactory的代码,那这是不是违反开闭原则呢?实际上,如果不是需要频繁地添加新的parser,只是偶尔修改一下 RuleConfigParserFactory代码,稍微不符合开闭原则,也是完全可以接受的。
  2. 除此之外,在RuleConfigParserFactory的第一种代码实现中,有一组if分支判断逻辑,是不是应该用多态或其他设计模式来替代呢?实际上,如果if分支并不是很多,代码中有if分支也是完全可以接受的。应用多态或设计模式来替代if分支判断逻辑,也并不是没有任何缺点的,它虽然提高了代码的扩展性,更加符合开闭原则,但也增加了类的个数,牺牲了代码的可读性。
  3. 总结一下,尽管简单工厂模式的代码实现中,有多处if分支判断逻辑,违背开闭原则,但权衡扩展性和可读性,这样的代码实现在大多数情况下(比如,不需要频繁地添加parser,也没有太多的parser)是没有问题的。

工厂方法

如果非要将if分支逻辑去掉,该怎么办呢?比较经典处理方法就是利用多态。按照多态的实现思路,对上面的代码进行重构。重构之后的代码如下所示:

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();
  	}
}

实际上,这就是工厂方法模式的典型代码实现。当我们新增一种parser的时候,只需要新增一个实现了 IRuleConfigParserFactory接口的Factory类即可。所以,工厂方法模式比起简单工厂模式更加符合开闭原则。

从上面的工厂方法的实现来看,一切都很完美,但是实际上存在挺大的问题。问题存在于这些工厂类的使用上。接下来看一下,如何用这些工厂类来实现RuleConfigSourceload()函数。具体的代码如下所示:

public class RuleConfigSource {
  	public RuleConfig load(String ruleConfigFilePath) {
    	String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
    	IRuleConfigParserFactory parserFactory = null;
    	if ("json".equalsIgnoreCase(ruleConfigFileExtension)) {
      		parserFactory = new JsonRuleConfigParserFactory();
    	} else if ("xml".equalsIgnoreCase(ruleConfigFileExtension)) {
      		parserFactory = new XmlRuleConfigParserFactory();
    	} else if ("yaml".equalsIgnoreCase(ruleConfigFileExtension)) {
      		parserFactory = new YamlRuleConfigParserFactory();
    	} else if ("properties".equalsIgnoreCase(ruleConfigFileExtension)) {
      		parserFactory = new PropertiesRuleConfigParserFactory();
    	} else {
      		throw new InvalidRuleConfigException("Rule config file format is not supported: " + ruleConfigFilePath);
    	}
    	IRuleConfigParser parser = parserFactory.createParser();

    	String configText = "";
    	// 从ruleConfigFilePath文件中读取配置文本到configText中
    	RuleConfig ruleConfig = parser.parse(configText);
    	return ruleConfig;
  	}

  	private String getFileExtension(String filePath) {
    	// ...解析文件名获取扩展名,比如rule.json,返回json
    	return "json";
  	}
}
  1. 从上面的代码实现来看,工厂类对象的创建逻辑又耦合进了load()函数中,跟最初的代码版本非常相似,引入工厂方法非但没有解决问题,反倒让设计变得更加复杂了。那怎么来解决这个问题呢?
  2. 可以为工厂类再创建一个简单工厂,也就是工厂的工厂,用来创建工厂类对象。其中,RuleConfigParserFactoryMap类是创建工厂对象的工厂类,getParserFactory()返回的是缓存好的单例工厂对象。
public class RuleConfigSource {
  	public RuleConfig load(String ruleConfigFilePath) {
    	String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
    	IRuleConfigParserFactory parserFactory = RuleConfigParserFactoryMap.getParserFactory(ruleConfigFileExtension);
    	if (parserFactory == null) {
      		throw new InvalidRuleConfigException("Rule config file format is not supported: " + ruleConfigFilePath);
    	}
    	IRuleConfigParser parser = parserFactory.createParser();

    	String configText = "";
    	// 从ruleConfigFilePath文件中读取配置文本到configText中
    	RuleConfig ruleConfig = parser.parse(configText);
    	return ruleConfig;
  	}

  	private String getFileExtension(String filePath) {
    	// ...解析文件名获取扩展名,比如rule.json,返回json
    	return "json";
  	}
}

// 因为工厂类只包含方法,不包含成员变量,完全可以复用,
// 不需要每次都创建新的工厂类对象,所以,简单工厂模式的第二种实现思路更加合适。
public class RuleConfigParserFactoryMap { 	// 工厂的工厂
  	private static final Map<String, IRuleConfigParserFactory> cachedFactories = new HashMap<>();

  	static {
        cachedFactories.put("json", new JsonRuleConfigParserFactory());
        cachedFactories.put("xml", new XmlRuleConfigParserFactory());
        cachedFactories.put("yaml", new YamlRuleConfigParserFactory());
        cachedFactories.put("properties", new PropertiesRuleConfigParserFactory());
	}

  	public static IRuleConfigParserFactory getParserFactory(String type) {
    	if (type == null || type.isEmpty()) {
      		return null;
    	}
    	IRuleConfigParserFactory parserFactory = cachedFactories.get(type.toLowerCase());
    	return parserFactory;
  	}
}
  1. 当需要添加新的规则配置解析器的时候,只需要创建新的parser类和parser factory类,并且在 RuleConfigParserFactoryMap类中,将新的parser factory对象添加到cachedFactories中即可。代码的改动非常少,基本上符合开闭原则。
  2. 实际上,对于规则配置文件解析这个应用场景来说,工厂模式需要额外创建诸多Factory类,也会增加代码的复杂性,而且每个Factory类只是做简单的new操作,功能非常单薄(只有一行代码),也没必要设计成独立的类,所以,在这个应用场景下,简单工厂模式简单好用,比工厂方法模式更加合适。

那什么时候该用工厂方法模式,而非简单工厂模式呢?

  1. 前面提到,之所以将某个代码块剥离出来,独立为函数或者类,原因是这个代码块的逻辑过于复杂,剥离之后能让代码更加清晰,更加可读、可维护。但是,如果代码块本身并不复杂,就几行代码而已,我们完全没必要将它拆分成单独的函数或者类。
  2. 基于这个设计思想,当对象的创建逻辑比较复杂,不只是简单的new一下就可以,而是要组合其他类对象,做各种初始化操作的时候,推荐使用工厂方法模式,将复杂的创建逻辑拆分到多个工厂类中,让每个工厂类都不至于过于复杂。而使用简单工厂模式,将所有的创建逻辑都放到一个工厂类中,会导致这个工厂类变得很复杂。
  3. 除此之外,在某些场景下如果对象不可复用,那工厂类每次都要返回不同的对象。如果使用简单工厂模式来实现,就只能选择第一种包含if分支逻辑的实现方式。如果还想避免烦人的if-else分支逻辑,这个时候,就推荐使用工厂方法模式。

抽象工厂(Abstract Factory)

  1. 讲完了简单工厂、工厂方法,再来看抽象工厂模式。抽象工厂模式的应用场景比较特殊,没有前两种常用,所以不是学习的重点,你简单了解一下就可以了。
  2. 在简单工厂和工厂方法中,类只有一种分类方式。比如,在规则配置解析例子中,解析器类只会根据配置文件格式(Json、Xml、Yaml……)来分类。但是,如果类有两种分类方式,比如,既可以按照配置文件格式来分类,也可以按照解析的对象(Rule规则配置还是System系统配置)分,那就会对应下面这 8 个parser类。
针对规则配置的解析器:基于接口IRuleConfigParser
JsonRuleConfigParser
XmlRuleConfigParser
YamlRuleConfigParser
PropertiesRuleConfigParser

针对系统配置的解析器:基于接口ISystemConfigParser
JsonSystemConfigParser
XmlSystemConfigParser
YamlSystemConfigParser
PropertiesSystemConfigParser
  1. 针对这种特殊的场景,如果还是继续用工厂方法来实现的话,要针对每个parser都编写一个工厂类,也就是要编写 8 个工厂类。如果未来还需要增加针对业务配置的解析器(比如 IBizConfigParser),那就要再对应地增加 4 个工厂类。而我们知道,过多的类也会让系统难维护。这个问题该怎么解决呢?
  2. 抽象工厂就是针对这种非常特殊的场景而诞生的。可以让一个工厂负责创建多个不同类型的对象(IRuleConfigParserISystemConfigParser等),而不是只创建一种 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代码

如何设计实现一个Dependency Injection框架?

当创建对象是一个“大工程”的时候,一般会选择使用工厂模式来封装对象复杂的创建过程,将对象的创建和使用分离,让代码更加清晰。那何为“大工程”呢?上面讲了两种情况,一种是创建过程涉及复杂的if-else分支判断,另一种是对象创建需要组装多个其他类对象或者需要复杂的初始化过程。

下面再讲一个创建对象的“大工程”,依赖注入框架,或者叫依赖注入容器(Dependency Injection Container),简称DI容器。目的在于搞懂以下几个问题:DI容器跟工厂模式有何区别和联系?DI容器的核心功能有哪些,以及如何实现一个简单的DI容器?

工厂模式和 DI 容器有何区别?

  1. 实际上,DI容器底层最基本的设计思路就是基于工厂模式的。DI容器相当于一个大的工厂类,负责在程序启动的时候,根据配置(要创建哪些类对象,每个类对象的创建需要依赖哪些其他类对象)事先创建好对象。当应用程序需要使用某个类对象的时候,直接从容器中获取即可。正是因为它持有一堆对象,所以这个框架才被称为“容器”。
  2. DI容器相对于上面讲的工厂模式的例子来说,它处理的是更大的对象创建工程。上面讲的工厂模式中,一个工厂类只负责某个类对象或者某一组相关类对象(继承自同一抽象类或者接口的子类)的创建,而DI容器负责的是整个应用中所有类对象的创建。
  3. 除此之外,DI容器负责的事情要比单纯的工厂模式要多。比如,它还包括配置的解析、对象生命周期的管理等。

DI 容器的核心功能有哪些?

总结一下,一个简单的DI容器的核心功能一般有三个:配置解析、对象创建和对象生命周期管理。

首先来看配置解析。
1. 在上面讲的工厂模式中,工厂类要创建哪个类对象是事先确定好的,并且是写死在工厂类代码中的。作为一个通用的框架来说,框架代码跟应用代码应该是高度解耦的,`DI`容器事先并不知道应用会创建哪些对象,不可能把某个应用要创建的对象写死在框架代码中。所以需要通过一种形式,让应用告知`DI`容器要创建哪些对象。 2. 将需要由`DI`容器来创建的类对象和创建类对象的必要信息(使用哪个构造函数以及对应的构造函数参数都是什么等等),放到配置文件中。容器读取配置文件,根据配置文件提供的信息来创建对象。 3. 下面是一个典型的 Spring 容器的配置文件。Spring 容器读取这个配置文件,解析出要创建的两个对象:`rateLimiter`和`redisCounter`,并且得到两者的依赖关系:`rateLimiter`依赖`redisCounter`。
<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>
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;
  	}
  	// ...
}
其次,再来看对象创建。
1. 在`DI`容器中,如果给每个类都对应创建一个工厂类,那项目中类的个数会成倍增加,这会增加代码的维护成本。要解决这个问题并不难。只需要将所有类对象的创建都放到一个工厂类中完成就可以了,比如`BeansFactory`。 2. 如果要创建的类对象非常多,BeansFactory 中的代码会不会线性膨胀(代码量跟创建对象的个数成正比)呢?实际上并不会。下面讲到`DI`容器的具体实现时,会讲`反射`机制,它能在程序运行的过程中,动态地加载类、创建对象,不需要事先在代码中写死要创建哪些对象。所以,不管是创建一个对象还是十个对象,`BeansFactory`工厂类代码都是一样的。
最后,来看对象的生命周期管理。
1. 上面讲到,简单工厂模式有两种实现方式,一种是每次都返回新创建的对象,另一种是每次都返回同一个事先创建好的对象,也就是所谓的单例对象。在 Spring 框架中,可以通过配置`scope`属性来区分这两种不同类型的对象。`scope=prototype`表示返回新创建的对象,`scope=singleton`表示返回单例对象。 2. 除此之外,还可以配置对象是否支持懒加载。如果`lazy-init = true`,对象在真正被使用到的时候(比如:`BeansFactory.getBean("userService")`)才被被创建;如果`lazy-init=false`,对象在应用启动的时候就事先创建好。 3. 不仅如此,还可以配置对象的`init-method`和`destroy-method`方法,比如`init-method = loadProperties()`,`destroy-method = updateConfigFile()`。`DI`容器在创建好对象之后,会主动调用`init-method`属性指定的方法来初始化对象。在对象被最终销毁之前,`DI`容器会主动调用`destroy-method`属性指定的方法来做一些清理工作,比如释放数据库连接池、关闭文件。

如何实现一个简单的 DI 容器?

用 Java 语言来实现一个简单的DI容器,核心逻辑只需要包括两个部分:配置文件解析、根据配置文件通过反射语法来创建对象。

最小原型设计

因为主要涉及设计模式,所以只实现一个DI容器的最小原型。像Spring框架这样的DI容器,它支持的配置格式非常灵活和复杂。为了简化代码实现,重点讲解原理,在最小原型中,只支持下面配置文件中涉及的配置语法。

<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>
</bean

最小原型的使用方式跟Spring框架非常类似,示例代码如下所示:

public class Demo {
	public static void main(String[] args) {
		ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
		RateLimiter rateLimiter = (RateLimiter) applicationContext.getBean("rateLimiter");
		rateLimiter.test();
	}
}

提供执行入口

面向对象设计的最后一步是:组装类并提供执行入口。在这里,执行入口就是一组暴露给外部使用的接口和类。通过最小原型使用示例代码可以看出,执行入口主要包含两部分:ApplicationContextClassPathXmlApplicationContext。其中,ApplicationContext接口ClassPathXmlApplicationContext是接口的实现类。两个类具体实现如下所示:

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);
  	}
}

从上面的代码中可以看出,ClassPathXmlApplicationContext负责组装BeansFactoryBeanConfigParser两个类,串联执行流程:从classpath中加载XML格式的配置文件,通过BeanConfigParser解析为统一的BeanDefinition格式,然后,BeansFactory根据BeanDefinition来创建对象。

配置文件解析

配置文件解析主要包含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. 最后来看BeansFactory 是如何设计和实现的。这也是DI容器最核心的一个类了。它负责根据从配置文件解析得到的 BeanDefinition来创建对象。
  2. 如果对象的scope属性是singleton,那对象创建之后会缓存在singletonObjects 这样一个map中,下次再请求此对象的时候,直接从map中取出返回,不需要重新创建。如果对象的scope属性是prototype,那每次请求对象,BeansFactory都会创建一个新的对象返回。
  3. 实际上,BeansFactory创建对象用到的主要技术点就是 Java 中的反射语法:一种动态加载类和创建对象的机制。JVM在启动的时候会根据代码自动地加载类、创建对象。至于要加载哪些类、创建哪些对象,这些都是在代码中写死的,或者说提前写好的。但是,如果某个对象的创建并不是写死在代码中,而是放到配置文件中,需要在程序运行期间,动态地根据配置文件来加载类、创建对象,那这部分工作就没法让JVM帮我们自动完成了,我们需要利用 Java 提供的反射语法自己去编写代码。
  4. 搞清楚了反射的原理,BeansFactory 的代码就不难看懂了。具体代码实现如下所示:
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. 执行入口那里调用addBeanDefinitions
  2. 然后addBeanDefinitions再调用createBean利用反射创建对象,如果对象的 scope 属性是 singleton,那对象创建之后会缓存在singletonObjects这样一个 map 中
  3. 最后最小原型设计那里再调用getBeansingletonObjects获取对象。

建造者模式【常用】

建造者模式的原理和代码实现非常简单,掌握起来并不难,难点在于应用场景。比如,你有没有考虑过这样几个问题:直接使用构造函数或者配合set方法就能创建对象,为什么还需要建造者模式来创建呢?建造者模式和工厂模式都可以创建对象,那它们两个的区别在哪里呢?

为什么需要建造者模式?

  1. 在平时的开发中,创建一个对象最常用的方式是,使用new关键字调用类的构造函数来完成。那么什么情况下这种方式就不适用了,需要采用建造者模式来创建对象呢?

  2. 假设有这样一道面试题:需要定义一个资源池配置类ResourcePoolConfig。这里的资源池,可以简单理解为线程池、连接池、对象池等。在这个资源池配置类中,有以下几个成员变量,也就是可配置项。现在,请你编写代码实现这个 ResourcePoolConfig类。

只要稍微有点开发经验,那实现这样一个类对你来说并不是件难事。最常见、最容易想到的实现思路如下代码所示。因为 maxTotalmaxIdleminIdle不是必填变量,所以在创建ResourcePoolConfig对象的时候,通过往构造函数中给这几个参数传递null值,来表示使用默认值。

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方法...
}

现在,ResourcePoolConfig只有 4 个可配置项,对应到构造函数中,也只有 4 个参数,参数的个数不多。但是,如果可配置项逐渐增多,变成了 8 个、10 个,甚至更多,那继续沿用现在的设计思路,构造函数的参数列表会变得很长,代码在可读性和易用性上都会变差。在使用构造函数的时候,就容易搞错各参数的顺序,传递进错误的参数值,导致非常隐蔽的bug

// 参数太多,导致可读性差、参数可能传递错误
ResourcePoolConfig config = new ResourcePoolConfig("dbconnectionpool", 16, null, 8, null, false , true, 10, 20falsetrue);

解决这个问题的办法就是用set()函数来给成员变量赋值,以替代冗长的构造函数。具体代码如下。其中,配置项name 是必填的,所以把它放到构造函数中设置,强制创建类对象的时候就要填写。其他配置项maxTotalmaxIdleminIdle都不是必填的,所以通过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使用
ResourcePoolConfig config = new ResourcePoolConfig("dbconnectionpool");
config.setMaxTotal(16);
config.setMaxIdle(8);

至此,仍然没有用到建造者模式,通过构造函数设置必填项,通过set()方法设置可选配置项,就能实现设计需求。如果把问题的难度再加大点,比如,还需要解决下面这三个问题,那现在的设计思路就不能满足了。

  • 刚刚讲到,name是必填项,所以把它放到构造函数中,强制创建对象的时候就设置。如果必填的配置项有很多,把这些必填配置项都放到构造函数中设置,那构造函数就又会出现参数列表很长的问题。如果把必填项也通过set()方法设置,那校验这些必填项是否已经填写的逻辑就无处安放了。
  • 除此之外,假设配置项之间有一定的依赖关系,比如,如果用户设置了maxTotalmaxIdleminIdle的其中一个,就必须显式地设置另外两个;或者配置项之间有一定的约束条件,比如,maxIdleminIdle要小于等于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();

实际上,使用建造者模式创建对象还能避免对象存在无效状态。比如定义了一个长方形类,如果不使用建造者模式,采用先创建后set的方式,就会导致在第一个set之后,对象处于无效状态。具体代码如下所示

Rectangle r = new Rectange(); // r is invalid
r.setWidth(2); // r is invalid
r.setHeight(3); // r is valid
        这里是说,长方形必须同时具备宽、高两个属性才是一个有效的长方形。只有其中一个属性,这个长方形对象就没有意义,是无效的。
1. 为了避免这种无效状态的存在,就需要使用构造函数一次性初始化好所有的成员变量。如果构造函数参数过多,就需要考虑使用建造者模式,**先设置建造者的变量,然后再一次性地创建对象,让对象一直处于有效状态**。(建造者主要解决参数过多、参数检验、控制对象创建后不可变的问题) 2. 实际上,如果并不是很关心对象是否有短暂的无效状态,也不是太在意对象是否是可变的。比如,对象只是用来映射数据库读出来的数据,那直接暴露`set()`方法来设置类的成员变量值是完全没问题的。而且,使用建造者模式来构建对象,代码实际上是有点重复的,`ResourcePoolConfig`类中的成员变量,要在`Builder`类中重新再定义一遍。

与工厂模式有何区别?

  1. 从上面的讲解中可以看出,建造者模式是让建造者类来负责对象的创建工作。工厂模式是由工厂类来负责对象创建的工作。那它们之间有什么区别呢?
  2. 实际上,工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”地创建不同的对象。

网上有一个经典的例子很好地解释了两者的区别:

        顾客走进一家餐馆点餐,利用工厂模式,根据用户不同的选择来制作不同的食物,比如披萨、汉堡、沙拉。对于披萨来说,用户又有各种配料可以定制,比如奶酪、西红柿、起司,通过建造者模式根据用户选择的不同配料来制作披萨。
实际上,我们也不要太学院派,非得把工厂模式、建造者模式分得那么清楚,我们需要知道的是,每个模式为什么这么设计,能解决什么问题。只有了解了这些最本质的东西,才能不生搬硬套,才能灵活应用,甚至可以混用各种模式创造出新的模式,来解决特定场景的问题。

原型模式【不常用】

原型模式和具体某一语言的语法机制无关,以下通过一个clone散列表的例子来搞清楚:原型模式的应用场景,以及它的两种实现方式:深拷贝和浅拷贝。

原型模式的原理与应用

如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式(Prototype Design Pattern),简称原型模式。

何为“对象的创建成本比较大”?
1. 实际上,创建对象包含的申请内存、给成员变量赋值这一过程,本身并不会花费太多时间,或者说对于大部分业务系统来说,这点时间完全是可以忽略的。应用一个复杂的模式,只得到一点点的性能提升,这就是所谓的过度设计,得不偿失。 2. **但是,**如果对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者需要从 RPC、网络、数据库、文件系统等非常慢速的 IO 中读取,这种情况下就可以利用原型模式,从其他已有对象中直接拷贝得到,而不用每次在创建新对象的时候,都重复执行这些耗时的操作。
接下来通过例子来解释。
1. 假设数据库中存储了大约 10 万条“搜索关键词”信息,每条信息包含关键词、关键词被搜索的次数、信息最近被更新的时间等。系统 A 在启动时会加载这份数据到内存中,用于处理某些其他的业务需求。为了方便快速地查找某个关键词对应的信息,给关键词建立一个散列表索引。
  1. Java可以直接使用语言中提供的HashMap容器来实现。其中,HashMapkey为搜索关键词,value为关键词详细信息(比如搜索次数)。只需要将数据从数据库中读取出来,放入HashMap就可以了。

  2. 不过,还有另外一个系统 B,专门用来分析搜索日志,定期(比如间隔 10 分钟)批量地更新数据库中的数据,并且标记为新的数据版本。比如,下面的示例图对 v2 版本的数据进行更新,得到 v3 版本的数据。假设只有更新和新添关键词,没有删除关键词的行为。

  3. 为了保证系统 A 中数据的实时性(不一定非常实时,但数据也不能太旧),系统 A 需要定期根据数据库中的数据,更新内存中的索引和数据。

  4. 该如何实现这个需求呢?我们只需要在系统 A 中,记录当前数据的版本 Va 对应的更新时间 Ta,从数据库中捞出更新时间大于 Ta 的所有搜索关键词,也就是找出 Va 版本与最新版本数据的“差集”,然后针对差集中的每个关键词进行处理。如果它已经在散列表中存在了,就更新相应的搜索次数、更新时间等信息;如果它在散列表中不存在,就将它插入到散列表中。

  5. 按照这个设计思路,设计代码如下所示:

    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;
      	}
    }

不过,现在有一个特殊的要求:任何时刻,系统 A 中的所有数据都必须是同一个版本的,要么都是版本 a,要么都是版本 b,不能有的是版本 a,有的是版本 b。那刚刚的更新方式就不能满足这个要求了(因为数据很多,一个一个的for循环肯定会出现不同数据版本的问题)。除此之外,还要求:在更新内存数据的时候,系统 A 不能处于不可用状态,也就是不能停机更新数据。

  1. 那该如何实现现在这个需求呢?

  2. 把正在使用的数据的版本定义为“服务版本”,当要更新内存中的数据的时候,并不是直接在服务版本(假设是版本 a 数据)上更新,而是重新创建另一个版本数据(假设是版本 b 数据),等新的版本数据建好之后,再一次性地将服务版本从版本 a 切换到版本 b。这样既保证了数据一直可用,又避免了中间状态的存在。

  3. 按照这个设计思路,设计代码如下所示:

    public class Demo {
        // 版本a
      	private HashMap<String, SearchWord> currentKeyword s= new HashMap<>();
    
      	public void refresh() {
            // 版本b
        	HashMap<String, SearchWord> newKeywords = new LinkedHashMap<>();
    
        	// 从数据库中取出所有满足条件的数据,放入到newKeywords中
        	List<SearchWord> toBeUpdatedSearchWords = getSearchWords();
        	for (SearchWord searchWord : toBeUpdatedSearchWords) {
          		newKeywords.put(searchWord.getKeyword(), searchWord);
        	}
    		// 一次性地将服务版本从版本 a 切换到版本 b
        	currentKeywords = newKeywords;
      	}
    
      	private List<SearchWord> getSearchWords() {
        	// TODO: 从数据库中取出所有的数据
        	return null;
      	}
    }
  4. 不过,在上面的代码实现中,newKeywords构建的成本比较高。需要将这 10 万条数据从数据库中读出,然后计算哈希值,构建newKeywords。这个过程显然是比较耗时。为了提高效率,原型模式就派上用场了。

  5. 拷贝currentKeywords数据到newKeywords中,然后从数据库中只捞出新增或者有更新的关键词,更新到 newKeywords中。而相对于 10 万条数据来说,每次新增或者更新的关键词个数是比较少的,所以,这种策略大大提高了数据更新的效率。

  6. 按照这个设计思路,设计代码如下所示:

    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;
      	}
    }
  7. 这里利用了 Java 中的clone()语法来复制对象。如果你熟悉的语言没有这个语法,那把数据从currentKeywords中一个个取出来,然后再重新计算哈希值,放入到 newKeywords 中也是可以接受的。毕竟,最耗时的还是从数据库中取数据的操作。相对于数据库的IO操作来说,内存操作和 CPU 计算的耗时都是可以忽略的。

  8. 实际上,刚刚的代码实现是有问题的。要弄明白到底有什么问题,需要先了解另外两个概念:深拷贝(Deep Copy)和浅拷贝(Shallow Copy)。

原型模式的实现方式:深拷贝和浅拷贝

    关于深拷贝浅拷贝,请参考文章:
> **我自己的总结:** > > **浅拷贝:对一个对象进行拷贝时,这个对象对应的类里的成员变量。** > > - 对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值拷贝,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据 > - 对于数据类型是引用数据类型的成员变量(也就是子对象,或者数组等),也就是只是将该成员变量的引用值(引用拷贝【并发引用传递,Java本质还是值传递】)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。 > > 1. **深拷贝**:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。 > 2. 也就是说浅拷贝对于子对象只是拷贝了引用值,并没有真正的拷贝整个对象。 > > **深拷贝实现思路:** > > 1. 对于每个子对象都实现Cloneable 接口,并重写clone方法。最后在最顶层的类的重写的 clone 方法中调用所有子对象的 clone 方法即可实现深拷贝。【简单的说就是:每一层的每个子对象都进行浅拷贝=深拷贝】 > 2. 利用序列化。【先对对象进行序列化,紧接着马上反序列化出 】

在内存中,用散列表组织的搜索关键词信息是如何存储的。从图中可以发现,散列表索引中,每个结点存储的key是搜索关键词,valueSearchWord对象的内存地址。SearchWord对象本身存储在散列表之外的内存空间中。

浅拷贝和深拷贝的区别在于,浅拷贝只会复制图中的索引(散列表),不会复制数据(SearchWord 对象)本身。相反,深拷贝不仅仅会复制索引,还会复制数据本身。浅拷贝得到的对象(newKeywords)跟原始对象(currentKeywords)共享数据(SearchWord 对象),而深拷贝得到的是一份完完全全独立的对象。具体的对比如下图所示:

  1. 在 Java 语言中,Object类的clone()方法执行的就是刚刚说的浅拷贝。它只会拷贝对象中的基本数据类型的数据(比如,int、long),以及引用对象(SearchWord)的内存地址,不会递归地拷贝引用对象本身。
  2. 上面的代码通过调用HashMap上的clone()浅拷贝方法来实现原型模式。当通过newKeywords更新SearchWord对象时(比如,更新“设计模式”这个搜索关键词的访问次数),newKeywordscurrentKeywords因为指向相同的一组 SearchWord对象,就会导致currentKeywords中指向的SearchWord,有的是老版本,有的是新版本,没法满足之前的需求:currentKeywords中的数据在任何时刻都是同一个版本的,不存在介于老版本与新版本之间的中间状态。
  3. 现在,我们又该如何来解决这个问题呢?
  4. 可以将浅拷贝替换为深拷贝。newKeywords不仅仅复制currentKeywords的索引,还把SearchWord对象也复制一份出来,这样newKeywordscurrentKeywords就指向不同的SearchWord对象,也就不存在更新newKeywords的数据会导致currentKeywords的数据也被更新的问题了。
  5. 那如何实现深拷贝呢?总结一下,有下面两种方法。

第一种方法:递归拷贝对象、对象的引用对象以及引用对象的引用对象……直到要拷贝的对象只包含基本数据类型数据,没有引用对象为止。根据这个思路对之前的代码进行重构。重构之后的代码如下所示:

public class Demo {
  	private HashMap<String, SearchWord> currentKeywords=new HashMap<>();
  	private long lastUpdateTime = -1;

  	public void refresh() {
    	// Deep copy
    	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);
		}

    	// 从数据库中取出更新时间 > 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;
  	}
}

第二种方法:先将对象序列化,然后再反序列化成新的对象。具体的示例代码如下所示:

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();
}
  1. 刚刚的两种实现方法,不管采用哪种,深拷贝都要比浅拷贝耗时、耗内存空间。针对我们这个应用场景,有没有更快、更省内存的实现方式呢?
  2. 可以先采用浅拷贝的方式创建newKeywords。对于需要更新的SearchWord对象,再使用深度拷贝的方式创建一份新的对象,替换newKeywords中的老对象。毕竟需要更新的数据是很少的。这种方式即利用了浅拷贝节省时间、空间的优点,又能保证currentKeywords中的数据都是老版本的数据。具体的代码实现如下所示。这也是标题中讲到的,在我们这个应用场景下,最快速clone散列表的方式。
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;
  	}
}

文章作者: Prannt
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Prannt !
评论
  目录