美文网首页计算机编程
23种模式 - 创建型

23种模式 - 创建型

作者: Zeppelin421 | 来源:发表于2022-04-18 08:53 被阅读0次

    创建型模式主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。
    单例模式:用来创建全局唯一的对象。
    工厂模式:用来创建不同但是相关类型的对象(继承同一父类或接口的一组子类),由给定的参数来决定创建哪种类型的对象
    建造者模式:用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象
    原型模式:针对创建成本比较大的对象,利用对已有对象复制的方式进行创建,以达到节省创建时间的目的

    单例模式

    单例设计模式(Singleton Design Pattern):一个类只允许创建一个对象(或实例),那这个类就是一个单例类,这种设计模式就叫做单例模式

    实现

    • 饿汉式
      饿汉式的实现方式,在类加载期间就已经将instance静态实例初始化好了,所以instance实例的创建是线程安全的,不过这样的实现方式不支持延迟加载实例
    public class IdGenerator {
        private AtomicLong id = new AtomicLong(0);
    
        private static final IdGenerator instance = new IdGenerator();
    
        public static IdGenerator getInstance() { return instance; }
    
        private IdGenerator() {}
    
        public long getId() {
            return id.incrementAndGet();
        }
    }
    
    • 懒汉式
      懒汉式相对于饿汉式的优势是支持延迟加载,这种实现方式会导致频繁加锁、释放锁,以及并发度低等问题,频繁的调用会产生性能瓶颈
    public class IdGenerator {
        private AtomicLong id = new AtomicLong(0);
    
        private static final IdGenerator instance = null;
    
        public static synchronized IdGenerator getInstance() {
            if (instance == null) {
                instance = new IdGenerator();
            }
            return instance;
        }
    
        private IdGenerator() {}
    
        public long getId() {
            return id.incrementAndGet();
        }
    }
    
    • 双重检测
      双重检测实现方式即支持延迟记载,又支持高并发。只要instance被创建之后,再调用getInstance函数都不会进入到加锁逻辑中。所以这种实现方式解决了懒汉式并发度低的问题
    public class IdGenerator {
        private AtomicLong id = new AtomicLong(0);
    
        private static final IdGenerator instance = null;
    
        public static IdGenerator getInstance() {
            if (instance == null) {
                synchronized(IdGenerator.class) {
                    if (instance == null) {
                        instance = new IdGenerator();
                    }           
                }
            }
            return instance;
        }
    
        private IdGenerator() {}
    
        public long getId() {
            return id.incrementAndGet();
        }
    }
    
    • 静态内部类
      利用Java的静态内部类来实现单例。这种实现方式即支持延迟加载,也支持高并发,实现起来比双重检测简单
    public class IdGenerator {
    
        private AtomicLong id = new AtomicLong(0);
    
        private static class SingletonHolder {
            private static final IdGenerator instance = new IdGenerator();
        }
    
        public static IdGenerator getInstance() {
            return SingletonHolder.instance;
        }
    
        private IdGenerator() {}
    
        public long getId() {
            return id.incrementAndGet();
        }
    }
    
    • 枚举
      基于枚举类型的单例实现。这种实现方式通过Java枚举类型本身的特性,保证了实例创建的线程安全和实例的唯一性。
    public enum IdGenerator {
        INSTANCE;
    
        private AtomicLong id = new AtomicLong(0);
    
        public long getId() {
            return id.incrementAndGet();
        }
    }
    

    存在问题

    • 单例对 OOP 特性的支持不友好
      IdGenerator的使用方式违背了基于接口而非实现的设计原则,也就违背了广义上理解的 OOP 的抽象特性。如果未来需要针对不同业务采用不同的ID生成算法,需要修改所有用到IdGenerator类的地方,代码改动会比较大。
    • 单例会隐藏类之间的依赖关系
      代码的可读性非常重要。在阅读代码时,希望一眼就能看出类与类之前的依赖关系,搞清楚这个类依赖了哪些外部类。单例类不需要显示创建,不需要依赖参数传递,在函数中直接调用就可以,如果代码比较复杂,这种调用关系就会非常隐蔽。
    • 单例对代码的扩展性不友好
      系统设计初期,觉得系统中应该只需要一个数据库连接池,这样方便控制对数据库连接资源的消耗,所以将数据库连接池设计成单例。后期发现有些SQL执行非常慢,长时间占用数据库连接资源,导致其他SQL请求无法响应。为了解决问题,需要创建两个数据库连接池,慢SQL独享一个,其他SQL独享一个。显然这种单例无法适应这样的需求变更,影响代码的扩展性、灵活性。
    • 单例对代码的可测试性不友好
      如果单例类依赖比较重的外部资源,在写单元测试时,无法通过mock替换;单例类成员变量实际上相当于一种全局变量,被所有代码共享,在编写单元测试的时候,需要注意不同测试用例之间不要互相影响。
    • 单例不支持有参数的构造函数
      单例不支持有参数的构造函数。
      方案一:创建完实例后,再调用init,init必须确保在getInstance之前调用;
      方案二:将参数放到getInstance中;
      方案三:将参数放到全局变量中

    替代方案

    • 使用静态方法来实现
    • 通过工厂模式、IOC容器等

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

    定义中提到“一个类只允许创建唯一一个对象”,那对象的唯一性的作用范围是什么呢?单例模式创建的对象是进程唯一的。

    如何实现线程唯一的单例

    通过一个HashMap来存储对象,key是线程ID,value是对象。例如Java语言本身提供的ThreadLocal工具类

    如何实现集群环境下的单例

    需要把单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。
    为了保证任何时刻,在进程间都只有一份对象存在,一个进程在获取对象之后,需要对对象加锁,避免其他进程再将其获取。

    如何实现一个多例模式

    “单例”指的是一个类只能创建一个对象。“多例”指的就是一个类可以创建多个对象,但个数是有限的。多例的实现也比较简单,通过一个Map来存储对象类型和对象之间的对应关系,来控制对象的个数。

    工厂模式

    工厂模式(Factory Design Pattern)是最常用的实例化对象模式,是用工厂方法代替 new 操作的一种模式。在创建对象时,不会对客户端暴露对象的创建逻辑,而是通过使用共同的接口来创建对象。

    简单工厂(Simple Factory)

    简单工厂模式中创建实例的方法通常为静态方法,因此简单工厂模式又叫做静态工厂方法模式

    public class RuleConfigSource {
      public RuleConfig load(String ruleConfigFilePath) {
        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 PropertiesRuleConfigParser();
        } else {
          throw new InvalidRuleConfigException(
                 "Rule config file format is not supported: " + ruleConfigFilePath);
        }
    
        String configText = "";
        //从ruleConfigFilePath文件中读取配置文本到configText中
        RuleConfig ruleConfig = parser.parse(configText);
        return ruleConfig;
      }
    
      private String getFileExtension(String filePath) {
        //...解析文件名获取扩展名,比如rule.json,返回json
        return "json";
      }
    }
    
    // 1、为了让代码逻辑更加清晰,可读性更好,要善于将功能独立的代码封装成函数
    // 2、为了让类的职责更加单一、代码更加清晰,可以将独立的函数剥离到一个单独的类中
    
    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 = "";
        //从ruleConfigFilePath文件中读取配置文本到configText中
        RuleConfig ruleConfig = parser.parse(configText);
        return ruleConfig;
      }
    }
    
    // 第一种简单工厂模式
    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;
      }
    }
    
    // 第二种简单工厂模式
    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;//返回null还是IllegalArgumentException全凭你自己说了算
        }
        IRuleConfigParser parser = cachedParsers.get(configFormat.toLowerCase());
        return parser;
      }
    }
    

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

    工厂方法(Factory Method)

    工厂方法模式是对简单工厂模式的进一步抽象化,其好处是可以使系统在不修改原来代码的情况下引进新的产品,即满足开闭原则

    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<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;
      }
    }
    
    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();
      }
    }
    

    对于当前场景来说,工厂模式需要额外创建诸多 Factory 类,也会增加代码的复杂性,而且每个 Factory 类只是做简单的 new 操作,功能非常单薄,也没有必要设计成独立的类,所以当前场景下,简单工厂模式简单好用,比工厂模式更加合适。

    什么时候该用工厂方法模式,而非简单工厂模式
    • 当对象的创建逻辑比较复杂,而不是简单的 new 一下就可以,而是要组合其他类对象,做各种初始化操作的时候,推荐使用工厂方法模式
    • 如果对象不可复用,那工厂类每次都要返回不同的对象,使用简单工厂模式就只能选择包含 if-esle 分支逻辑的实现方式,为了避免这种分支逻辑,推荐使用工厂方法模式

    抽象工厂(Abstract Factory)

    在简单工厂和工厂方法中,类只有一种分类方式。但如果类有两种分类方式,比如既可以按照配置文件格式来分类,也可以按照解析的对象来分类,那就会对应8个 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 分支判断,动态地根据不同的类型创建不同的对象
      当每个对象的创建逻辑都比较简单的时候,推荐使用简单工厂模式,将多个对象的创建逻辑放到一个工厂类中;当每个对象的创建逻辑都比较复杂的时候,为了避免设计一个过于庞大的简单工厂类,推荐使用工厂方法模式,将创建逻辑拆分得更细
    • 单个对象本身的创建过程比较复杂
      推荐使用工厂方法模式

    上升到思维层面来看工厂模式,可以从四个方面判断要不要使用工厂模式:

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

    建造者模式

    建造者模式(Builder Design Pattern、构建者模式、生成器模式)是设计模式的一种,将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

    为什么需要建造者模式

    如果一个类中有很多属性,为了避免构造函数的参数列表过长,影响代码的可读性和易用性,可以通过构造函数配合 set 方法来解决。
    如果存在以下几种情况,就需要考虑使用构造者模式

    • 必填属性过多
      把类的必填属性放到构造函数中,强制创建对象的时候就设置。如果必填属性也有很多,就会导致构造函数又会出现参数列表过长。如果把必填属性通过 set 方法设置,那校验这些必填属性是否已填写的逻辑就无处安放
    • 类属性之间有一定的依赖关系或约束条件
      如果类属性之间有依赖关系或约束条件,继续使用构造函数配合 set 方法的思路一样会导致这些依赖关系或约束条件的校验逻辑无处安放
    • 创建出不可变对象
      对象在创建好之后,就不能再修改内部的属性值,要实现这个功能,就不能在类中暴露 set 方法。构造函数配合 set 方法来设置属性值的方式就不合适了
    public class ResourcePoolConfig {
      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(String name, Integer maxTotal, Integer maxIdle, Integer minIdle) {
        if (StringUtils.isBlank(name)) {
          throw new IllegalArgumentException("name should not be empty.");
        }
        this.name = name;
    
        if (maxTotal != null) {
          if (maxTotal <= 0) {
            throw new IllegalArgumentException("maxTotal should be positive.");
          }
          this.maxTotal = maxTotal;
        }
    
        if (maxIdle != null) {
          if (maxIdle < 0) {
            throw new IllegalArgumentException("maxIdle should not be negative.");
          }
          this.maxIdle = maxIdle;
        }
    
        if (minIdle != null) {
          if (minIdle < 0) {
            throw new IllegalArgumentException("minIdle should not be negative.");
          }
          this.minIdle = minIdle;
        }
      }
      //...省略getter方法...
    }
    
    // 采用构建者模式重构
    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();
    

    与工厂模式有何区别

    工厂模式是用来创建不同但是相关类型的对象(继承同一父类或接口的一组子类),由给定的参数决定创建哪种类型的对象。
    构造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”地创建不同的对象

    顾客走进一家餐厅点餐,利用工厂模式,根据用户不同的选择,来制作不同的食物,比如披萨、汉堡、沙拉。对于披萨来说,用户又有各种配料可以定制,比如奶酪、西红柿、起司,通过构造者模式根据用户选择的不同配料来制作披萨

    只有了解这些最本质的东西,才能不生搬硬套,才能灵活应用,甚至可以混用各种模式创造出新的模式,来解决特定场景的问题

    原型模式

    如果对象的创建成本比较大,而同一个类的不同对象之间差别不大,这种情况下,可以利用对已有对象(原型)进行复制(或拷贝)的方式来创建新对象,已达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式(Prototype Design Pattern)

    何为“对象的创建成本比较大”

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

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

    原型模型的实现方式:深拷贝和浅拷贝

    private HashMap currentKeywords=new HashMap<>();
    // 原型模式就这么简单,拷贝已有对象的数据,更新少量差值 
    HashMap newKeywords = (HashMap) currentKeywords.clone();
    

    浅拷贝和深拷贝的区别在于,浅拷贝只会复制图中的索引(散列表),不会复制数据(SearchWord对象)本身。相反,深拷贝不仅仅会复制索引,还会复制数据本身。浅拷贝得到的对象(newKeywords)跟原始对象(currentKeywords)共享数据(SearchWord对象),而深拷贝得到的是一份完完全全独立的对象


    浅拷贝
    深拷贝

    Java语言中,Object类的 clone() 方法执行的就是浅拷贝。它只会拷贝对象中的基本数据类型的数据(int,long),以及引用对象(SearchWord)的内存地址,不会递归地拷贝引用对象本身。

    如何深拷贝

    • 递归拷贝对象、对象的引用对象以及引用对象的引用对象...
    • 先将对象序列化,然后再反序列化成新的对象

    针对HashMap这种新老替换的数据结构,可以先采用浅拷贝的方式创建 newKeywords。对于需要更新的 SearchWord 对象,再使用深拷贝的方式创建一份新的对象,替换 newKeywords 中的老对象。这种方式即利用了浅拷贝节省时间、空间的优点,又能保证 currentKeywords 中的数据都是老版本的。

    相关文章

      网友评论

        本文标题:23种模式 - 创建型

        本文链接:https://www.haomeiwen.com/subject/qeakertx.html