01 创建型模式

Posted by WZhong on Thursday, March 12, 2020

TOC

单例模式

单例的定义

  • 一个类只允许创建一个对象,那这个类就是一个单例类,这种设计模式就叫做单例设计模式

单例的用处

  • 有些数据在系统中只应该保存一份:如系统的配置信息

    • 唯一递增id生成器:AtomicLong是一个Java并发库中提供的一个原子变量类型,它将一些线程不安全需要加锁的复合操作封装为了线程安全的原子操作,比如下面会用到的incrementAndGet().
    public class IdGenerator {
      private AtomicLong id = new AtomicLong(0);
    
      private static final IdGenerator instance = new IdGenerator();
    
      private IdGenerator() {
      }
    
      public static IdGenerator getInstance() {
          return instance;
      }
    
      public long getId() {
          return id.getAndIncrement();
      }
    }
    
  • 解决资源访问冲突问题:如日志记录

    • v1:在web容器的servlet多线程环境下,如果两个servlet线程同时执行两个函数并写入日志,因为共享变量是竞争资源,可能存在日志信息互相覆盖的情况
    public class Logger {
    
      private FileWriter writer;
    
      public Logger() {
          File file = new File("/Users/xx/log.txt");
            writer = new FileWriter(file, true);
      }
    
      public void log(String message) {
          writer.write(message);
      }
    }
    
    • v2:给log()函数加互斥锁synchronized,同一时刻只允许一个线程调用执行log()函数:这种锁是一个对象级别的锁(一个对象在不同线程下同时调用log()函数,会被强制要求顺序执行),不同对象之间并不共享一把锁,通过不同对象调用log()函数,锁并不起作用;同时FileWriter本身就是线程安全的,它的内部实现中本身就加了对象级别的锁
    public class Logger {
    
      private FileWriter writer;
    
      public Logger() {
          File file = new File("/Users/xx/log.txt");
            writer = new FileWriter(file, true);
      }
    
      public void log(String message) {
          synchronized (this) {
              writer.write(message);
          }
      }
    }
    
    • v3:把对象级别的锁,换成类级别的锁,让所有对象都共享一把锁,避免不同对象之间同时调用log()函数;但是在使用时仍然需要创建多个对象,浪费内存空间和系统文件句柄
    public class Logger {
    
      private FileWriter writer;
    
      public Logger() {
          File file = new File("/Users/xx/log.txt");
          writer = new FileWriter(file, true);
      }
    
      public void log(String message) throws IOException {
          synchronized (Logger.class) {
              writer.write(message);
          }
      }
    }
    
    • v4:将Logger设计成一个单例类:不用创建那么多Logger对象,节省内存空间,节省系统文件句柄
    public class Logger {
    
      private FileWriter writer;
    
      private static final Logger instance = new Logger();
    
      private Logger() {
          File file = new File("/Users/xx/log.txt");
          writer = new FileWriter(file, true);
      }
    
      public static Logger getInstance() {
          return instance;
      }
    
      public void log(String message) throws IOException {
          writer.write(message);
      }
    }
    

单例的实现

  1. 构造函数需要是private访问权限的,这样才能避免外部通过new创建实例
  2. 考虑对象创建时的线程安全问题
  3. 考虑是否支持延迟加载
  4. 考虑getInstance()性能是否高,是否加锁**
  • 饿汉式:

    • 在类加载的时候,instance静态实例就已经创建并初始化好,所以instance的实例创建过程是线程安全的

    • 采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。如果实例占用资源多,按照 fail-fast 的设计原则(有问题及早暴露),那我们也希望在程序启动时就将这个实例初始化好

    public class IdGenerator {
    
      private AtomicLong id = new AtomicLong(0);
    
      private static final IdGenerator instance = new IdGenerator();
    
      private IdGenerator() {};
    
      public static IdGenerator getInstance() {
          return instance;
      }
    
      public long getId() {
          return id.getAndIncrement();
      }
    
    }
    
  • 懒汉式

    • 懒汉式相对于饿汉式的优势是支持延迟加载

    • 给 getInstance() 这个方法加了一把大锁(synchronzed),导致这个函数的并发度很低。如果频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶颈,这种实现方式就不可取了

    public class IdGenerator {
    
      private AtomicLong id = new AtomicLong(0);
      private static IdGenerator instance;
    
      private IdGenerator() {
      }
    
      public static synchronized IdGenerator getInstance() {
          if (instance == null) {
              instance = new IdGenerator();
          }
          return instance;
      }
    
      public long getId() {
          return id.getAndIncrement();
      }
    }
    
  • 双重检测

    • 既支持延迟加载、又支持高并发

    • 只要 instance 被创建之后,即便再调用 getInstance() 函数也不会再进入到加锁逻辑中了

    • 因为指令重排序,可能会导致 IdGenerator 对象被 new 出来,并且赋值给 instance 之后,还没来得及初始化(执行构造函数中的代码逻辑),就被另一个线程使用了:需要给 instance 成员变量加上 volatile 关键字,禁止指令重排序

    public class IdGenerator {
    
      private AtomicLong id = new AtomicLong(0);
    
      private static volatile IdGenerator instance;
    
      private IdGenerator() {
      }
    
      public static IdGenerator getInstance() {
          if (instance == null) {
              synchronized (IdGenerator.class) {
                  if (instance == null) {
                      instance = new IdGenerator();
                  }
              }
          }
          return instance;
      }
    
      public long getId() {
          return id.getAndIncrement();
      }
    }
    
  • 静态内部类

    • SingletonHolder 是一个静态内部类,当外部类 IdGenerator 被加载的时候,并不会创建 SingletonHolder 实例对象。只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载
    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.getAndIncrement();
      }
    }
    
  • 枚举

    • 基于枚举类型的单例实现。这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性
    public enum IdGenerator {
      INSTANCE;
    
      private AtomicLong id = new AtomicLong(0);
    
      public long getId() {
          return id.getAndIncrement();
      }
    }
    

单例存在的问题

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

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

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

    • 从理论上来讲,单例类也可以被继承、也可以实现多态,只是实现起来会非常奇怪,会导致代码的可读性变差。一旦选择将某个类设计成到单例类,也就意味着放弃了继承和多态这两个强有力的面向对象特性,也就相当于损失了可以应对未来需求变化的扩展性

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

    • 单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。如果代码比较复杂,这种调用关系就会非常隐蔽。在阅读代码的时候,我们就需要仔细查看每个函数的代码实现,才能知道这个类到底依赖了哪些单例类
  • 单例对代码的扩展性不友好

    • 如果未来某一天,我们需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动

    • 如:数据库连接池

      • 发现系统中有些 SQL 语句运行得非常慢,长时间占用数据库连接资源,导致其他 SQL 请求无法响应。
      • 为了解决这个问题,可以在系统中创建两个数据库连接池,慢 SQL 独享一个数据库连接池,其他 SQL 独享另外一个数据库连接池,这样就能避免慢 SQL 影响到其他 SQL 的执行。
      • 如果将数据库连接池设计成单例类,显然就无法适应这样的需求变更,也就是说,单例类在某些情况下会影响代码的扩展性、灵活性
      • 数据库连接池、线程池这类的资源池,最好还是不要设计成单例类
  • 单例对代码的可测试性不友好

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

    • 如果单例类持有成员变量(比如 IdGenerator 中的 id 成员变量),那它实际上相当于一种全局变量,被所有的代码共享。

      • 如果这个全局变量是一个可变全局变量,也就是说,它的成员变量是可以被修改的,那我们在编写单元测试的时候,还需要注意不同测试用例之间,修改了单例类中的同一个成员变量的值,从而导致测试结果互相影响的问题
  • 单例不支持有参数的构造函数

    • 单例不支持有参数的构造函数,比如我们创建一个连接池的单例对象,我们没法通过参数来指定连接池的大小

    • 第一种解决思路是:创建完实例之后,调用 init() 函数传递参数,再获取instance

    public class SingletonV1 {
    
      private static SingletonV1 instance = null;
      private final int paramA;
      private final int paramB;
    
      private SingletonV1(int paramA, int paramB) {
          this.paramA = paramA;
          this.paramB = paramB;
      }
    
      public static SingletonV1 getInstance() {
          if (instance == null) {
              throw new RuntimeException("Run init() first.");
          }
          return instance;
      }
    
      public synchronized static SingletonV1 init(int paramA, int paramB) {
          if (instance != null) {
              throw new RuntimeException("Singleton has been created.");
          }
          instance = new SingletonV1(paramA, paramB);
          return instance;
      }
    }
    
    • 第二种解决思路是:将参数放到 getIntance() 方法中
    public class SingletonV2 {
    
      private static SingletonV2 instance = null;
      private final int paramA;
      private final int paramB;
    
      private SingletonV2(int paramA, int paramB) {
          this.paramA = paramA;
          this.paramB = paramB;
      }
    
      public synchronized static SingletonV2 getInstance(int paramA, int paramB) {
          if (instance == null) {
              instance = new SingletonV2(paramA, paramB);
          }
          return instance;
      }
    }
    
    • 第三种解决思路是:将参数放到另外一个全局变量中(Config 是一个存储了 paramA 和 paramB 值的全局变量。里面的值既可以像下面的代码那样通过静态常量来定义,也可以从配置文件中加载得到。实际上,这种方式是最值得推荐的)
    public class SingletonV3 {
      private static SingletonV3 instance = null;
      private final int paramA;
      private final int paramB;
    
      private SingletonV3() {
          this.paramA = Config.PARAM_A;
          this.paramB = Config.PARAM_B;
      }
    
      public synchronized static SingletonV3 getInstance() {
          if (instance == null) {
              instance = new SingletonV3();
          }
          return instance;
      }
    }
    

单例的替代解决方案

  • 为了保证全局唯一,除了使用单例,我们还可以用静态方法来实现。这也是项目开发中经常用到的一种实现思路。但是,静态方法这种实现思路,并不能解决我们之前提到的问题。实际上,它比单例更加不灵活,比如,它无法支持延迟加载

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

  • 类对象的全局唯一性可以通过单例模式来强制保证,也可以通过工厂模式、IOC 容器(比如 Spring IOC 容器)来保证,还可以通过程序员自己来保证(自己在编写代码的时候自己保证不要创建两个类对象)

  • 如果单例类并没有后续扩展的需求,并且不依赖外部系统,那设计成单例类就没有太大问题。对于一些全局的类,我们在其他地方 new 的话,还要在类之间传来传去,不如直接做成单例类,使用起来简洁方便

扩展与思考

  • 如何理解单例模式的唯一性

    • 单例模式创建的对象是进程唯一的

    • 进程之间是不共享地址空间的,如果在一个进程中创建另外一个进程(比如,代码中有一个 fork() 语句,进程执行到这条语句的时候会创建一个新的进程),操作系统会给新进程分配新的地址空间,并且将老进程地址空间的所有内容,重新拷贝一份到新进程的地址空间中,这些内容包括代码、数据(比如 user 临时变量、User 对象)

  • 如何实现线程唯一的单例

    • “线程唯一”指的是线程内唯一,线程间可以不唯一

    • 通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java 语言本身提供了 ThreadLocal 工具类,可以更加轻松地实现线程唯一单例。不过,ThreadLocal 底层实现原理也是基于下面代码中所示的 HashMap

    public class IdGenerator {
    
      private AtomicLong id = new AtomicLong(0);
    
      private static final ConcurrentHashMap<Long, IdGenerator> instances = new ConcurrentHashMap<>();
    
      private IdGenerator() {
      }
    
      public static IdGenerator getInstance() {
          Long currentThreadId = Thread.currentThread().getId();
          instances.putIfAbsent(currentThreadId, new IdGenerator());
          return instances.get(currentThreadId);
      }
    
      public long getId() {
          return id.getAndIncrement();
      }
    
    }
    
  • 如何实现集群环境下的单例

    • 不同的进程间共享同一个对象,不能创建同一个类的多个对象

    • 需要把这个单例对象序列化并存储到外部共享存储区(比如文件)。

      • 进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。
      • 为了保证任何时刻,在进程间都只有一份对象存在,一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使用完这个对象之后,还需要显式地将对象从内存中删除,并且释放对对象的加锁
    public class IdGenerator {
    
      private AtomicLong id = new AtomicLong(0);
    
      private static IdGenerator instance;
    
      private static SharedObjectsStorage storage = new FileSharedObjectStorage("/user/xxx/xx");
    
      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 synchronized void freeInstance() {
          storage.save(this, IdGenerator.class);
          instance = null;
          lock.unlock();
      }
    
      public long getId() {
          return id.getAndIncrement();
      }
    }
    
  • 如何实现一个多例模式

    • 多例指的是一个类可以创建多个对象,但是个数是有限制的,比如只能创建 3 个对象
    public class BackendServer {
      private long serverNo;
      private String serverAddress;
    
      private static final int SERVER_COUNT = 3;
      private static final Map<Long, BackendServer> serverInstances = new HashMap<>(SERVER_COUNT);
    
      private BackendServer(long serverNo, String serverAddress) {
          this.serverNo = serverNo;
          this.serverAddress = serverAddress;
      }
    
      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"));
      }
    
      public static BackendServer getInstance(long serverNo) {
          return serverInstances.get(serverNo);
      }
    
      public static BackendServer getRandomInstance() {
          Random random = new Random();
          int no = random.nextInt(SERVER_COUNT) + 1;
          return serverInstances.get(no);
      }
    }
    
    • 多例也可以指同一类型的只能创建一个对象,不同类型的可以创建多个对象
    public class Logger {
    
      private static final ConcurrentHashMap<String, Logger> instances = new ConcurrentHashMap<>();
    
      private Logger() {
      }
    
      public static Logger getInstance(String loggerName) {
          instances.putIfAbsent(loggerName, new Logger());
          return instances.get(loggerName);
      }
    
      public void log() {
      }
    }
    
    • 多例模式创建的对象都是同一个类的对象,而工厂模式创建的是不同子类的对象

    • 枚举类型也相当于多例模式,一个类型只能对应一个对象,一个类可以创建多个对象

工厂模式

简单工厂

  • v1:根据配置文件的后缀选择不同的解析器,将存储在文件中的配置解析成内存对象
public class RuleConfigSource {

    public RuleConfig load(String ruleConfigFilePath) throws InvalidRuleConfigException {

        String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
        IRuleConfigParser parser = null;
        if ("json".equalsIgnoreCase(ruleConfigFileExtension)) {
            parser = new JsonRuleConfigParser();
        } else if ("xml".equalsIgnoreCase(ruleConfigFileExtension)) {
            parser = new XmlRuleConfigParser();
        } else if ("yaml".equalsIgnoreCase(ruleConfigFileExtension)) {
            parser = new YamlRuleConfigParser();
        } else if ("properties".equalsIgnoreCase(ruleConfigFileExtension)) {
            parser = new PropertiesRuleConfigParse();
        } else {
            throw new InvalidRuleConfigException("Rule config file format is not supported: " + ruleConfigFileExtension);
        }

        String configText = getFileContent(ruleConfigFilePath);
        RuleConfig ruleConfig = parser.parse(configText);
        return ruleConfig;
    }

    private String getFileContent(String filePath) {
        File file = new File(filePath);
        BufferedReader reader = null;
        StringBuffer stringBuffer = new StringBuffer();
        try {
            reader = new BufferedReader(new FileReader(file));
            String temp = null;
            while ((temp = reader.readLine()) != null) {
                stringBuffer.append(temp);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return stringBuffer.toString();
    }

    private String getFileExtension(String filePath) {
        return filePath.split(".")[-1];
    }

}
  • v2:将功能独立的代码块paser创建的部分,封装成函数,增加逻辑性和可读性
public class RuleConfigSource {

    public RuleConfig load(String ruleConfigFilePath) throws InvalidRuleConfigException {

        String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
        IRuleConfigParser parser = createParser(ruleConfigFileExtension);
        if (parser == null) {
            throw new InvalidRuleConfigException("Rule config file format is not supported: " + ruleConfigFileExtension);
        }

        String configText = getFileContent(ruleConfigFilePath);
        RuleConfig ruleConfig = parser.parse(configText);
        return ruleConfig;
    }

    private 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 PropertiesRuleConfigParse();
        }
        return parser;
    }
...
}
  • v3:进一步将createParser()函数剥离到一个独立的类,让类的职责更加单一、代码更加清晰;

    • 这个类就是简单工厂模式类
    • 大部分工厂类都是以“Factory”这个单词结尾的,但也不是必须的,比如 Java 中的 DateFormat、Calender
    • 工厂类中创建对象的方法一般都是 create 开头,比如代码中的 createParser(),但有的也命名为 getInstance()、createInstance()、newInstance(),有的甚至命名为 valueOf()(比如 Java String 类的 valueOf() 函数)等等,这个我们根据具体的场景和习惯来命名就好
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 PropertiesRuleConfigParse();
        }
        return parser;
    }
}
  • v4:将parser实现创建好并缓存起来,以节省内存和对象创建的时间
public class RuleConfigParserFactory {

    private static final Map<String, IRuleConfigParser> cachedParsers = new HashMap<>();

    static {
        cachedParsers.put("json", new JsonRuleConfigParser());
        cachedParsers.put("xml", new XmlRuleConfigParser());
        cachedParsers.put("yaml", new YamlRuleConfigParser());
        cachedParsers.put("properties", new PropertiesRuleConfigParse());
    }

    public static IRuleConfigParser createParser(String configFormat) {
        if (configFormat == null || configFormat.isEmpty()) {
            return null;
        }
        return cachedParsers.get(configFormat.toLowerCase());
    }
}
  • 总结:

    • 应用多态或设计模式来替代 if 分支判断逻辑,也并不是没有任何缺点的,它虽然提高了代码的扩展性,更加符合开闭原则,但也增加了类的个数,牺牲了代码的可读性

    • 尽管简单工厂模式的代码实现中,有多处 if 分支判断逻辑,违背开闭原则,但权衡扩展性和可读性,这样的代码实现在大多数情况下(比如,不需要频繁地添加 parser,也没有太多的 parser)是没有问题的

工厂方法

  • v1:如果要将if分支逻辑去掉,比较经典的处理方法就是利用多态的实现思路

    • 这就是工厂方法模式的典型代码实现
    • 当新增一种parser的时候,只需要新增一个实现了IRuleConfigParserFactory接口的Factory类即可
    • 所以,工厂方法模式比起简单工厂模式更符合开闭原则
    
    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();
      }
    }
    
    • 在使用工厂类时,工厂类对象的创建逻辑又耦合进了load()函数中,反倒让设计变得更加复杂
    public class RuleConfigSource {
    
        public RuleConfig load(String ruleConfigFilePath) throws InvalidRuleConfigException {
    
            String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
            IRuleConfigParserFactory factory = null;
    
            if ("json".equalsIgnoreCase(ruleConfigFileExtension)) {
                factory = new JsonRuleConfigParserFactory();
            } else if ("xml".equalsIgnoreCase(ruleConfigFileExtension)) {
                factory = new XmlRuleConfigParserFactory();
            } else if ("yaml".equalsIgnoreCase(ruleConfigFileExtension)) {
                factory = new YamlRuleConfigParserFactory();
            } else if ("properties".equalsIgnoreCase(ruleConfigFileExtension)) {
                factory = new PropertiesRuleConfigParserFactory();
            } else {
                throw new InvalidRuleConfigException("Rule config file format is not supported: " + ruleConfigFileExtension);
            }
            IRuleConfigParser parser = factory.createParser();
    
            String configText = getFileContent(ruleConfigFilePath);
            RuleConfig ruleConfig = parser.parse(configText);
            return ruleConfig;
        }
    ...
    }
    
  • v2:为工厂类再创建一个简单工厂,用来创建工厂类对象

    • 当需要添加新的规则配置解析器的时候,只需要创建新的 parser 类和 parser factory 类,并且在 RuleConfigParserFactoryMap 类中,将新的 parser factory 对象添加到 cachedFactories 中即可。代码的改动非常少,基本上符合开闭原则
    • 对于规则配置文件解析这个应用场景来说,工厂模式需要额外创建诸多 Factory 类,也会增加代码的复杂性,而且,每个 Factory 类只是做简单的 new 操作,功能非常单薄(只有一行代码),也没必要设计成独立的类,所以,在这个应用场景下,简单工厂模式简单好用,比工方法厂模式更加合适
    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;
            }
            return cachedFactories.get(type);
        }
    }
    
  • 什么时候该用工厂方法模式,而非简单工厂模式呢?

    • 如果代码块本身并不复杂,就几行代码而已,我们完全没必要将它拆分成单独的函数或者类
    • 基于这个设计思想,当对象的创建逻辑比较复杂,不只是简单的 new 一下就可以,而是要组合其他类对象,做各种初始化操作的时候,我们推荐使用工厂方法模式,将复杂的创建逻辑拆分到多个工厂类中,让每个工厂类都不至于过于复杂
    • 而使用简单工厂模式,将所有的创建逻辑都放到一个工厂类中,会导致这个工厂类变得很复杂
    • 除此之外,在某些场景下,如果对象不可复用,那工厂类每次都要返回不同的对象。如果我们使用简单工厂模式来实现,就只能选择第一种包含 if 分支逻辑的实现方式。如果我们还想避免烦人的 if-else 分支逻辑,这个时候,我们就推荐使用工厂方法模式

抽象工厂

  • 在简单工厂和工厂方法中,类只有一种分类方式。比如,在规则配置解析那个例子中,解析器类只会根据配置文件格式(Json、Xml、Yaml……)来分类。

  • 但是,如果类有两种分类方式,比如,我们既可以按照配置文件格式来分类,也可以按照解析的对象(Rule 规则配置还是 System 系统配置)来分类,那就会对应 8 个 parser 类

  • 让一个工厂负责创建多个不同类型的对象(IRuleConfigParser、ISystemConfigParser 等),而不是只创建一种 parser 对象。这样就可以有效地减少工厂类的个数


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代码

工厂模式总结

  • 当创建逻辑比较复杂,就考虑使用工厂模式,封装对象的创建过程,将对象的创建和使用相分离

    • 情况一:代码中存在if-else分支判断,动态地根据不同的类型创建不同的对象:将这一系列if-else创建对象的代码抽离出来,放到工厂类中

      • 当每个对象的创建逻辑都比较简单的时候,推荐简单工厂模式,将多个对象的创建逻辑放到一个工厂类中
      • 当每个对象的创建逻辑都比较复杂的时候,为了避免设计一个过于庞大的简单工厂类,推荐使用工厂方法模式,将创建逻辑拆分更细,每个对象的创建逻辑独立到各自的工厂类中
    • 情况二:单个对象本身的创建过程比较复杂,比如前面提到的要组合其他类对象,做各种初始化操作:将对象的创建过程封装到工厂类中

      • 因为单个对象本身的创建逻辑就比较复杂,所以建议使用工厂方法模式
    • 如果创建对象的逻辑并不复杂,就直接通过new来创建对象,不需要使用工厂模式

  • 工厂模式的作用,也是判断是否使用工厂模式的最本质的参考标准

    • 封装变化:创建逻辑有可能发生变化,封装成工厂类后,创建逻辑的变更对调用者透明
    • 代码复用:创建代码抽离到独立的工厂类之后可以复用
    • 隔离复杂性:封装复杂的创建逻辑,调用者无需了解如何创建对象
    • 控制复杂度:将创建代码抽离出来,让原本的函数或类职责更单一,代码更简洁

如何设计实现一个 Dependency Injection 框架

  • 工厂模式与 DI 容器有何区别

    • 依赖注入框架,或叫依赖注入容器(Dependency Injection Container),简称 DI 容器

    • DI 容器底层的最基本设计思路就是基于工厂模式的

      • DI 容器相当于一个大的工厂类,负责在程序启动的时候,根据配置(要创建哪些类对象,每个类对象的创建需要依赖哪些其他类对象)实现创建好对象
      • 当应用程序需要使用某个类对象的时候,直接从容器中获取即可
      • 因为它持有一堆对象,所以这个框架才被称为“容器”
  • DI 容器的核心功能有哪些

    • 配置解析

      • 需要通过一种形式,让应用告知 DI 容器要创建哪些对象,这种形式就是配置
      • 将需要由 DI 容器来创建的类对象和创建类对象的必要信息,放到配置文件中
      • 容器读取配置文件,根据配置文件提供的信息来创建对象
    • 对象创建

      • 在 DI 容器中,如果给每个类都对应创建一个工厂类,那么项目中类的个数会成倍增加
      • 只需要将所有类对象的创建都放到一个工厂类中完成就可以了,比如 BeansFactory
      • 因为创建的类对象非常多,为了避免 BeansFactory中的代码不会线性膨胀,使用反射机制,在程序运行中,动态地加载类、创建对象,不需要事先在代码中写死要创建哪些对象,所以,不管是创建一个对象还是十个对象,BeansFactory工厂类代码都是一样的
    • 对象的生命周期管理

      • 通过配置 scope 属性,区分 prototype 和 singleton 分别为新创建对象和返回单例对象
      • 通过配置 lazy-init 属性,区分是否懒加载
      • 通过配置 init-method 方法,在容器创建好对象之后,初始化对象
      • 通过配置 destroy-method 方法,在对象被销毁之前,做一些清理工作,如释放数据库连接池、关闭文件等
  • 如何实现一个简单的 DI 容器

    • 最小原型设计

      • 配置文件 beans.xml
      • 使用方式 ApplicationContext、ClassPathXmlApplication,getBean()
      public static void main(String[] args) {
          ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
          RateLimiter ratelimiter = ratelimiter = (RateLimiter) applicationContext.getBean("rateLimiter");
          ratelimiter.test();
      }
      
    • 提供执行入口

      • 面向对象的最后一步:组装类并提供执行入口,执行入口即一组暴露给外部使用的接口和类
      • 执行入口包含两部分:
        • ApplicationContext接口
        • ClassPathXmlApplication实现类:负责组装BeansFactory和BeanConfigParser两个类
          串联执行流程:
          1.从classpath中加载XML格式的配置文件
          2.通过BeanConfigParser解析为统一的BeanDefinition格式
          3.BeansFactory根据BeanDefinition来创建对象
      public class ClassPathXmlApplicationContext implements ApplicationContext {
        private BeanFactory      beanFactory;
        private BeanConfigParser beanConfigParser;
      
        public ClassPathXmlApplicationContext(String configLocation) {
            this.beanFactory = new BeanFactory();
            this.beanConfigParser = new XmlBeanConfigParser();
            loadBeanDefinition(configLocation);
        }
      
        private void loadBeanDefinition(String configLocation) {
            List<BeanDefinition> beanDefinitions = beanConfigParser.parse(configLocation);
            beanFactory.addBeanDefinitions(beanDefinitions);
        }
      
        @Override
        public Object getBean(String beanId) throws NoSuchBeanDefinitionException {
            return beanFactory.getBean(beanId);
        }
      }
      
    • 配置文件解析

      • BeanConfigParser接口
      • XmlBeanConfigParser实现类:负责将配置文件解析为BeanDefinition结构,以便BeansFactory根据这个结构来创建对象
      public class XmlBeanConfigParser implements BeanConfigParser {
      
        @Override
        public List<BeanDefinition> parse(String configLocation) {
            List<BeanDefinition> beanDefinitions = new ArrayList<>();
      
            File file = new File("E:\\IdeaProjects\\something\\target\\classes\\beans.xml");
            try {
                Document document = new SAXReader().read(file);
                Element rootElement = document.getRootElement();
                List<Element> beans = rootElement.elements("bean");
                for (int i = 0; i < beans.size(); i++) {
                    Element bean = beans.get(i);
                    String beanId = bean.attributeValue("id");
                    String beanClass = bean.attributeValue("class");
                    BeanDefinition beanDefinition = new BeanDefinition(beanId, beanClass);
                    if ("singleton".equals(bean.attributeValue("scope"))) {
                        beanDefinition.setScope(BeanDefinition.Scope.SINGLETON);
                    }
                    if ("true".equals(bean.attributeValue("lazy-init"))) {
                        beanDefinition.setLazyInit(true);
                    }
                    List<Element> args = bean.elements("constructor-arg");
                    loadConstructorArgs(beanDefinition, args);
                    beanDefinitions.add(beanDefinition);
                }
            } catch (DocumentException e) {
                e.printStackTrace();
            }
            return beanDefinitions;
        }
      
        private void loadConstructorArgs(BeanDefinition beanDefinition, List<Element> args) {
            for (int i = 0; i < args.size(); i++) {
                Element arg = args.get(i);
                BeanDefinition.ConstructorArg constructorArg = null;
                try {
                    if ((arg.attributeValue("type") != null) && (!arg.attributeValue("type").isEmpty())) {
                        constructorArg = new BeanDefinition.ConstructorArg();
                        constructorArg.setType(Class.forName(arg.attributeValue("type")));
                        constructorArg.setArg(arg.attributeValue("value"));
                    }
                    if ((arg.attributeValue("ref") != null) && !arg.attributeValue("ref").isEmpty()) {
                        constructorArg = new BeanDefinition.ConstructorArg();
                        constructorArg.setRef(true);
                        constructorArg.setArg(arg.attributeValue("ref"));
                    }
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
                beanDefinition.addConstructorArg(constructorArg);
            }
        }
      }
      
    • 核心工厂设计

      • DI 容器最核心的类:负责根据从配置文件解析得到的 BeanDefinition 来创建对象
        • 如果scope属性是singleton,对象创建后缓存在singletonObjects这样一个map中,下次再请求此对象时,直接从map中取出返回,不需要重新创建
        • 如果scope属性是prototype,那么每次请求对象,BeansFactory都会创建一个新的对象返回
      • BeansFactory创建对象用到的主要技术点就是Java中的反射语法:一种动态加载类和创建对象的机制
        • JVM在启动时会根据代码自动地加载类、创建对象:要加载哪些类、创建哪些对象,都是在代码中写死的
        • 如果某个对象的创建不是写死在代码中,而是放在配置文件中,就需要我们在程序运行期间,动态地根据配置文件来加载类、创建对象,这部分工作就无法让JVM帮我们自动完成,需要利用Java提供的反射语法自己去编写代码
      public class BeanFactory {
        private ConcurrentHashMap<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>();
        private ConcurrentHashMap<String, Object> singletonMap = new ConcurrentHashMap<>();
      
        public void addBeanDefinitions(List<BeanDefinition> beanDefinitions) {
            for (BeanDefinition beanDefinition : beanDefinitions) {
                beanDefinitionMap.putIfAbsent(beanDefinition.getId(), beanDefinition);
            }
            for (BeanDefinition beanDefinition : beanDefinitions) {
                if (!beanDefinition.isLazyInit() && beanDefinition.isSingleton()) {
                    createBean(beanDefinition);
                }
            }
        }
      
        private Object createBean(BeanDefinition beanDefinition) {
            if (beanDefinition.isSingleton() && singletonMap.containsKey(beanDefinition.getId())) {
                return singletonMap.get(beanDefinition.getId());
            }
            Object bean = null;
            try {
                Class beanClass = Class.forName(beanDefinition.getClassName());
      
                List<BeanDefinition.ConstructorArg> constructorArgs = beanDefinition.getConstructorArgs();
                if (constructorArgs.isEmpty()) {
                    bean = beanClass.newInstance();
                } else {
                    Class[] argClasses = new Class[constructorArgs.size()];
                    Object[] argObjects = new Object[constructorArgs.size()];
      
                    for (int i = 0; i < constructorArgs.size(); i++) {
                        BeanDefinition.ConstructorArg arg = constructorArgs.get(i);
                        if (!arg.isRef()) {
                            argClasses[i] = arg.getType();
                            argObjects[i] = arg.getArg();
                        } else {
                            BeanDefinition refBeanDefinition = beanDefinitionMap.get(arg.getArg());
                            if (beanDefinition == null) {
                                throw new NoSuchBeanDefinitionException("Bean is not defined: " + arg.getArg());
                            }
                            argObjects[i] = createBean(refBeanDefinition);
                            argClasses[i] = argObjects[i].getClass();
                        }
                    }
                    bean = beanClass.getConstructor(argClasses).newInstance(argObjects);
                }
            } catch (InstantiationException | InvocationTargetException | NoSuchMethodException | NoSuchBeanDefinitionException | IllegalAccessException | ClassNotFoundException e) {
                e.printStackTrace();
            }
            if (bean != null && beanDefinition.isSingleton()) {
                singletonMap.putIfAbsent(beanDefinition.getId(), bean);
                return singletonMap.get(beanDefinition.getId());
            }
            return bean;
        }
      
        public Object getBean(String beanId) throws NoSuchBeanDefinitionException {
            BeanDefinition beanDefinition = beanDefinitionMap.get(beanId);
            if (beanDefinition == null) {
                throw new NoSuchBeanDefinitionException("Bean is not defined: " + beanId);
            }
            return createBean(beanDefinition);
        }
      }
      

建造者模式

为什么需要建造者模式

  • 引入

    • ResourcePoolConfig 只有 4 个可配置项,对应到构造函数中,也只有 4 个参数
    • 但是,如果可配置项逐渐增多,那继续沿用现在的设计思路,构造函数的参数列表会变得很长,代码在可读性和易用性上都会变差。在使用构造函数的时候,我们就容易搞错各参数的顺序,传递进错误的参数值,导致非常隐蔽的 bug。
    • 解决这个问题的办法:通过构造函数设置必填项,通过 set() 方法设置可选配置项,就能实现我们的设计需求
    • 如果还需解决以下问题,现在的设计思路就不能满足:
      • 如果必填的配置项有很多,把这些必填配置项都放到构造函数中设置,那构造函数就又会出现参数列表很长的问题。如果我们把必填项也通过 set() 方法设置,那校验这些必填项是否已经填写的逻辑就无处安放了
      • 假设配置项之间有一定的依赖关系,比如,如果用户设置了 maxTotal、maxIdle、minIdle 其中一个,就必须显式地设置另外两个;或者配置项之间有一定的约束条件,比如,maxIdle 和 minIdle 要小于等于 maxTotal。如果我们继续使用现在的设计思路,那这些配置项之间的依赖关系或者约束条件的校验逻辑就无处安放了
      • 如果我们希望 ResourcePoolConfig 类对象是不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值。要实现这个功能,我们就不能在 ResourcePoolConfig 类中暴露 set() 方法
  • 建造者模式的实现

    • 把校验逻辑放置到 Builder 类中,先创建建造者,并且通过 set() 方法设置建造者的变量值,然后在使用 build() 方法真正创建对象之前,做集中的校验,校验通过之后才会创建对象
    • 除此之外,把 ResourcePoolConfig 的构造函数改为 private 私有权限。这样我们就只能通过建造者来创建 ResourcePoolConfig 类对象
    • 并且,ResourcePoolConfig 没有提供任何 set() 方法,这样我们创建出来的对象就是不可变对象
    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;
          }
      }
    }
    
  • 使用建造者模式创建对象,还能避免对象存在无效状态

    Rectangle r = new Rectange(); // r is invalid
    r.setWidth(2); // r is invalid
    r.setHeight(3); // r is valid
    
    • 为了避免这种无效状态的存在,就需要使用构造函数一次性初始化好所有的成员变量。如果构造函数参数过多,我们就需要考虑使用建造者模式,先设置建造者的变量,然后再一次性地创建对象,让对象一直处于有效状态

    • 如果并不是很关心对象是否有短暂的无效状态,也不是太在意对象是否是可变的。比如,对象只是用来映射数据库读出来的数据,那我们直接暴露 set() 方法来设置类的成员变量值是完全没问题的

    • 而且,使用建造者模式来构建对象,代码实际上是有点重复的,ResourcePoolConfig 类中的成员变量,要在 Builder 类中重新再定义一遍

建造者模式 与 工厂模式

  • 工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象

  • 建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”地创建不同的对象

原型模式

原型模式的原理与应用

  • 什么是原型模式

    • 如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的
    • 这种基于原型来创建对象的方式就叫作原型设计模式(Prototype Design Pattern),简称原型模式
  • 什么是“对象的创建成本比较大”?

    • 创建对象包含的申请内存、给成员变量赋值这一过程,本身并不会花费太多时间,或者说对于大部分业务系统来说,这点时间完全是可以忽略的。应用一个复杂的模式,只得到一点点的性能提升,这就是所谓的过度设计,得不偿失

    • 如果对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者需要从 RPC、网络、数据库、文件系统等非常慢速的 IO 中读取,这种情况下,我们就可以利用原型模式,从其他已有对象中直接拷贝得到,而不用每次在创建新对象的时候,都重复执行这些耗时的操作

  • 举例:系统A在启动时加载“搜索关键词”数据到内存中,系统B定期批量更新数据库中的数据,并且标记为新的数据版本。为保证系统A中数据的实时性,系统A需要定期根据数据库中的数据,更新内存中的索引和数据

    • v1:在A中记录当前数据的版本Va相应的更新时间Ta,从数据库中捞出更新时间大于Ta的所有搜索关键词,对每个关键词进行处理:如果已经在散列表中存在,就更新相应的搜索次数、更新时间等信息;如果在散列表中不存在,就将它插入到散列表中

    • v2:增加需求:任何时刻A中的所有数据都必须是同一版本;更新内存数据时,系统A不能处于不可用状态,即不能停机更新数据

      • 把正在使用的数据的版本定义为“服务版本”,当我们要更新内存中的数据的时候,并不直接在服务版本上更新,而是重新创建另一个版本数据,等新的版本数据建好之后,再一次性地将服务版本从版本 a 切换到版本 b。这样既保证了数据一直可用,又避免了中间状态的存在
      • 利用了 Java 中的 clone() 语法来复制一个对象。如果你熟悉的语言没有这个语法,那把数据从 currentKeywords 中一个个取出来,然后再重新计算哈希值,放入到 newKeywords 中也是可以接受的。毕竟,最耗时的还是从数据库中取数据的操作。相对于数据库的 IO 操作来说,内存操作和 CPU 计算的耗时都是可以忽略的
      • 通过调用 HashMap 上的 clone() 浅拷贝方法来实现原型模式

原型模式的实现方式

  • 深拷贝 与 浅拷贝

    • 浅拷贝只会复制图中的索引(散列表),不会复制数据(SearchWord 对象)本身
    • 深拷贝不仅仅会复制索引,还会复制数据本身
  • 实现深拷贝的方法

    • v3:递归拷贝对象、对象的引用对象、引用对象的引用对象…

    • v4:将对象序列化,在反序列化成新的对象

  • 深拷贝耗时、耗内存空间:

    • v5:先采用浅拷贝方式创建复制map;对于需要更新的SearchWord对象,再使用深度拷贝的方式创建一份新的对象,替换copyMap的老对象。因为需要更新的数据是很少的,这种方式既利用了浅拷贝节省时间、空间的优点,又能保证currentKeyWords中的数据都是老版本数据

总结

「真诚赞赏,手留余香」

WZhong

真诚赞赏,手留余香

使用微信扫描二维码完成支付