创建型设计模式汇总
1. 单例模式
1.1 单例模式的定义
一个类只允许创建一个对象或实例。
1.2 单例模式的作用
- 有些数据在系统中只应该保存一份,就比较适合设计为单例模式
1.3 单例模式的经典实现
饿汉式
public class Ehan {
private static Ehan instance = new Ehan();
private Ehan() {
}
public static Ehan getInstance() {
return instance;
}
}
优点:线程安全
缺点:无法支持延迟加载
懒汉式
public class Lhan {
private static Lhan lhan;
private Lhan() {
}
public static synchronized Lhan getInstance() {
if (lhan == null) {
lhan = new Lhan();
}
return lhan;
}
}
优点:支持延迟加载,并且线程安全
缺点:获取对象实例的方法加了一把大锁,导致函数并发效率很低。
双重检测锁
public class DoubleCheck {
private static DoubleCheck instance;
private DoubleCheck() {
}
public static DoubleCheck getInstance() {
if (instance == null) {
synchronized (DoubleCheck.class) {
if (instance == null) {
instance = new DoubleCheck();
}
}
}
return instance;
}
}
优点:线程安全并支持延迟加载。
缺点:在低版本 Java 中,由于创建对象的操作和初始化(也就是执行构造函数)不是原子操作,在指令重排序后,有可能会出现 instance 被赋值后,还未初始化就被其它线程调用了。
解决低版本 Java 中由于指令重排序,未初始化就被调用的问题
在 instance 成员变量加上 volatile 关键字,禁止重排序就可以了。
静态内部类
public class StaticInnerClass {
private StaticInnerClass() {
}
private static class Single {
private static StaticInnerClass instance = new StaticInnerClass();
}
public static StaticInnerClass getInstance() {
return Single.instance;
}
}
优点:线程安全由 JVM 来保证,同时支持延迟加载
缺点:无
权举
public enum SingleEnum {
INSTANCE;
private AtomicLong id = new AtomicLong();
public long getId() {
return id.incrementAndGet();
}
}
优点:线程安全
1.4 单例模式延迟加载优点和缺点
优点:当真正需要使用对象的时候,才去创建并初始化对象,避免提前初始化导致的内存浪费。
缺点:如果初始化耗时长,那真正使用的时候再去加载,会影响系统的性能(比如:会导致接口请求的响应时间变长,甚至超时)。
1.5 单例存在的问题
1. 单例会隐藏类之间的依赖关系
通过构造函数、参数传递等方式声明的类之间的依赖关系,我们通过查看函数的定义,就能很容易识别出来。但是,单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。如果代码比较复杂,这种调用关系就会非常隐蔽。在阅读代码的时候,我们就需要仔细查看每个函数的代码实现,才能知道这个类到底依赖了哪些单例类。
2. 单例对代码的扩展性不友好
如果未来某一天,我们需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动。
在系统设计初期,我们觉得系统中只应该有一个数据库连接池,这样能方便我们控制对数据库连接资源的消耗。所以,我们把数据库连接池类设计成了单例类。但之后我们发现,系统中有些 SQL 语句运行得非常慢。这些 SQL 语句在执行的时候,长时间占用数据库连接资源,导致其他 SQL 请求无法响应。为了解决这个问题,我们希望将慢 SQL 与其他 SQL 隔离开来执行。为了实现这样的目的,我们可以在系统中创建两个数据库连接池,慢 SQL 独享一个数据库连接池,其他 SQL 独享另外一个数据库连接池,这样就能避免慢 SQL 影响到其他 SQL 的执行。
3. 单例对代码的可测试性不友好
单例模式的使用会影响代码的可测试性,当单例类依赖比较重的外部资源,比如:DB,我们在写测试用例的时候希望通过 mock 的方式替换掉。而单例这种硬编码方式,导致无法实现 mock 替换。
1.6 单例模式的替代方案
- 静态方法实现
- 工厂模式
- IOC 容器
1.7 如何理解单例模式的唯一性
单例类中对象唯一性的作用范围是“进程唯一”的。
“集群唯一”指的是进程内唯一,进程间也唯一。
1.8 如何实现一个线程内唯一的单例
使用 HashMap 来存储对象,key 为线程 ID,值为实例对象,保证每个线程都对应一个单例对象。
当然,也可以通过 Java 中自带的 ThreadLocal 来实现线程唯一。
1.9 如何实现集群下的单例
需要将单例对象序列化到外部共享存储区(如:文件)。进程在使用这个对象时,需要先从外部共享存储区中将它读取到内存,并反序列化为对象后使用。同时,为了保证任何时候进程间都只有一份对象存在,一个进程在获取到对象后,需要对对象进行加锁,避免其它进程再将其获取。
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();
1.10 如何实现一个多例模式
多例模式表示可以创建有限多个实例。通过一个 Map 来存储对象类型和对象之间的对应关系,来控制对象的个数。
public class BackendServer {
private long serverNo;
private String serverAddress;
private static final int SERVER_COUNT = 3;
private static final Map 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. 工厂模式
2.1 工厂模式定义
工厂模式包括简单工厂、工厂方法和抽象工厂三个部分。其中,简单工厂可以看作工厂方法的一种特例形式。
2.2 工厂模式的作用
将类的创建与类的使用分离,职责单一,减少代码的复杂度,降低代码的耦合性,增加代码的可读性和可扩展性。
- 封装变化。如果逻辑有可能变化,封装成工厂类之后,创建逻辑的变更不会影响调用者。
- 代码复用。创建代码抽离到单独的工厂类之后可以复用。
- 隔离复杂性。封装复杂的创建逻辑,调用者无需知道对象是如何创建的。
- 控制复杂度。将创建代码抽离出来,让原本的函数或类职责更单一,代码更简洁。
2.3 工厂模式的经典实现
简单工厂
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;
}
}
特点:使用一个工厂类和一个方法来通过传入参数的不同类型来创建对应的对象。
工厂方法
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 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 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 时,使用抽象工厂实现方式,可以避免工厂方法实现中工厂类膨胀的问题。
总的来说就是让一个工厂负责多个不同类型对象的创建工作。
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.4 什么时候用简单工厂,什么时候用工厂方法,什么时候使用抽象工厂?
使用简单工厂的场景
一般需要工厂来创建的对象,逻辑并不复杂,通常只需要几行代码或者直接 new 出来就完成了创建。这种情况下,直接使用简单工厂比较合适。
一句话概括:对象的初始化比较简单,通过 new 就可以搞定的情况。
使用工厂方法的场景
当对象的创建逻辑比较复杂,不是简单使用 new 一下就可以,而是要依赖其它的类,做各种初始化的时候,推荐使用工厂方法模式。
还有一种是如果每次创建的对象都是不同的,不需要复用的情况下,如果想避免烦人的 if-else 分支逻辑,这个时候就可以使用工厂模式实现(通过在工厂的工厂中缓存创建对象所对应的工厂类,然后,直接通过类型来获取对应的工厂类,工厂类里面的实现是直接通过 new 来创建的)。
一名话概括:对象的创建比较复杂,需要依赖很多外部类,并且初始化过程比较复杂的情况。
使用抽象工厂的场景
如果类的分类方式大于或等于 2 时,使用抽象工厂实现方式,可以避免工厂方法实现中工厂类膨胀的问题。
总的来说就是让一个工厂负责多个不同类型对象的创建工作。
一句话概括:对象的创建分为 2 个或以上的维度(分类)
2.5 工厂模式和 DI 容器的区别
什么是 DI 容器
DI 容器是依赖注入框架。主要作用是根据配置文件创建并持有一堆的对象。
其主要职责有:配置的解析、对象的创建及其生命周期的管理。
DI 容器与工厂模式的关系
DI 容器的设计思路就是基于工厂模式的。DI 容器相当于一个“大工厂”,在程序启动的时候,通过配置(要创建哪些类对象,要创建的对象依赖哪些其它类等)事先创建好对象。当程序需要使用某个类的某个对象时,直接通过容器获取即可。
DI 的核心功能
- 配置的解析
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;
}
//...
}
Spring 容器的配置文件beans.xml:
- 对象的创建
通过一个工厂类,比如:BeanFactory,通过反射在程序运行过程中,动态地加载类并创建对象。不管创建几个对象,创建对象所对应的工厂是一样的。
- 对象的生命周期管理
DI 容器对生命周期的管理主要有对象是否是单例、是否支持懒加载、配置对象创建时的初始化方法和对象销毁后的销毁方法等。
DI 容器核心工厂类的设计
public class BeansFactory {
private ConcurrentHashMap singletonObjects = new ConcurrentHashMap<>();
private ConcurrentHashMap beanDefinitions = new ConcurrentHashMap<>();
public void addBeanDefinitions(List 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 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;
}
}
其核心思想是:从配置文件中解析得到 BeanDefinition(主要包括创建类所必须的数据),通过 BeanId 获取到对应的 BeanDefinition 对象,然后,根据 beanDefinitioin 相关的构造函数信息,通过反射来创建对应的对象。
3. 建造者模式
3.1 定义
建造者模式是用来创建一种类型的复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。
3.2 建造者模式作用
将类的创建与类的使用解耦,并解决构造方法过长导致容易出错的问题,或者通过构造方法和 set 方法配合使用时,可能出现的类之间的依赖或约束关系的逻辑无处安放。
3.3 构造方法 + set 方法方案可能导致的问题
解决方式
将必填参数写在构造函数中,将非必填参数使用 set 方法来提供配置性。
存在问题
- 必填的属性有很多,也会导致构造函数参数过长。如果将必填参数通过 set 来进行配置,校验必填属性是否已经填写的方式就无处安放了。
- 如果参数之间有一定的依赖或约束关系,那这些依赖或约束关系就无处安放了。
- 如果需要创建一个不可变对象,也就是创建后不能改变对象的属性,也就不能提供 set 方法了。
- 这种方法可能会出现无效状态,
Rectangle r = new Rectange(); // r is invalid
r.setWidth(2); // r is invalid
r.setHeight(3); // r is valid
而如果使用建造者函数则在创建对象之前,所有的参数已经配置完成。
3.4 使用构造函数或者配合 set 方法就能创建对象,为什么还需要建造者模式?
因为构造函数 + set 函数无法满足四种情况。分别是:
- 必填构造函数参数过长问题。
- 参数之间存在依赖或约束关系问题。
- 不可变对象无法提供 set 方法配置属性问题。
- 存在无效状态问题。
3.5 与工厂模式的区别
工厂模式是用来创建对象不同但相关类型的对象(继承了同一个父类,或者接口),通过参数来决定创建哪一类型的对象。而建造者模式是用来创建同一种类型的复杂对象,通过不同的参数来定制化创建不同的对象。
例子
你到餐馆去点餐,使用工厂模式选择不同类型的食物,比如:汉堡、披萨,选择了披萨后,再通过建造者模式来创建不同的披萨,比如:添加榴莲、香肠、起司等等。
4. 原型模式
4.1 定义
通过「复制」一个已经存在的实例来返回新的实例,而不是新建实例。 被复制的实例就是我们所称的「原型」,这个原型是可定制的。
4.2 原型模式的作用
如果对象的创建成本比较大,而同一类的不同对象之间的差异不大的情况下,可以通过直接拷贝(无需执行对象初始化)的方式,来节省创建对象的时间。
4.3 为何创建一个对象的成本比较大?
如果对象中的数据需要通过复杂的计算才能得到(比如:计算哈希值),或者需要通过 IO(如:文件,RPC,数据库等) 来进行数据的获取。
如果直接通过拷贝其它对象得到相应的数据,就不需要重复执行这些耗时的操作了。
4.3 原型模式的实现
浅拷贝
浅拷贝只会复制对象所对应的内存地址,不会复制对象本身。和原对象共享同一个对象,一方改变了对象的配置,另一方也会跟着改变。
深拷贝
深拷贝不仅会复制对象地址,还会复制对象本身。和原对象完全独立,互不影响。
4.4 对于基本数据类型的浅拷贝操作,对一个对象的值的改变会改变另一个对象的值么?
如果拷贝对象中只包含基本数据类型,对此对象的浅拷贝操作后,一个对象的值改变之后,另一下对象的值也会跟着改变。
为什么
首先,Object 对象的 clone 是没有实现的,需要具体的子类去实现,而如果直接返回 this 的话,直接改变某一个对象中基本数据类型的值,对应的值也会跟着改变。因为基本数据类型是本质上还是一个对象,只是被 JVM 通过自动装箱拆箱给处理了。
对于深拷贝
一个对象进行了深拷贝后,其内部的所有基本数据类型的值,都需要手动进行重新赋值给新对象,所以,是相互独立,互不影响的。
4.5 如何实现深拷贝呢?
- 递归地拷贝对象,对象的引用对象以及引用对象的引用对象,直到只剩下基本数据类型为止。
- 先将对象序列化,然后,再将数据反序列化。
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();
}
应用场景
clone 散列表
假设数据库中存储了大约 10 万条“搜索关键词”信息,每条信息包含关键词、关键词被搜索的次数、信息最近被更新的时间等。系统 A 在启动的时候会加载这份数据到内存中,用于处理某些其他的业务需求。为了方便快速地查找某个关键词对应的信息,我们给关键词建立一个散列表索引。
public class Demo {
private HashMap currentKeywords=new HashMap<>();
public void refresh() {
HashMap newKeywords = new LinkedHashMap<>(); // 从数据库中取出所有的数据,放入到 newKeywords 中
List toBeUpdatedSearchWords = getSearchWords();
for (SearchWord searchWord : toBeUpdatedSearchWords) {
newKeywords.put(searchWord.getKeyword(), searchWord);
}
currentKeywords = newKeywords;
}
private List getSearchWords() {
// TODO: 从数据库中取出所有的数据 return null;
}
}
在上面的使用场景中,newKeywords 对象构建的成本很高,需要将数据库中 10 万条数据读取出来,并进行 hash 运算后,再构造出对象,这个过程会非常耗时。
如果使用原型模式,先在内存中直接拷贝原对象,然后,再从数据库中取出新增或更新过了的数据,更新到 newKeywords 对象中,由于新增或更新的数据往往会比原始数据要少很多,所以,这样做,大大提高了数据更新的效率。
最优拷贝散列表的方式
先使用浅拷贝对原对象进行拷贝操作,来创建 newKeywords 对象,对于需要更新的 SearchWord 对象,我们再使用深拷贝的方式拷贝一份新的对象,替换 newKeywords 中的老对象。一般更新的数据整体来讲会少很多,利用浅拷贝既能节省时间、空间,又能保证老的 currentKeywords 中保留老版本的数据。
说明
此文是根据王争设计模式之美相关专栏内容整理而来,非原创。