设计模式与范式:创建型 
单例设计模式(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 万条数据并构建散列表索引,操作⾮常耗时,这种情况下⽐较推荐使⽤浅拷⻉,否则,没有充分的理由,不要为了⼀点点的性能提升⽽使⽤浅拷⻉。