设计模式与范式:创建型
单例设计模式(Singleton Design Pattern)
单例的定义
单例设计模式(Singleton Design Pattern)理解起来⾮常简单。⼀个类只允许创建⼀个对象(或者叫实例),那这个类就是⼀个单例类,这种设计模式就叫作单例设计模式,简称单例模式。
单例的⽤处
从业务概念上,有些数据在系统中只应该保存⼀份,就⽐较适合设计为单例类。⽐如,系统的配置信息类。除此之外,我们还可以使⽤单例解决资源访问冲突的问题。
单例存在哪些问题
单例对 OOP 特性的⽀持不友好
单例会隐藏类之间的依赖关系
单例对代码的扩展性不友好
单例对代码的可测试性不友好
单例不⽀持有参数的构造函数
单例有什么替代解决⽅案?
为了保证全局唯⼀,除了使⽤单例,我们还可以⽤静态⽅法来实现。不过,静态⽅法这种实现思路,并不能解决我们之前提到的问题。如果要完全解决这些问题,我们可能要从根上,寻找其他⽅式来实现全局唯⼀类了。⽐如,通过⼯⼚模式、IOC 容器(⽐如 Spring IOC 容器)来保证,由程序员⾃⼰来保证(⾃⼰在编写代码的时候⾃⼰保证不要创建两个类对象)。
有⼈把单例当作反模式,主张杜绝在项⽬中使⽤。我个⼈觉得这有点极端。模式没有对错,关键看你怎么⽤。如果单例类并没有后续扩展的需求,并且不依赖外部系统,那设计成单例类就没有太⼤问题。对于⼀些全局的类,我们在其他地⽅ new 的话,还要在类之间传来传去,不如直接做成单例类,使⽤起来简洁⽅便。
⼯⼚模式(Factory Design Pattern)
简单⼯场(Simple Factory)
实现⼀
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 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 = "" ; RuleConfig ruleConfig = parser.parse(configText); return ruleConfig; } private String getFileExtension (String filePath) { 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 ; } IRuleConfigParser parser = cachedParsers.get(configFormat.toLowerCase()); return parser; } }
⼯⼚⽅法模式
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 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 = "" ; RuleConfig ruleConfig = parser.parse(configText); return ruleConfig; } private String getFileExtension (String filePath) { 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; } }
那什么时候该⽤⼯⼚⽅法模式,⽽⾮简单⼯⼚模式呢?
我们前⾯提到,之所以将某个代码块剥离出来,独⽴为函数或者类,原因是这个代码块的逻辑过于复杂,剥离之后能让代码更加清晰,更加可读、可维护。但是,如果代码块本身并不复杂,就⼏⾏代码⽽已,我们完全没必要将它拆分成单独的函数或者类。
基于这个设计思想,当对象的创建逻辑⽐较复杂,不只是简单的 new ⼀下就可以 ,⽽是要组合其他类对象,做各种初始化操作的时候,我们推荐使⽤⼯⼚⽅法模式,将复杂的创建逻辑拆分到多个⼯⼚类中,让每个⼯⼚类都不⾄于过于复杂。⽽使⽤简单⼯⼚模式,将所有的创建逻辑都放到⼀个⼯⼚类中,会导致这个⼯⼚类变得很复杂。
抽象⼯⼚(Abstract Factory)
在简单⼯⼚和⼯⼚⽅法中,类只有⼀种分类⽅式。⽐如,在规则配置解析那个例⼦中,解析器类只会根据配置⽂件格式(Json、Xml、Yaml……)来分类。但是,如果类有两种分类⽅式,⽐如,我们既可以按照配置⽂件格式来分类,也可以按照解析的对象(Rule 规则配置还是 System 系统配置)来分类,那就会对应下⾯这 8 个 parser 类。
1 2 3 4 5 6 7 8 9 10 针对规则配置的解析器:基于接⼝IRuleConfigParser JsonRuleConfigParser XmlRuleConfigParser YamlRuleConfigParser PropertiesRuleConfigParser 针对系统配置的解析器:基于接⼝ISystemConfigParser JsonSystemConfigParser XmlSystemConfigParser YamlSystemConfigParser PropertiesSystemConfigParser
针对这种特殊的场景,如果还是继续⽤⼯⼚⽅法来实现的话,我们要针对每个 parser 都编写⼀个⼯⼚类,也就是要编写 8 个⼯⼚类。如果我们未来还需要增加针对业务配置的解析器(⽐如 IBizConfigParser),那就要再对应地增加 4 个⼯⼚类。⽽我们知道,过多的类也会让系统难维护。这个问题该怎么解决呢?
抽象⼯⼚就是针对这种⾮常特殊的场景⽽诞⽣的。我们可以让⼀个⼯⼚负责创建多个不同类型的对象(IRuleConfigParser、ISystemConfigParser 等),⽽不是只创建⼀种 parser 对象。这样就可以有效地减少⼯⼚类的个数。具体的代码实现如下所示:
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 public interface IConfigParserFactory { IRuleConfigParser createRuleParser () ; ISystemConfigParser createSystemParser () ; } 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 (); } }
⼯⼚模式和 DI 容器有何区别?
实际上,DI 容器底层最基本的设计思路就是基于⼯⼚模式的。DI 容器相当于⼀个⼤的⼯⼚类,负责在程序启动的时候,根据配置(要创建哪些类对象,每个类对象的创建需要依赖哪些其他类对象)事先创建好对象。当应⽤程序需要使⽤某个类对象的时候,直接从容器中获取即可。正是因为它持有⼀堆对象,所以这个框架才被称为“容器”。
DI 容器相对于我们上节课讲的⼯⼚模式的例⼦来说,它处理的是更⼤的对象创建⼯程。上节课讲的⼯⼚模式中,⼀个⼯⼚类只负责某个类对象或者某⼀组相关类对象(继承⾃同⼀抽象类或者接⼝的⼦类)的创建,⽽ DI 容器负责的是整个应⽤中所有类对象的创建。
除此之外,DI 容器负责的事情要⽐单纯的⼯⼚模式要多。⽐如,它还包括配置的解析、对象⽣命周期的管理。接下来,我们就详细讲讲,⼀个简单的 DI 容器应该包含哪些核⼼功能。
DI 容器的核⼼功能有哪些?
总结⼀下,⼀个简单的 DI 容器的核⼼功能⼀般有三个:配置解析、对象创建和对象⽣命周期管理。
构建者模式(Builder pattern)
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 68 69 70 71 72 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; } 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 ; } } } ResourcePoolConfig config = new ResourcePoolConfig .Builder() .setName("dbconnectionpool" ) .setMaxTotal(16 ) .setMaxIdle(10 ) .setMinIdle(12 ) .build();
原型模式(Prototype Pattern)
什么是原型模式?
如果对象的创建成本⽐较⼤,⽽同⼀个类的不同对象之间差别不⼤(⼤部分字段都相同),在这种情况下,我们可以利⽤对已有对象(原型)进⾏复制(或者叫拷⻉)的⽅式,来创建新对象,以达到节省创建时间的⽬的。这种基于原型来创建对象的⽅式就叫作原型设计模式,简称原型模式。
原型模式的两种实现⽅法
原型模式有两种实现⽅法,深拷⻉和浅拷⻉。浅拷⻉只会复制对象中基本数据类型数据和引⽤对象的内存地址,不会递归地复制引⽤对象,以及引⽤对象的引⽤对象……⽽深拷⻉得到的是⼀份完完全全独⽴的对象。所以,深拷⻉⽐起浅拷⻉来说,更加耗时,更加耗内存空间。
如果要拷⻉的对象是不可变对象,浅拷⻉共享不可变对象是没问题的,但对于可变对象来说,浅拷⻉得到的对象和原始对象会共享部分数据,就有可能出现数据被修改的⻛险,也就变得复杂多了。除⾮像我们今天实战中举的那个例⼦,需要从数据库中加载 10 万条数据并构建散列表索引,操作⾮常耗时,这种情况下⽐较推荐使⽤浅拷⻉,否则,没有充分的理由,不要为了⼀点点的性能提升⽽使⽤浅拷⻉。