创建型设计模式学习总结

前言(照常废话)

最近学习争哥的《设计模式之美》正式进入到23种常用设计模式的讲解阶段,这两个星期因为工作上面的原因也一直没有时间好好去沉淀一下学习的内容。今天周六,终于处理完杂七杂八的事情,逮着机会把可以比较简单但是非常常用的创建型设计模式做一个学习总结。

问题

顾名思义,创建型设计模式是为了解决程序设计过程中所遇到的创建问题。在java环境当中自然指的就是对象的创建。关于创建对象,面向对象编程(呸,哪来的对象)的我们再熟悉不过了,没有new一个不就完了吗。但是追求代码极致的我们,总是要多想一点,不能放过任何可以优化的点让代码变得跟精辟。

对象的创建过程复杂吗?需要的资源多不多,会不会影响程序执行效率?对象能不能重复利用呢?有没有线程安全问题?构建逻辑对代码的复用性,耦合度,扩展性等等有没有影响?其实仔细一想问题还是蛮多的。
创建型设计模式常用的主要有:单例,工厂,建造者,原型这四种。

单例

单例模式,即只需要保证环境中有且只有一个实例来提供服务(这是全局唯一的)。这意味着,当有多个实例同时提供服务时可能会造成资源冲突,同时这一个实例就可以完成工作,不需要浪费资源去重复创建。

单例模式有两种形式,俗称“饿汉”和“懒汉”模式。其区别在于,创建的时间不同。“饿汉”因为饿,所以资源加载的时候就赶紧去创建好目标对象,后面就不在创建了,调用时直接返回此对象来提供服务。

而“懒汉模式”则不会预创建,而是在第一次调用的时候才会去创建(这种加载模式也叫做懒加载或者延迟加载)。

以上两种模式,“饿汉模式”不支持懒加载,而“懒汉模式”直接加锁的设计导致并发度低。
但是我们可以使用双重检测来解决这两个问题。

1
2
public class IdGenerator { 
3
  private AtomicLong id = new AtomicLong(0);
4
  private static IdGenerator instance;
5
  private IdGenerator() {}
6
  public static IdGenerator getInstance() {
7
    if (instance == null) {
8
      synchronized(IdGenerator.class) { // 此处为类级别的锁
9
        if (instance == null) {
10
          instance = new IdGenerator();
11
        }
12
      }
13
    }
14
    return instance;
15
  }
16
  public long getId() { 
17
    return id.incrementAndGet();
18
  }
19
}

其他实现单例模式的方法还有:静态内部类方式

1
	
2
public class IdGenerator { 
3
  private AtomicLong id = new AtomicLong(0);
4
  private IdGenerator() {}
5
6
  private static class SingletonHolder{
7
    private static final IdGenerator instance = new IdGenerator();
8
  }
9
  
10
  public static IdGenerator getInstance() {
11
    return SingletonHolder.instance;
12
  }
13
 
14
  public long getId() { 
15
    return id.incrementAndGet();
16
  }
17
}

SingletonHolder 是一个静态内部类,当外部类 IdGenerator 被加载的时候,并不会创建 SingletonHolder 实例对象。只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。

instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。

另外我们也可以通过枚举来实现,就不具体说明了。

工厂

对于工厂我的理解是用来定制化生产的地方,工厂模式解决的问题就是对象的定制化创建。而具体的创建过程对使用者隐藏(隔离复杂性),对外暴露创建接口。使用者只需要告诉工厂,需要生成什么样的产品即可使得调用代码职责更加单一,代码更加简洁(控制复杂度)。

根据创建逻辑的复杂度,我们可以分别用简单工厂和工厂方法两种形式来应对。
对于少量对象类型,较为简单的创建逻辑,可以直接把if else创建逻辑直接剥离到工厂当中,即简单工厂。

而对于较多类型(短期内未考虑到的类型,后期会做扩展),以及单个对象创建具有较为复杂的创建逻辑,或者依赖其他比较多的资源。我们可以把工厂进行抽象。暴露创建接口,让具体的工厂类实现此接口来生成不同的复杂对象。

但此时,虽然解决了对象的扩展问题,却引入的新的复杂性,即多个工厂类。这些工厂类的创建又成了一个根据类型创建对象的大规模if else问题。对此我们可以用“超级工厂”来解决这样的问题–即生产工厂的工厂。

因为工厂只需要是单个对象即可提供服务,因此结合单例模式,我们可以把工厂单例对象缓存到超级工厂当中。当需要调用具体的工厂来提供生产服务的时候直接返回次对象提供服务即可。

1
2
public interface IConfigParserFactory {
3
  IRuleConfigParser createRuleParser();
4
  ISystemConfigParser createSystemParser();
5
  //此处可以扩展新的parser类型,比如IBizConfigParser
6
}
7
8
public class JsonConfigParserFactory implements IConfigParserFactory {
9
  @Override
10
  public IRuleConfigParser createRuleParser() {
11
    return new JsonRuleConfigParser();
12
  }
13
14
  @Override
15
  public ISystemConfigParser createSystemParser() {
16
    return new JsonSystemConfigParser();
17
  }
18
}
19
20
public class XmlConfigParserFactory implements IConfigParserFactory {
21
  @Override
22
  public IRuleConfigParser createRuleParser() {
23
    return new XmlRuleConfigParser();
24
  }
25
26
  @Override
27
  public ISystemConfigParser createSystemParser() {
28
    return new XmlSystemConfigParser();
29
  }
30
}
31
32
// 省略YamlConfigParserFactory和PropertiesConfigParserFactory代码
33
34
35
public class RuleConfigSource {
36
  public RuleConfig load(String ruleConfigFilePath) {
37
    String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
38
39
    IRuleConfigParserFactory parserFactory = RuleConfigParserFactoryMap.getParserFactory(ruleConfigFileExtension);
40
    if (parserFactory == null) {
41
      throw new InvalidRuleConfigException("Rule config file format is not supported: " + ruleConfigFilePath);
42
    }
43
    IRuleConfigParser parser = parserFactory.createParser();
44
45
    String configText = "";
46
    //从ruleConfigFilePath文件中读取配置文本到configText中
47
    RuleConfig ruleConfig = parser.parse(configText);
48
    return ruleConfig;
49
  }
50
51
  private String getFileExtension(String filePath) {
52
    //...解析文件名获取扩展名,比如rule.json,返回json
53
    return "json";
54
  }
55
}
56
57
//因为工厂类只包含方法,不包含成员变量,完全可以复用,
58
//不需要每次都创建新的工厂类对象,所以,简单工厂模式的第二种实现思路更加合适。
59
public class RuleConfigParserFactoryMap { //工厂的工厂
60
  private static final Map<String, IRuleConfigParserFactory> cachedFactories = new HashMap<>();
61
62
  static {
63
    cachedFactories.put("json", new JsonRuleConfigParserFactory());
64
    cachedFactories.put("xml", new XmlRuleConfigParserFactory());
65
    cachedFactories.put("yaml", new YamlRuleConfigParserFactory());
66
    cachedFactories.put("properties", new PropertiesRuleConfigParserFactory());
67
  }
68
69
  public static IRuleConfigParserFactory getParserFactory(String type) {
70
    if (type == null || type.isEmpty()) {
71
      return null;
72
    }
73
    IRuleConfigParserFactory parserFactory = cachedFactories.get(type.toLowerCase());
74
    return parserFactory;
75
  }
76
}

建造者

如果说工厂模式是注重创建不同类型但是又有关联的对象,那么建造者模式就是专注于建造对象本身的设计模式了。解决的是对象本身建造过程的复杂度隐藏,而对调用者透明。将建造的前期准备工作都委托给“建造者”去做,万事俱备了才交给目标对象构造器去执行。

关于使用的场景,我总结了几个:

  • 构造的对象属性较多,我们手动构造时需要大量的set操作(代码冗余,而且排版不好看)
  • 存在某些属性,生命周期内有且仅需赋值一次,为了控制权限和安全,不能暴露set方法(例如某些资源类对象的构造,线程池等等)
  • 对于对象本身的属性,赋值需要校验,属性间存在关联的情况,直接在set方法中校验会违反单一职责,而且校验规则耦合,做不到统一校验。利用建造模式,可以进行统一校验,校验完毕再调用属性的set私有方法来构建最终对象。
    1
    2
    public class ResourcePoolConfig {
    3
      private String name;
    4
      private int maxTotal;
    5
      private int maxIdle;
    6
      private int minIdle;
    7
    8
      private ResourcePoolConfig(Builder builder) {
    9
        this.name = builder.name;
    10
        this.maxTotal = builder.maxTotal;
    11
        this.maxIdle = builder.maxIdle;
    12
        this.minIdle = builder.minIdle;
    13
      }
    14
      //...省略getter方法...
    15
    16
      //我们将Builder类设计成了ResourcePoolConfig的内部类。
    17
      //我们也可以将Builder类设计成独立的非内部类ResourcePoolConfigBuilder。
    18
      public static class Builder {
    19
        private static final int DEFAULT_MAX_TOTAL = 8;
    20
        private static final int DEFAULT_MAX_IDLE = 8;
    21
        private static final int DEFAULT_MIN_IDLE = 0;
    22
    23
        private String name;
    24
        private int maxTotal = DEFAULT_MAX_TOTAL;
    25
        private int maxIdle = DEFAULT_MAX_IDLE;
    26
        private int minIdle = DEFAULT_MIN_IDLE;
    27
    28
        public ResourcePoolConfig build() {
    29
          // 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等
    30
          if (StringUtils.isBlank(name)) {
    31
            throw new IllegalArgumentException("...");
    32
          }
    33
          if (maxIdle > maxTotal) {
    34
            throw new IllegalArgumentException("...");
    35
          }
    36
          if (minIdle > maxTotal || minIdle > maxIdle) {
    37
            throw new IllegalArgumentException("...");
    38
          }
    39
    40
          return new ResourcePoolConfig(this);
    41
        }
    42
    43
        public Builder setName(String name) {
    44
          if (StringUtils.isBlank(name)) {
    45
            throw new IllegalArgumentException("...");
    46
          }
    47
          this.name = name;
    48
          return this;
    49
        }
    50
    51
        public Builder setMaxTotal(int maxTotal) {
    52
          if (maxTotal <= 0) {
    53
            throw new IllegalArgumentException("...");
    54
          }
    55
          this.maxTotal = maxTotal;
    56
          return this;
    57
        }
    58
    59
        public Builder setMaxIdle(int maxIdle) {
    60
          if (maxIdle < 0) {
    61
            throw new IllegalArgumentException("...");
    62
          }
    63
          this.maxIdle = maxIdle;
    64
          return this;
    65
        }
    66
    67
        public Builder setMinIdle(int minIdle) {
    68
          if (minIdle < 0) {
    69
            throw new IllegalArgumentException("...");
    70
          }
    71
          this.minIdle = minIdle;
    72
          return this;
    73
        }
    74
      }
    75
    }
    76
    77
    // 这段代码会抛出IllegalArgumentException,因为minIdle>maxIdle
    78
    ResourcePoolConfig config = new ResourcePoolConfig.Builder()
    79
            .setName("dbconnectionpool")
    80
            .setMaxTotal(16)
    81
            .setMaxIdle(10)
    82
            .setMinIdle(12)
    83
            .build();

    原型

    关于原型模式,使用比较少,大致的思想就是利用已经存在的对象,使用拷贝的方式来创建新的对象,达到快速创建的目的。(java语言环境中分为深拷贝和浅拷贝的模式,有兴趣的小伙伴可以自行了解,这里就不展开讨论啦)

后记

第一次尝试以这种方式来总结设计模式,参考了争哥的课程。对于理解帮助很大,因为设计模式的应用,有时候你看原理觉得很简单,但是实际开发过程当中能熟练运用还是需要能深入理解它的思想,知道它解决什么问题,才能做到恰到好处,而不是过度设计,或者胡乱设计。