zoukankan      html  css  js  c++  java
  • 设计模式之美

    设计模式之美 - 工厂模式

    设计模式之美目录:https://www.cnblogs.com/binarylei/p/8999236.html

    工厂模式实现了创建者和调用者的分离。工厂模式可分为三种类型:简单工厂、工厂方法、抽象工厂。不过,在 GoF 的《设计模式》一书中,它将简单工厂模式看作是工厂方法模式的一种特例,所以工厂模式只被分成了工厂方法和抽象工厂两类。实际上,前面一种分类方法更加常见。

    首先,我们要明确的是使用工厂模式并不能减少代码行数,只是将合适的代码放在合适的类中,这其实也适用于其它所有的设计模式。不使用工厂模式的什么缺陷呢?最大的问题是对象创建逻辑(if-else)和业务代码耦合在一起,代码可读性可维护性都很差。

    • 简单工厂:也叫静态方法工厂,根据不同的条件创建不同的对象,if-else 逻辑在这个工厂类中。
    • 工厂方法:一个工厂方法只创建一类对象。一般先用简单工厂类来得到某个工厂方法,再用这个工厂方法来创建对象,if-else 逻辑在简单工厂类中。
    • 抽象工厂:复杂对象的创建,一般用于产品簇的创建。相对于前两种工厂模式,使用比较少。

    当然,工厂类的创建对象也不全部是 if-else 逻辑,也可以根据参数拼凑出类名,然后使用反射创建对象。如 Jdk 中创建 URL 协议的工厂类 sun.misc.Launcher.Factory 就是根据参数 protocol 拼凑类名 sun.net.www.protocol.${protocol}.Handler。

    下面分别介绍一下这三种工厂模式,重点关注它们的使用场景。

    1. 简单工厂(Simple Factory)

    1.1 场景分析

    需求分析:配置文件的解析类 IRuleConfigParser 有 xml、yaml、properteis、json 等不同格式的解析,使用时需要根据文件的后缀名获取不同的解析器进行解析。

    在 v1 版本中,我们的实现方案简单粗暴,代码实现如下:

    public RuleConfig load(String ruleConfigFilePath) {
        String fileExtension = getFileExtension(ruleConfigFilePath);
        IRuleConfigParser parser = null;
        if ("json".equalsIgnoreCase(fileExtension)) {
            parser = new JsonRuleConfigParser();
        } else if ("xml".equalsIgnoreCase(fileExtension)) {
            parser = new XmlRuleConfigParser();
        } else if ("yaml".equalsIgnoreCase(fileExtension)) {
            parser = new YamlRuleConfigParser();
        } else if ("properties".equalsIgnoreCase(fileExtension)) {
            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;
    }
    

    说明: 很明显,v1 版本最大的问题是解析器的创建和业务耦合在一起,影响代码的可读可维护性,也不符合单一职责原则。这样就有了 v2 版本,在 v2 版本中将解析器的创建过程提取出成一个单独的方法。

    public RuleConfig load(String ruleConfigFilePath) {
        String fileExtension = getFileExtension(ruleConfigFilePath);
        IRuleConfigParser parser = createParser(fileExtension);
    
        String configText = "";
        RuleConfig ruleConfig = parser.parse(configText);
        return ruleConfig;
    }
    
    // 提出创建解析器的方法
    private IRuleConfigParser createParser(String fileExtension) {
        IRuleConfigParser parser = null;
        if ("json".equalsIgnoreCase(fileExtension)) {
            parser = new JsonRuleConfigParser();
        } 
        ...
        return parser;
    }
    

    说明: 在 v2 版本中将解析器的创建过程单独抽象成一个单独的方法。

    1.2 简单工厂

    简单工厂也叫静态工厂方法模式(Static Factory Method Pattern),这是因为其中创建对象的方法是静态的。

    在 v2 版本中已经将解析器的创建过程单独抽象成一个单独的方法,不过,为了让类的职责更加单一、代码更加清晰,我们还可以进一步将 createParser() 函数剥离到一个独立的类中,让这个类只负责对象的创建。而这个类就是我们现在要讲的简单工厂模式类 v3。具体的代码如下所示:

    public RuleConfig load(String ruleConfigFilePath) {
        String fileExtension = getFileExtension(ruleConfigFilePath);
        IRuleConfigParser parser = RuleConfigParserFactory_v1.createParser(fileExtension);
    
        String configText = "";
        RuleConfig ruleConfig = parser.parse(configText);
        return ruleConfig;
    }
    
    // 简单工厂:负责解析器的创建,在简单工厂模式中,会根据条件创建不同的解析器
    public class RuleConfigParserFactory_v1 {
        public static IRuleConfigParser createParser(String configFormat) {
            IRuleConfigParser parser = null;
            if ("json".equalsIgnoreCase(fileExtension)) {
                parser = new JsonRuleConfigParser();
            } else if ("xml".equalsIgnoreCase(configFormat)) {
                parser = new XmlRuleConfigParser();
            } 
            ...
            return parser;
        }
    }
    

    说明: 在 v3 版本中,解析器的创建由单独的工厂类负责,也就是简单工厂模式。在简单工厂模式中,工厂类会根据不同的条件创建不同的解析器对象。你可能会说,如果新增加一种格式的解析器,不是也需要修改这个简单工厂类吗?是的,但我认为这种修改,我们是可以接受的。

    在很多场景中,我们会提前将解析器初始化完成,放到缓存中,使用时直接取出即可,这有点类似 "简单工厂 + 单例模式"。代码如下:

    public class RuleConfigParserFactory_v2 {
        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 PropertiesRuleConfigParser());
        }
    
        public static IRuleConfigParser createParser(String configFormat) {
            if (configFormat == null || configFormat.isEmpty()) {
                // 返回 null 还是 IllegalArgumentException 全凭你自己说了算
                return null;
            }
            IRuleConfigParser parser = cachedParsers.get(configFormat.toLowerCase());
            return parser;
        }
    }
    

    大部分工厂类都是以 "Factory" 这个单词结尾的,但也不是必须的,比如 Java 中的 DateFormat、Calender。除此之外,工厂类中创建对象的方法一般都是 create 开头,比如代码中的 createParser(),但有的也命名为 getInstance()、createInstance()、newInstance(),有的甚至命名为 valueOf()(比如 Java String 类的 valueOf() 函数)等等,这个我们根据具体的场景和习惯来命名就好。

    2. 工厂方法模式(Factory Method)

    工厂方法模式比起简单工厂模式更加符合开闭原则。我们可以为工厂类再创建一个简单工厂,也就是工厂的工厂,用来创建工厂类对象。

    一般有以下两种场景下,我们需要使用工厂方法模式:

    1. 高扩展性。简单工厂模式扩展性差,新增加一种实现都需要修改源码。
    2. 对象创建复杂。如果每个解析器对象创建过程都很复杂,需要好几个步骤,那么我更推荐使用工厂方法,将复杂的对象创建过程封装起来。

    2.1 工厂方法典型实现

    在 v3 版本中,如果我们需要扩展一种新的 ini 格式的解析器,就需要修改简单工厂类,有没有一种方式,不需要修改工厂类呢?答案就是工厂方法。

    在 v4 版本中,我们提供了一个 IRuleConfigParserFactory#createParser 接口来创建对应的解析器,不同的解析器工厂只要实现这个接口即可。

    public RuleConfig load(String ruleConfigFilePath) {
        String fileExtension = getFileExtension(ruleConfigFilePath);
        IRuleConfigParserFactory parserFactory = null;
        if ("json".equalsIgnoreCase(fileExtension)) {
            parserFactory = new JsonRuleConfigParserFactory();
        } else if ("xml".equalsIgnoreCase(fileExtension)) {
            parserFactory = new XmlRuleConfigParserFactory();
        } 
        ...
        IRuleConfigParser parser = parserFactory.createParser();   
        return parser.parse(...);
    }
    

    说明: 在 v4 版本中,我们先需要获取工厂方法的工厂类,再通过这个工厂类创建解析器。此时,获取工厂方法的逻辑又和业务逻辑耦合,为此可以再使用一个简单工厂来创建工厂方法。

    // 通过简单工厂创建工厂方法,再通过工厂方法创建对象。
    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;
        }
    }
    

    说明: 在 v5 版本中,我们通过简单工厂创建工厂方法,再通过工厂方法创建对象,这是工厂方法最常用的方式。你可能会说这不又回到 if-else 模式了吗?每增加一种解析器的实现不也是要修改这个简单工厂吗?事实上,if-else 的逻辑我们是逃不掉的,只能将最将合适的代码放在合适的类中。当然我们也有一些手段来避免修改简单工厂类,最核心的思想其实就是外部化配置。

    2.2 外部化配置

    在 v5 版本中,工厂方法的获取是通过简单工厂 RuleConfigParserFactoryMap 创建的,其中使用了大量的 if-else,在程序升级过程中,也需要修改这个简单工厂。虽然,这个简单工厂类代码修改非常简单,但还是违反了开闭原则。

    那有没有一种方法,只增加对应的工厂方法扩展类,不修改这个简单工厂类呢?答案是有的,而且还是有多种方式,但核心的思想都是外部化配置。

    • SPI:Service Provider Interface,是 JDK 提供的一种外部化配置手段。
    • Spring IoC:通过外部化配置,向容器中直接注入工厂方法实例。
    • 契约编程:通过参数获取工厂方法的实现类名称,再通过 JDK 反射创建工厂方法实例。如 "URL 协议扩展"
    • 自定义外部化配置:如 "Dubbo 自适应扩展"。

    2.2.1 SPI

    此时,简单工厂通过 JDK SPI 机制 ServiceLoader 加载工厂方法实例,而不是通过 if-else。

    在 META-INF/services 下,配置 com.binarylei.design.factory.IRuleConfigParserFactory 文件:

    com.binarylei.design.factory.JsonRuleConfigParserFactory
    

    2.2.2 Spring IoC

    通过 Config 类,直接向容器中注入工厂方法实例,本质还是外部配置思想。

    2.2.3 契约编程

    通过参数获取工厂方法的实现类名称,再通过 JDK 反射创建工厂方法实例。 以 URL 协议扩展为例。

    当实例化 URL 时,会获取参数 url 对应的协议 protocol,再通过协议 protocol 获取其对应的工厂方法。我们看一下这个工厂类 sun.misc.Launcher.Factory,Factory 类会通过协议获取对应的工厂方法名称,再通过反射创建工厂方法实例。代码非常简单,实际上是通过契约编程的方式,规避 if-else。这样扩展时,只需要实现对应的协议的实现类即可。

    new URL("http://www.baidu.com").openConnection()
        
    // 通过契约编程的方式,规避 if-else
    private static class Factory implements URLStreamHandlerFactory {
        private static String PREFIX = "sun.net.www.protocol";
        public URLStreamHandler createURLStreamHandler(String protocol) {
            String name = PREFIX + "." + protocol + ".Handler";
            Class<?> c = Class.forName(name);
            return (URLStreamHandler)c.newInstance();
        }
    }
    

    2.2.4 自定义

    自定义外部配置,这样可以读取配置文件,通过参数获取工厂方法名称。以 Dubbo 自适应扩展为例。

    Dubbo 协议会在 META-INF/dubbo/internal 下配置 org.apache.dubbo.rpc.Protocol 文件

    dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
    

    这样,我们就可以通过 Dubbo URL 中的 protocol 参数查找对应的工厂方法名称。

    3. 抽象工厂(Abstract Factory)

    抽象工厂模式的应用场景比较特殊,没有前两种常用,所以不是我们本节课学习的重点,简单了解一下就可以了。

    如果对象由多个组件组成,如 IRuleConfigParser 和 ISystemConfigParser,这时候不可能针对每个组件都编写一个工厂类,也不可能让用户来组装最终的对象,这时就需要用到抽象工厂模式。

    4. 什么时候使用工厂模式

    当创建逻辑比较复杂,是一个 "大工程" 的时候,我们就考虑使用工厂模式,封装对象的创建过程,将对象的创建和使用相分离。何为创建逻辑比较复杂呢?我总结了下面两种情况。

    1. 对象创建存在 if-else 分支判断,动态地根据不同的类型创建不同的对象。针对这种情况,我们就考虑使用简单工厂模式,对象创建逻辑和业务逻辑分离。简单工厂模式的扩展性比较差。
    2. 如果代码扩展性要求高,或单个对象创建比较复杂,我们也可以考虑使用工厂方法模式。
    3. 如果对象由多个组件组成,每个组件都有不同的创建方式,这里往往就是抽象工厂。

    现在,我们上升一个思维层面来看工厂模式,它的作用无外乎下面这四个。这也是判断要不要使用工厂模式的最本质的参考标准。

    1. 封装变化:创建逻辑有可能变化,封装成工厂类之后,创建逻辑的变更对调用者透明。
    2. 代码复用:创建代码抽离到独立的工厂类之后可以复用。
    3. 隔离复杂性:封装复杂的创建逻辑,调用者无需了解如何创建对象。

    每天用心记录一点点。内容也许不重要,但习惯很重要!

  • 相关阅读:
    Python Web框架Django (三)
    谷歌把域名标记为不安全的解决办法
    tkmybatis VS mybatisplus
    jdbc预编译实现方式
    分析mybatis中 #{} 和${}的区别
    实体类id的几种生成方式
    java 获取mac地址
    javafx 表格某一列设置未复选框
    关闭在chrome里使用双指前进后退页面的功能
    调试maven源代码
  • 原文地址:https://www.cnblogs.com/binarylei/p/8999849.html
Copyright © 2011-2022 走看看