设计模式-创建型-常用:单例模式、工厂模式、建造者模式

单例模式

概念

一个类只允许创建一个对象(或实例),那这个类就是单例类,这种设计模式就叫做单例模式。对于一些类,创建和销毁比较复杂,如果每次使用都创建一个对象会很耗费性能,因此可以把它设置为单例类。有的地方会用数据库连接池来举例,实际上一些数据库连接池、线程池是没有被设计成单例类的,这点在下面单例模式存在的问题中会讲。

如何实现一个单例类

需要关注以下几点:

  • 构造函数需要时private访问权限,这样才能避免外部通过new创建实例
  • 对象创建是否是线程安全
  • 是否支持懒加载
  • getInstance()性能是否达标(有无加锁)
  • 能否通过反射破坏(通过反射创建实例)

饿汉式

java 复制代码
public class Singleton {
    private Singleton() {} // 构造器私有
    private static final Singleton instance = new Singleton();
    public static Singleton getInstance() {
        return instance;
    }
}

在类加载的时候,instace静态实例就已经创建并初始化好了,所以饿汉式是线程安全的。不过饿汉式不支持懒加载(在真正用到Singleton的时候才创建实例)。

有人说这种实现方式不好,认为懒加载的好处是只有真正使用到的时候才会创建,防止一些对象的构建比较耗费性能(比如需要加载各种配置文件),且在下一次创建对象之前从没有被使用过,会造成资源浪费。

不过我更赞同另一种观点,如果初始化耗时长,那么我们最好不要等到真正要用它的时候才去执行这个初始化过程,比如在响应客户端请求的时候做这个初始化操作会导致请求的响应时间变长。并且如果实例占用资源多,按照fail-fast设计原则(有问题及早暴露),我们也希望在程序启动时就将这个实例初始化好,如果资源不够就会在程序启动时触发报错,也可以避免一些程序运行一段时间后因为初始化实例占用资源过多而报错的情况。

懒汉式

java 复制代码
public class Singleton {
    private Singleton() {} // 构造器私有
    private static Singleton instance = null;// 初始化对象为nul1
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

懒汉式的缺点很明显,就是synchronized锁粒度太粗,同一时间只能有一个线程访问getInstace()方法去尝试获取实例,如果这个实例被频繁用到,那么加锁释放锁、以及方法的并发度问题会导致性能瓶颈。

双检锁

java 复制代码
public class Singleton {
    private AtomicLong id = new AtomicLong(0);
    private static Singleton instance = null;
    private Singleton() {} // 构造器私有
    public static Singleton getInstance() {
        if (instance == null) {  // 1
            synchronized (Singleton.class) {  // 2
                if (instance == null) {    // 虽然只能有一个线程进入2,但可能有其他线程在1处等待释放锁,因此需要二次校验
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
    public long getId(){
        return id.incrementAndGet();
    }
}

这种实现方式还有一个问题,因为指令重排序,可能会导致Singleton对象被new出来,并且赋值给instance后,还没来得及初始化(在指令层面,赋值那行被分为三步:1、分配内存;2、初始化对象;3、对象指向内存地址),就被另一个线程使用了。

要解决这个问题,instance实例需要加上volatile关键字禁止指令重排序。

不过还有人说只有很低版本的Java才会有这个问题,高版本的Java已经在jdk内部实现中解决了这个问题(通过把前面三步改为原子操作)。这块博主问了gpt后无果,暂时先放在这里。

静态内部类

java 复制代码
public class IdGenerator {
    private AtomicLong id = new AtomicLong(0);
    private IdGenerator() {}
    private static class SingletonHolder{
        private static final IdGenerator instance = new IdGenerator();
    }
    public static IdGenerator getInstance() {
        return SingletonHolder.instance;
    }
    public long getId() {
        return id.incrementAndGet();
    }
}

静态内部类在程序启动的时候不会加载,只有在第一次被调用的时候才会加载。instance的唯一性、线程安全,都由JVM来保证。

枚举

java 复制代码
public enum IdGenerator {
  INSTANCE;
  private AtomicLong id = new AtomicLong(0);
 
  public long getId() { 
    return id.incrementAndGet();
  }
}

上面4种写法都是会被反射破坏的,不过反射是一种人为的方式,不会有太大影响。而这个枚举方式是不能通过反射进行构建的,在效果上类似饿汉式,通过Java枚举类型的特性,在类加载的时候就会创建对应的实例。

单例模式存在的问题

大部分情况下,我们在项目中使用单例,都是用它来表示一些全局唯一类,比如配置信息类、连接池类、ID生成器类。单例模式书写简洁、使用方便,在代码中,我们不需要创建对象,直接通过类似IdGenerator.getInstance().getId()这样的方法来调用就可以了。但是,这种使用方法有点类似硬编码(hard code),会带来诸多问题。接下来,我们就具体看看到底有哪些问题。

1.单例对OOP特性的支持不友好

我们知道,OOP的四大特性是封装、抽象、继承、多态。单例这种设计模式对于其中的抽象、继承、多态都支持得不好。为什么这么说呢?我们还是通过IdGenerator这个例子来讲解。

java 复制代码
public class Order {
  public void create(...) {
    //...
    long id = IdGenerator.getInstance().getId();
    //...
  }
}

public class User {
  public void create(...) {
    // ...
    long id = IdGenerator.getInstance().getId();
    //...
  }
}

IdGenerator的使用方式违背了基于接口而非实现的设计原则,也就违背了广义上理解的OOP的抽象特性。如果未来某一天,我们希望针对不同的业务采用不同的ID生成算法。比如,订单ID和用户ID采用不同的ID生成器来生成。为了应对这个需求变化,我们需要修改所有用到IdGenerator类的地方,这样代码的改动就会比较大。

java 复制代码
public class Order {
  public void create(...) {
    //...
    long id = IdGenerator.getInstance().getId();
    // 需要将上面一行代码,替换为下面一行代码
    long id = OrderIdGenerator.getIntance().getId();
    //...
  }
}

public class User {
  public void create(...) {
    // ...
    long id = IdGenerator.getInstance().getId();
    // 需要将上面一行代码,替换为下面一行代码
    long id = UserIdGenerator.getIntance().getId();
  }
}

除此之外,单例对继承、多态特性的支持也不友好。这里我之所以会用"不友好"这个词,而非"完全不支持",是因为从理论上来讲,单例类也可以被继承、也可以实现多态,只是实现起来会非常奇怪,会导致代码的可读性变差。不明白设计意图的人,看到这样的设计,会觉得莫名其妙。所以,一旦你选择将某个类设计成到单例类,也就意味着放弃了继承和多态这两个强有力的面向对象特性,也就相当于损失了可以应对未来需求变化的扩展性。

2.单例会隐藏类之间的依赖关系

我们知道,代码的可读性非常重要。在阅读代码的时候,我们希望一眼就能看出类与类之间的依赖关系,搞清楚这个类依赖了哪些外部类。

通过构造函数、参数传递等方式声明的类之间的依赖关系,我们通过查看函数的定义,就能很容易识别出来。但是,单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。如果代码比较复杂,这种调用关系就会非常隐蔽。在阅读代码的时候,我们就需要仔细查看每个函数的代码实现,才能知道这个类到底依赖了哪些单例类。

3.单例对代码的扩展性不友好

我们知道,单例类只能有一个对象实例。如果未来某一天,我们需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动。你可能会说,会有这样的需求吗?既然单例类大部分情况下都用来表示全局类,怎么会需要两个或者多个实例呢?

实际上,这样的需求并不少见。我们拿数据库连接池来举例解释一下。

在系统设计初期,我们觉得系统中只应该有一个数据库连接池,这样能方便我们控制对数据库连接资源的消耗。所以,我们把数据库连接池类设计成了单例类。但之后我们发现,系统中有些SQL语句运行得非常慢。这些SQL语句在执行的时候,长时间占用数据库连接资源,导致其他SQL请求无法响应。为了解决这个问题,我们希望将慢SQL与其他SQL隔离开来执行。为了实现这样的目的,我们可以在系统中创建两个数据库连接池,慢SQL独享一个数据库连接池,其他SQL独享另外一个数据库连接池,这样就能避免慢SQL影响到其他SQL的执行。

如果我们将数据库连接池设计成单例类,显然就无法适应这样的需求变更,也就是说,单例类在某些情况下会影响代码的扩展性、灵活性。所以,数据库连接池、线程池这类的资源池,最好还是不要设计成单例类。实际上,一些开源的数据库连接池、线程池也确实没有设计成单例类。

4.单例对代码的可测试性不友好

单例模式的使用会影响到代码的可测试性。如果单例类依赖比较重的外部资源,比如DB,我们在写单元测试的时候,希望能通过mock的方式将它替换掉。而单例类这种硬编码式的使用方式,导致无法实现mock替换。

除此之外,如果单例类持有成员变量(比如IdGenerator中的id成员变量),那它实际上相当于一种全局变量,被所有的代码共享。如果这个全局变量是一个可变全局变量,也就是说,它的成员变量是可以被修改的,那我们在编写单元测试的时候,还需要注意不同测试用例之间,修改了单例类中的同一个成员变量的值,从而导致测试结果互相影响的问题。

5.单例不支持有参数的构造函数

单例不支持有参数的构造函数,比如我们创建一个连接池的单例对象,我们没法通过参数来指定连接池的大小。针对这个问题,我们来看下都有哪些解决方案。

第一种解决思路是:创建完实例之后,再调用init()函数传递参数。需要注意的是,我们在使用这个单例类的时候,要先调用init()方法,然后才能调用getInstance()方法,否则代码会抛出异常。具体的代码实现如下所示:

java 复制代码
public class Singleton {
  private static Singleton instance = null;
  private final int paramA;
  private final int paramB;

  private Singleton(int paramA, int paramB) {
    this.paramA = paramA;
    this.paramB = paramB;
  }

  public static Singleton getInstance() {
    if (instance == null) {
       throw new RuntimeException("Run init() first.");
    }
    return instance;
  }

  public synchronized static Singleton init(int paramA, int paramB) {
    if (instance != null){
       throw new RuntimeException("Singleton has been created!");
    }
    instance = new Singleton(paramA, paramB);
    return instance;
  }
}

Singleton.init(10, 50); // 先init,再使用
Singleton singleton = Singleton.getInstance();

第二种解决思路是:将参数放到getIntance()方法中。具体的代码实现如下所示:

java 复制代码
public class Singleton {
  private static Singleton instance = null;
  private final int paramA;
  private final int paramB;

  private Singleton(int paramA, int paramB) {
    this.paramA = paramA;
    this.paramB = paramB;
  }

  public synchronized static Singleton getInstance(int paramA, int paramB) {
    if (instance == null) {
      instance = new Singleton(paramA, paramB);
    }
    return instance;
  }
}

Singleton singleton = Singleton.getInstance(10, 50);

不知道你有没有发现,上面的代码实现稍微有点问题。如果我们如下两次执行getInstance()方法,那获取到的singleton1和signleton2的paramA和paramB都是10和50。也就是说,第二次的参数(20,30)没有起作用,而构建的过程也没有给与提示,这样就会误导用户。这个问题如何解决呢?留给你自己思考,你可以在留言区说说你的解决思路。

Singleton singleton1 = Singleton.getInstance(10, 50);

Singleton singleton2 = Singleton.getInstance(20, 30);

第三种解决思路是:将参数放到另外一个全局变量中。具体的代码实现如下。Config是一个存储了paramA和paramB值的全局变量。里面的值既可以像下面的代码那样通过静态常量来定义,也可以从配置文件中加载得到。实际上,这种方式是最值得推荐的。

java 复制代码
public class Config {
  public static final int PARAM_A = 123;
  public static final int PARAM_B = 245;
}

public class Singleton {
  private static Singleton instance = null;
  private final int paramA;
  private final int paramB;

  private Singleton() {
    this.paramA = Config.PARAM_A;
    this.paramB = Config.PARAM_B;
  }

  public synchronized static Singleton getInstance() {
    if (instance == null) {
      instance = new Singleton();
    }
    return instance;
  }
}

有何替代解决方案?

刚刚我们提到了单例的很多问题,你可能会说,即便单例有这么多问题,但我不用不行啊。我业务上有表示全局唯一类的需求,如果不用单例,我怎么才能保证这个类的对象全局唯一呢?

为了保证全局唯一,除了使用单例,我们还可以用静态方法来实现。这也是项目开发中经常用到的一种实现思路。比如,上一节课中讲的ID唯一递增生成器的例子,用静态方法实现一下,就是下面这个样子:

java 复制代码
// 静态方法实现方式
public class IdGenerator {
  private static AtomicLong id = new AtomicLong(0);
  
  public static long getId() { 
    return id.incrementAndGet();
  }
}
// 使用举例
long id = IdGenerator.getId();

不过,静态方法这种实现思路,并不能解决我们之前提到的问题。实际上,它比单例更加不灵活,比如,它无法支持延迟加载。我们再来看看有没有其他办法。实际上,单例除了我们之前讲到的使用方法之外,还有另外一种使用方法。具体的代码如下所示:

java 复制代码
// 1. 老的使用方式
public demofunction() {
  //...
  long id = IdGenerator.getInstance().getId();
  //...
}

// 2. 新的使用方式:依赖注入
public demofunction(IdGenerator idGenerator) {
  long id = idGenerator.getId();
}
// 外部调用demofunction()的时候,传入idGenerator
IdGenerator idGenerator = IdGenerator.getInsance();
demofunction(idGenerator);

基于新的使用方式,我们将单例生成的对象,作为参数传递给函数(也可以通过构造函数传递给类的成员变量),可以解决单例隐藏类之间依赖关系的问题。不过,对于单例存在的其他问题,比如对OOP特性、扩展性、可测性不友好等问题,还是无法解决。

所以,如果要完全解决这些问题,我们可能要从根上,寻找其他方式来实现全局唯一类。实际上,类对象的全局唯一性可以通过多种不同的方式来保证。我们既可以通过单例模式来强制保证,也可以通过工厂模式、IOC容器(比如Spring IOC容器)来保证,还可以通过程序员自己来保证(自己在编写代码的时候自己保证不要创建两个类对象)。这就类似Java中内存对象的释放由JVM来负责,而C++中由程序员自己负责,道理是一样的。

总结

上面写出了五种单例模式的经典实现方案,其中懒汉式由于线程不安全是不可取的,其他几种实现在功能上都没有太大问题,可以根据需求选择。

单例存在哪些问题?

  • 单例对OOP特性的支持不友好
  • 单例会隐藏类之间的依赖关系
  • 单例对代码的扩展性不友好
  • 单例对代码的可测试性不友好
  • 单例不支持有参数的构造函数

单例有什么替代解决方案?

为了保证全局唯一,除了使用单例,我们还可以用静态方法来实现。不过,静态方法这种实现思路,并不能解决我们之前提到的问题。如果要完全解决这些问题,我们可能要从根上,寻找其他方式来实现全局唯一类了。比如,通过工厂模式、IOC容器(比如Spring IOC容器)来保证,由程序员自己来保证(自己在编写代码的时候自己保证不要创建两个类对象)。

有人把单例当作反模式,主张杜绝在项目中使用。我个人觉得这有点极端。模式没有对错,关键看你怎么用。如果单例类并没有后续扩展的需求,并且不依赖外部系统,那设计成单例类就没有太大问题。对于一些全局的类,我们在其他地方new的话,还要在类之间传来传去,不如直接做成单例类,使用起来简洁方便。

工厂模式

概念

一般工厂模式分为:简单工厂、工厂方法、抽象工厂,在Gof-23中简单工厂被看做是工厂方法的一种特例,抽象工厂在实际项目中不算常见。

简单工厂

日常开发中,一些场景会遇到根据不同的条件创建不同的对象,比如根据配置文件的后缀创建不同的解析器对象等,把这些if判断和创建对象的代码单独提出一个函数,可以使逻辑更清晰,如果进一步把这个函数放到一个独立的类中,那么这个类就是简单工厂模式的类。示例:

java 复制代码
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;
  }
}

一般这个类会用Factory后缀命名,但也不是必须的,比如DateFormat、Calender。另外工厂类中创建对象的方法一般是create开头,有的也会命名为get。。new。。甚至String类的valueOf()函数。

上面的实现中,每次调用create方法都要创建一个新的parser,可以通过先创建好缓存起来,来节省内存和对象创建的时间,这可以说是单例模式和简单工厂模式的结合,示例:

java 复制代码
public class RuleConfigParserFactory {
  private static final Map 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;
  }
}

对于上面两种实现方式,如果要添加新的解析器,就需要改动工厂类的代码,会违反开闭原则。不过实际上,如果不是频繁地改动这部分代码,稍微不符合开闭原则也是可以接受的。

工厂方法

如果非要不改动Factory类的代码该怎么做呢?一个经典的方式是利用多态,示例如下:

java 复制代码
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类即可。所以,工厂方法模式比简单工厂模式更符合开闭原则。

但实际上上面的工厂方法在实现上有挺大的问题,因为工厂类没有实现if判断,所以需要先if判断类型后再决定创建哪个具体的类对象,这就和不使用工厂模式几乎没有区别了。下面这段是包括了创建Parser解析器之后使用他的一段代码示例:

java 复制代码
public class RuleConfigSource {
  public RuleConfig load(String ruleConfigFilePath) {
    String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);

    IRuleConfigParserFactory parserFactory = null;
    // 工厂方法中只是创建了解析器,需要在前面if判断创建哪个类对象
    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";
  }
}

这个问题的一般解决思路是为工厂类再创建一个简单工厂,这个简单工厂用来创建工厂类对象,代码示例如下:

java 复制代码
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;
  }
}

其实也很好理解,就是把之前简单工厂的迭代过程重演了一遍,目标实例从解析器对象变成了类对象。

抽象工厂

不算常用,在上面两个例子中,类只有一种分类方式:IRuleConfigParser 。假设现在场景需要另一种解析规则,

针对规则配置的解析器:基于接口IRuleConfigParser

JsonRuleConfigParser

XmlRuleConfigParser

YamlRuleConfigParser

PropertiesRuleConfigParser

针对系统配置的解析器:基于接口ISystemConfigParser

JsonSystemConfigParser

XmlSystemConfigParser

YamlSystemConfigParser

PropertiesSystemConfigParser

创建的类就需要翻倍了,此时可以改为以下写法:

java 复制代码
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();
  }
}

工厂类中增加另一个解析规则的对象的实现方法。

总结

如果某个代码块变的比较复杂,为了让代码更清晰,就可以考虑单独拆除一个工厂类。此外如果想避免if-else逻辑,就可以考虑工厂方法。简单工厂的单例实现在if-else方面和工厂方法没什么不同,感觉两个模式最大的区别是工厂方法可以在去除if-else的基础上定制化createParser()方法。

因此,当对象的创建逻辑比较简单时推荐用简单工厂模式,当创建逻辑比较复杂的时候,为了避免出现一个过于庞大的简单工厂类,推荐用工厂方法模式。

扩展-DI框架中的应用

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

配置解析

在上面的工厂模式中,要创建哪个类对象是事先确定好的,并且是写死在工厂类代码中的。作为一个通用的框架,框架代码跟应用代码应该是高度解耦的,DI容器并不知道应用将会创建哪些对象。所以我们需要通过一种形式让应用告知DI容器要创建哪些对象,这种形式就是配置解析。

比如Spring中通过依赖注入的方式让DI容器解析配置。

对象创建

在DI容器中,如果我们给每个类都创建一个工厂类是不现实的,我们只需要将所有类对象的创建都放到一个工厂类中完成就可以了,比如BeanFactory。

你可能会说,要创建的类非常多,BeanFactory中的代码会不会线性膨胀,实际上不会,原因就是反射机制,在程序运行的过程中,动态地加载类、创建对象。

生命周期管理

在上面的简单工厂模式有两种实现方式,一种是每次都返回新创建的对象,另一种是每次都返回同一个实现创建好的对象,也就是所谓的单例对象。比如Spring中通过@scope来配置。

除此之外,还可以配置对象是否支持懒加载。另外还可以配置对象的初始化方法和销毁前方法。

实现

最后,我们来看,BeansFactory是如何设计和实现的。这也是我们这个DI容器最核心的一个类了。它负责根据从配置文件解析得到的BeanDefinition来创建对象。

如果对象的scope属性是singleton,那对象创建之后会缓存在singletonObjects这样一个map中,下次再请求此对象的时候,直接从map中取出返回,不需要重新创建。如果对象的scope属性是prototype,那每次请求对象,BeansFactory都会创建一个新的对象返回。

实际上,BeansFactory创建对象用到的主要技术点就是Java中的反射语法:一种动态加载类和创建对象的机制。我们知道,JVM在启动的时候会根据代码自动地加载类、创建对象。至于都要加载哪些类、创建哪些对象,这些都是在代码中写死的,或者说提前写好的。但是,如果某个对象的创建并不是写死在代码中,而是放到配置文件中,我们需要在程序运行期间,动态地根据配置文件来加载类、创建对象,那这部分工作就没法让JVM帮我们自动完成了,我们需要利用Java提供的反射语法自己去编写代码。

搞清楚了反射的原理,BeansFactory的代码就不难看懂了。具体代码实现如下所示:

java 复制代码
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;
  }
}

建造者模式

建造者模式可以简单概括为:

  1. 为了防止一个对象中属性过多,构造函数冗长;
  2. 如果用set赋值,无法把控有些参数必填的逻辑(如果必填参数放到构造函数中,同样会有第一点问题);
  3. 如果几个属性之间有依赖关系或约束条件,校验逻辑会变得无处安放;
  4. 如果希望对象是不可变对象,也就是创建好之后就不能在修改内部的属性,那么就不能暴露set方法;
  5. 在有些场景还能避免对象存在无效状态,比如定义一个长方形类,在创建类对象和set第一个值时,这个对象都是不可用的,只有在第二个值也set后,才是可以用的状态。

为了解决这些问题,建造者模式就派上用场了,代码示例:

java 复制代码
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();

与工厂模式的区别

建造者模式是让建造者类来负责对象的创建工作,工厂模式是让工厂类来负责对象的创建工作,他们之间的区别是工厂模式是创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,来具体创建不同的对象。

相关推荐
Damon_X1 小时前
桥接模式(Bridge Pattern)
设计模式·桥接模式
越甲八千6 小时前
重温设计模式--享元模式
设计模式·享元模式
码农爱java7 小时前
设计模式--抽象工厂模式【创建型模式】
java·设计模式·面试·抽象工厂模式·原理·23种设计模式·java 设计模式
越甲八千8 小时前
重温设计模式--中介者模式
windows·设计模式·中介者模式
犬余8 小时前
设计模式之桥接模式:抽象与实现之间的分离艺术
笔记·学习·设计模式·桥接模式
Theodore_10229 小时前
1 软件工程——概述
java·开发语言·算法·设计模式·java-ee·软件工程·个人开发
越甲八千10 小时前
重拾设计模式--组合模式
设计模式·组合模式
思忖小下13 小时前
梳理你的思路(从OOP到架构设计)_设计模式Composite模式
设计模式·组合模式·eit
机器视觉知识推荐、就业指导13 小时前
C++设计模式:组合模式(公司架构案例)
c++·后端·设计模式·组合模式
越甲八千14 小时前
重拾设计模式--工厂模式(简单、工厂、抽象)
c++·设计模式