转自:http://www.ibm.com/developerworks/cn/java/j-lo-decoupling/

解耦是软件设计领域中一个永恒不变的话题,在软件设计过程中,为了最大程度降低各个应用组件之间的耦合性,以提高其可维护性和可复用性,出现了诸多设计原则和解决方案。例如面向接口编程,开 - 闭原则,依赖倒转原则等,另外更出现一系列设计模式。同时,由于如何实现解耦涉及面相当广,大至组件的划分和关联,小至对象的创建和引用,往往使软件开发人员感到迷惑。本文将从对象创建和引用的角度出发,介绍常见的一些解决方案,并比较之间的差别,期望读者能从一个侧面加深对解耦概念的理解。

应用场景

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

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

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

清单 1. 计算器实现方式一 (new 操作符 )
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. 计算器实现方式二 ( 工厂模式 )
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 之间的直接耦合,对其而言,它只需要调用 ExpressionEvaluatorFactorycreateExpressionEvaluator方法即可。使用方式二的计算器应用程序如果需要更换 ExpressionEvaluator实现,无需修改 CalculatorUI的代码,只要改动 ExpressionEvaluatorFactory 工厂类的代码。正如通常所说的,客户端代码不需要修改了,而且当应用程序多处使用ExpressionEvaluatorFactory 时,其优势更加明显。

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

清单 3. 计算器实现方式三 (Service Look-up)
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. 计算器实现方式四 ( 注册表组件 )
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. 计算器实现方式五 ( 对象注入 )
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)
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
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 @SimpleExpressionEvaluatorprivate 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 类的标识。通过耦合点的转移,使得原先的对象之间耦合性得到了降低。而之所以可以去转移,原因是当发生实现更换时,新的耦合点更方便修改,并且影响范围更小。

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

  • 不需要重现编译代码,而是通过修改配置文件或者系统属性即可达到目标。
  • 客户端的代码不需要修改并重新编译,例如只需要修改或者更新库文件即可。

转载于:https://www.cnblogs.com/gossip/p/3808249.html

从对象创建和引用小议解耦相关推荐

  1. java对象创建与引用变量的详解

    创建对象与引用变量 创建对象 基本类型变量和引用类型变量的区别 存储的值 赋值 引用数据和NULL 创建对象 ClassName objectRefVar = new ClassName(); 这条语 ...

  2. tomcat中request对象是被创建的_Python中对象的创建与引用

    上文传送门,又见面向对象,不变的是思想,变的只是语言. 今日开始,我们深入来了解面向对象. 四.创建与引用 1.创建对象的流程 在创建对象时,看不见的手,帮我们做了三件事情,如下图: 1class S ...

  3. 干货收集者:为什么大家都说程序员必须要学习JVM?真的是这样吗?(内存区域、栈、堆、对象创建、Full GC 、引用)

    当然有必要.对于面试来说JVM知识是大厂必问的,你不会你就大概率被PASS,你说你学不学?对于平时工作来说,万一遇到内存异常的情况,你学过JVM有一定基础是不是对你查找问题更好呢?要注重实战,理论有时 ...

  4. Objective-C设计模式——生成器Builder(对象创建)

    生成器 生成器,也成为建造者模式,同样是创建对象时的设计模式.该模式下有一个Director(指挥者),客户端知道该类引用用来创建产品.还有一个Builder(建造者),建造者知道具体创建对象的细节. ...

  5. Python+OpenCV 图像处理系列(8)—— Numpy 对象及图像对象创建与赋值

    1. Numpy 相关知识 1.1 Ndarray 对象 在了解 OpenCV 的图像对象之前我们先对 NumPy 的基础知识做一回顾,方便我们后续更进一步理解图像对象的一系列操作. In [2]: ...

  6. (1)访问控制 (2)final关键字 (3)对象创建的过程 (4)多态

    1.访问控制(笔试题) 1.1 常用的访问控制符 public - 公有的 protected - 保护的 啥也不写 - 默认的 private - 私有的 1.2 访问控制符的比较 访问控制符 访问 ...

  7. OpenCV【零】—————cv::Mat——Mat对象创建方法

    OpenCV (一)--Mat对象创建方法 目录 OpenCV (一)--Mat对象创建方法 1. cv::Mat优点及原理(本质类) 2. Mat类拷贝及对象的创建方法 3. Mat 对象元素的高效 ...

  8. 【JVM】Java对象创建的流程步骤

    · 本文摘要 · 罗列Java创建对象的各种方式: · 讲解Java对象创建的流程步骤: 一.Java创建对象的各种方式 · 1. 用关键字new,老少皆知的方法:StringBuffer sb = ...

  9. java简述对象的组合_Java程序运行和对象创建过程简述

    Java中一个对象创建分为两个步骤: 加载类,创建对象. 加载类是将所写的程序.java文件编译生成的.class文件加载到内存中,保证了对象创建的预置环境.类加载完毕后才可以创建该类的对象. 第一步 ...

最新文章

  1. 六、MyBatis教程之六注解使用详解
  2. MySQL-MMM架构部署(有图)
  3. jzoj2941-贿赂【数学期望,dfs】
  4. WebService的基本概念:java webservice,什么是webservice
  5. Shiro框架原理及应用分析
  6. rman坏块的检测与恢复
  7. DrawerLayout和NavigationView的简单实用
  8. HDU2516 取石子游戏(斐波那契)
  9. win10锁定计算机命令,win10怎么锁定磁盘 锁住win10计算机磁盘的操作步骤
  10. Steam帐号被盗怎么办
  11. 老鸟程序员才知道的一些事
  12. Raspberry Pi 4 树莓派4 支持操作系统
  13. AUTOSAR OS和OSEK OS
  14. Java小白入门200例81之Java接口
  15. A* 流程+代码详细注释
  16. fw300r虚拟服务器设置,迅捷(Fast)FW300RM路由器怎么设置 | 192路由网
  17. Worksoft Certify学习之路
  18. android自动弹出浏览器打开文件,android使用主流浏览器打开网页,无需弹出选择。...
  19. Windows如何定制键盘按键
  20. java英文翻译_关于JAVA领域的外文翻译(适用于毕业论文外文翻译+中英文对照).doc...

热门文章

  1. 仿分词统计的MapReduce 程序。
  2. html5 教程网站
  3. 第三章:什么是组织结构
  4. 数据传递型情景下事件机制与消息机制的架构设计剖析(目录)
  5. Steps And Uses Of Product Costing
  6. 给Fedora11安装五笔
  7. 如何解决移动硬盘找不到的问题
  8. CV学习笔记-Alexnet
  9. 详细解读ORBSLAM中的描述子提取过程
  10. collectionutils包_CollectionUtils工具类的常用方法