zoukankan      html  css  js  c++  java
  • 【转】从对象创建和引用小议解耦

    从对象创建和引用小议解耦

    应用场景

    为方便后续介绍,本文假设一个计算器的应用。初始设计由以下几部分组成:

    • 计算器界面类 CalculatorUI该类接受用户输入的表达式,执行一些输入校验工作,并将合法的表达式传递到具体的分析器,最终将计算结果返回给用户。
    • 语法分析器接口,ExpressionEvaluator及其实现类 ExpressionEvaluatorImpl,其承担实际的计算工作。

    在该应用场景中,CalculatorUI类需要持有指向 ExpressionEvaluator实现的引用,以便在运行时委派其实际的计算工作。文章后续将围绕如何持有和初始化 ExpressionEvaluator实现展开介绍,并依次提出多种解决方案。

    清单 1. 计算器实现方式一 (new 操作符 )
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class CalculatorUI {
     
        private ExpressionEvaluator expressionEvaluator;
     
        public CalculatorUI() {
            expressionEvaluator = new ExpressionEvaluatorImpl();
        }
     
        public String evaluate(String expression) {
            if (expression == null || expression.isEmpty()) {
                throw new IllegalArgumentException("[" + expression + "]
                    is not a valid expression");
            }
            return expressionEvaluator.evaluate(expression);
        }
     }

    清单 1 展示了一种非常典型的实现方式,即直接在 CalculatorUI 中使用 new 操作符创建 ExpressionEvaluator 的实例。该种方式虽然简单明了,但是将 ExpressionEvaluator 的实现类 ExpressionEvaluatorImpl 被硬编码在 CalculatorUI 的代码中,两者的耦合程度相当高。设想该计算器应用程序在后续使用时,如果希望更改成其他 ExpressionEvaluator 的实现,则必须修改 CalculatorUI 的代码并重新编译。

    清单 2. 计算器实现方式二 ( 工厂模式 )
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class ExpressionEvaluatorFactory {
     
        public static ExpressionEvaluator createExpressionEvaluator() {
            return new ExpressionEvaluatorImpl();
        }
     }
     
     public class CalculatorUI {
     
        private ExpressionEvaluator expressionEvaluator;
     
        public CalculatorUI() {
            expressionEvaluator = ExpressionEvaluatorFactory.createExpressionEvaluator();
        }
         
        ......
     }

    针对 清单 1 中所示实现方式的缺点,在 清单 2 中引入常见的静态工厂设计模式。引入工厂类 ExpressionEvaluatorFactory 后,CaculatorUI将不需要自行创建和初始化 ExpressionEvaluator 的实现,而是将相关工作委派给工厂类。相对于第一种实现方式,CalculatorUI 脱离了与 ExpressionEvaluatorImpl 之间的直接耦合,对其而言,它只需要调用 ExpressionEvaluatorFactory的 createExpressionEvaluator方法即可。使用方式二的计算器应用程序如果需要更换 ExpressionEvaluator实现,无需修改 CalculatorUI的代码,只要改动 ExpressionEvaluatorFactory工厂类的代码。正如通常所说的,客户端代码不需要修改了,而且当应用程序多处使用 ExpressionEvaluatorFactory 时,其优势更加明显。

    通过 清单 2 所示的第二种实现方式,清单 1 实现方式的缺点得到解决,但是值得注意的是,耦合并没有消除,而是转移到 ExpressionEvaluatorFactory 工厂类中。当需要换成其他 ExpressionEvaluator 的实现时,仍然需要修改并重新编译 ExpressionEvaluatorFactory 类,当然优势自然是客户端代码无需修改了。在此我们引入第三种实现方式。

    清单 3. 计算器实现方式三 (Service Look-up)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    public class ExpressionEvaluatorFactory {
     
        public static final String EXPRESSION_EVALUATOR_PROPERTY_NAME="ExpressionEvaluator";
     
        private static final String DEFAULT_EXPRESSION_EVALUATOR_IMPL
            = "ExpressionEvaluatorImpl";
     
        public static ExpressionEvaluator createExpressionEvaluator() {
            String implClassName = loadFromSystemProperty();
            if (implClassName == null) {
                implClassName = loadFromJREPropertyFile();
                if (implClassName == null) {
                    implClassName = loadFromServiceEntryURL();
                    if (implClassName == null) {
                        implClassName = DEFAULT_EXPRESSION_EVALUATOR_IMPL;
                    }
                }
            }
            Class cls = loadClass(implClassName);
            try {
                return (ExpressionEvaluator) cls.newInstance();
            } catch (Exception e) {
                throw new ExpressionEvaluatorException("Fail to create instance of ["
                    + implClassName + "]", e);
            }
        }
     
        private static Class loadClass(String implClassName) {
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            try {
                if (cl != null) {
                    return cl.loadClass(implClassName);
                } else {
                    return Class.forName(implClassName);
                }
            } catch (Exception e) {
                throw new ExpressionEvaluatorException("Fail to load class ["
                    + implClassName + "]", e);
            }
        }
         
        ......
    }

    在 清单 3 中,对工厂类 ExpressionEvaluatorFactory 做了修改,在创建 ExpressionEvaluator 实例时,采取了常见的 Service Look-up 方式。即依次通过如下方式搜索实现类:

    • 读取系统属性,用户可以在应用启动时通过 -D 选项进行设置。
    • 读取 JRE 目录中的某个属性文件,以获取实现类类名。
    • 通过类载入器检索记录实现类类名的文件,常见的检索格式为 META=INF/services/ExpressionEvaluator.
    • 如通过以上途经均未找到合适的实现类,则使用默认的实现类 ExpressionEvaluatorImpl

    事实上,在 Java EE 平台中,很多组件均使用此方式检索服务端接口的实现类,例如 javax.xml.bind.JAXBContextjavax.el.ExpressionFactoryjavax.xml.soap.MessageFactory等。由此,不但避免了工厂类与实现类之间的紧耦合,而且提供多种策略在运行时获取实现类。可以预见,使用第三种方式实现的计算器应用,可以非常方便的更换 ExpressionEvaluator的实现类而无需修改和编译 ExpressionEvaluatorFactory的代码,例如在应用启动时设置对应系统属性值为实现类类名等。

    继续观察包括 清单 3在内的以上实现方式,可以发现,无论是直接使用 new操作符创建,还是使用 ExpressionEvaluatorFactory工厂类创建,均是由 CalculatorUI负责 ExpressionEvaluator实现实例的创建。事实上,完全可以将相关逻辑从其中剥离出去,毕竟对于 CalculatorUI而言,它并不关心如何创建或者由谁去创建实例对象,其更关注的是如何使用 ExpressionEvaluator的功能。常见的解决方案之一是定义一个全局的注册表,服务的提供者和使用者分别在其中发布和获取相关服务。

    清单 4. 计算器实现方式四 ( 注册表组件 )
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class CalculatorUI {
     
        private ExpressionEvaluator expressionEvaluator;
     
        public CalculatorUI() {
            expressionEvaluator = (ExpressionEvaluator)
                GlobalRegistry.getService("SimpleExpressionEvaluator");
        }
     
        public String evaluate(String expression) {
            if (expression == null || expression.isEmpty()) {
                throw new IllegalArgumentException("[" + expression
                    + "] is not a valid expression");
            }
            return expressionEvaluator.evaluate(expression);
        }
     
    }

    在 清单 4 中,CalculatorUI 不再关注通过何种方式创建 ExpressionEvaluator 实现实例,相对于前者前通过工厂模式创建实例,现在则从全局注册表中获取。通过引入 GlobalRegistry 之后,CalculatorUI 和 ExpressionEvaluator 实现类之间达到了更大程度上的解耦,两者完全通过注册表实现了相互关联。在 Java 相关的诸多技术中,常常可以看到此实现方式的身影。例如 JavaEE 的目录服务也是类似的实现方式,联想到在应用程序中,我们通过标准的 JNDI 接口去访问后台的目录服务,从而获取 EJB,Web Service 等等的引用。同样,在 OSGI 中,各个 Bundle 通过 BundleContext注册和获取相关服务,其目的之一也是降低 Bundle 之间的耦合性。

    继续观察上述实现方式,无论是通过工厂模式,抑或是通过注册表组件,始终在 CalculatorUI 中包含如何获取 ExpressionEvaluator实现实例的逻辑,那么有无方法彻底将其从客户端代码中移除呢?答案之一便是对象注入技术。当前常见的实现策略是通过构造函数或者 JavaBean 的标准 setXXX 方法进行对象实例的注入。两种方式各有优缺点,一般认为使用后者会更具备灵活性。这里,将借助于 Spring 这个优秀的开源项目,展示第五种解决方案。

    清单 5. 计算器实现方式五 ( 对象注入 )
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    public class CalculatorUI {
     
        private ExpressionEvaluator expressionEvaluator;
     
        public CalculatorUI() {
        }
         
        public void setExpressionEvaluator(ExpressionEvaluator expressionEvaluator){
            this.expressionEvaluator = expressionEvaluator;
        }
        ......
     }
     
    Spring Configuration Fragment A (Use ExpressionEvaluatorImpl directly)
     
    <beans>
     
      <bean id="calculatorUI" class="CalculatorUI">
        <property name="expressionEvaluator">
            <ref bean="expressionEvaluator"/>
        </property>
      </bean>
     
      <bean id="expressionEvaluator" class="ExpressionEvaluatorImpl"/>
      
     </beans>
     
     Spring Configuration Fragment B (Use ExpressionEvaluatorFactory)
     
     <beans>
     
      <bean id="calculatorUI" class="CalculatorUI">
        <property name="expressionEvaluator">
            <ref bean="expressionEvaluator"/>
        </property>
      </bean>
     
      <bean id="expressionEvaluator" class="ExpressionEvaluatorFactory"
        factory-method="createExpressionEvaluator"/>
      
    </beans>

    在 清单 5 中,基于前一种解决方式做了如下修改:

    • 去除了原先在 CalculatorUI 构造函数中关于从 GlobalRegistry 中获取 ExpressionEvaluator 实现实例的初始化代码,而为其添加了 setExpressionEvaluator 方法以实现对 ExpressionEvaluator 的设置。
    • 使用 Spring 的配置文件实现运行时的注入,<ref> 标签用于关联 ExpressionEvaluator 实例和 CalculatorUI实例,即运行时注入的实例对象。示例中的两种配置文件分别是直接使用 ExpressionEvaluatorImpl 和使用 ExpressionEvaluatorFactory,实际应用中可依据需要和项目约定使用任一种方式。

    在第五种解决方式中,依托于 Spring 项目,CalculatorUI 和 ExpressionEvaluator 得到了进一步的解耦。通过在 XML 配置中引用的设置,Spring 容器会在运行时将期望的 ExpressionEvaluator 实现的实例注入到 CalculatorUI 中去。至此,CalculatorUI 甚至不需关心如何获取 ExpressionEvaluator 实例对象。后续使用过程中,如果需要更换新的实现,需要的是修改 Spring 的 XML 配置文件,可以很方便地更新相关实现。

    另外,熟悉 Spring 的读者知道其独特的 AutoWire 的功能,简而言之,Spring 容器会按照一定的规则自动寻找适合的 Bean 实例对象进行注入操作,常见的规则包括通过名称,类型等匹配。适当的使用该功能,会使应用程序的耦合性更低,相对而言也更加灵活。

    在刚发布的 Java EE 6 平台包含的众多组件中,JSR-299 : Contexts and Dependency Injection for the JavaEE platform/JSR-330 : Dependency Injection for Java 是最受瞩目的技术之一 。究其原因,其提供的依赖注入特性功不可没。在进一步介绍 JSR-330 注入技术之前,首先看一下应用程序代码的变化。

    清单 6. 计算器实现方式六 (WebBeans)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import javax.inject.Inject;
    public class CalculatorUI {
     
        @Inject
        private ExpressionEvaluator expressionEvaluator;
     
        public CalculatorUI() {
        }
     
        public String evaluate(String expression) {
            if (expression == null || expression.isEmpty()) {
                throw new IllegalArgumentException("[" + expression +
                    "] is not a valid expression");
            }
            return expressionEvaluator.evaluate(expression);
        }
    }

    如 清单 6 所展示,CalculatorUI 类中出现了一个 Inject 标识,在 JSR-299 中使用其标识注入点,即容器会在运行时对其执行注入操作。该标识可以标注在类成员变量,构造函数以及普通函数上。那么 Web Beans 容器是如何知道将哪个实例注入?其是通过如下两个标准检索对应的实现:

    • 实现类必需具备注入参数所要求的匹配类型,以本例而言,实现类必需实现 ExpressionEvaluator接口。事实上针对不同类型的 Web Beans 之间规则略有差别,详细情况请参照 JSR-299 规范。
    • 实现类具备所有注入点所要求的 Qualifier。在 JSR-299 规范中,允许用户自定义匹配标识,容器在检索实现类时会校验注入点参数和实现类是否具有相同 Qualifer。在本例中,并未显示定义具体的 Qualifier,则默认为 Default 类型,因为我们的计算器应用只包含一个 ExpressionEvaluator接口的实现。如果有多个,则需要自定义 Qualifier 以确保唯一匹配。如 清单 7 所示:
    清单 7. 自定义 Qualifer
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    SimpleExpressionEvaluator.java
    @Qualifier
    @Target( { ElementType.TYPE, ElementType.METHOD,
        ElementType.PARAMETER, ElementType.FIELD })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface SimpleExpressionEvaluator {
    }
     
    CalculatorUI.java
    import javax.inject.Inject;
    public class CalculatorUI {
     
        @Inject
        @SimpleExpressionEvaluator
        private ExpressionEvaluator expressionEvaluator;
     
        public CalculatorUI() {
        }
     
        public String evaluate(String expression) {
            if (expression == null || expression.isEmpty()) {
                throw new IllegalArgumentException("[" + expression +
                    "] is not a valid expression");
            }
            return expressionEvaluator.evaluate(expression);
        }
    }
     
    ExpressionEvaluatorImpl.java@SimpleExpressionEvaluator
    public class ExpressionEvaluatorImpl implements ExpressionEvaluator {
     
        public String evaluate(String expression) {
            ......       
        }
    }

    与基于前面基于 Spring 的实现方式相比,两者均是使用对象注入技术,只是如何定义 Bean 对象之间的关联不一样,前者是使用 XML 配置文件,而后者则是通过标识。当然,Web Beans 的优势之一是类型安全。在使用 Spring 的 XML 配置时,不同 Bean 对象之间的注入是通过其 Id 实现的。例如在 清单 5所示的配置文件中,<ref> 元素用于指定目标注入实例对象。而从上述 WebBeans 的检索规则中可以确保注入操作的类型安全性,同时在部署过程中即可执行相关检测。

    总结

    本文中,我们以实现一个计算器应用为例,从对象的创建和引用的角度,列举了多种实现方式来诠释如何实现对象之间的解耦。应该说,上述的六种方式各有优缺点,没有哪种是最好的,实际使用时,可以根据实际场景的不同,而采取不同的解决方案。

    • 在简单的场景中,如果实现类本身就是一个内部类,或者从开发预期看,没有必要更换实现,抑或即使会有更换的需求,但是所涉及的范围可控,那么就可以直接使用 new 操作符创建对应的实现实例。
    • 同样,如果评估之后,确有可能在后期更换实现,并该实例在应用程序中多处会被使用到,便可以考虑使用工厂模式或者服务定位的方式,以降低后期因为更改实现而对程序产生影响的范围。
    • 如果应用本身即时基于 Spring 框架开发,或者是以 Bundle 的形式运行与 OSGI 环境中,又或者是运行于 WebBeans 容器之中,最后的几种方式便是当仁不让的选择。

    事实上,解耦是个相对的概念,并不存在完全程度的解耦。从上述各种解决方案可以看出,其目的都是转移了耦合点,从直接使用 new 操作符创建到将其移至工厂类中,进一步移至配置文件和系统属性,最终到 Java 类的标识。通过耦合点的转移,使得原先的对象之间耦合性得到了降低。而之所以可以去转移,原因是当发生实现更换时,新的耦合点更方便修改,并且影响范围更小。

    那么,究竟什么时候,我们可以大声宣称对象之间实现解耦了呢?通常而言,当发生实现更换时,达到如下之一的要求即可:

    • 不需要重现编译代码,而是通过修改配置文件或者系统属性即可达到目标。
    • 客户端的代码不需要修改并重新编译,例如只需要修改或者更新库文件即可。
  • 相关阅读:
    系统维护相关问题
    Python环境维护
    哈希表解决字符串问题
    论文笔记二:《A Tutoral on Spectral Clustering》
    论文笔记之哈希学习比较--《Supervised Hashing with Kernels》《Towards Optimal Binary Code Learning via Ordinal Embedding》《Top Rank Supervised Binary Coding for Visual Search》
    Java中String、StringBuffer、StringBuilder的比较与源 代码分析
    浙大pat1040 Longest Symmetric String(25 分)
    浙大pat1039 Course List for Student(25 分)
    浙大pat---1036 Boys vs Girls (25)
    百炼oj-4151:电影节
  • 原文地址:https://www.cnblogs.com/kira2will/p/11415968.html
Copyright © 2011-2022 走看看