我偶然在google或yahoo这样的搜索引擎搜索GRASP发现,除了国外的网站,国内网站多介绍和讨论GoF而很少介绍GRASP,即使这少量的文章也讲解非常粗略。个人认为作为优秀的开发人员,理解GRASP比GoF更重要,故写此文章。前面我在 《 ( 原创)一个优秀软件开发人员的必修课: GRASP 软件开发模式浅析 》 中介绍了使用GRASP的目的,今天允许我调换一下顺序,先从低耦合讲起,因为诸如创建者模式、信息专家模式的根本目的就是降低耦合。
1. 低耦合( Low Coupling )
“低耦合”这个词相信大家已经耳熟能详,我们在看 spring 的书籍、 MVC 的数据、设计模式的书籍,无处不提到“低耦合、高内聚”,它已经成为软件设计质量的标准之一。那么什么是低耦合?耦合就是对某元素与其它元素之间的连接、感知和依赖的量度。这里所说的元素,即可以是功能、对象(类),也可以指系统、子系统、模块。假如一个元素 A 去连接元素 B ,或者通过自己的方法可以感知 B ,或者当 B 不存在的时候就不能正常工作,那么就说元素 A 与元素 B 耦合。耦合带来的问题是,当元素 B 发生变更或不存在时,都将影响元素 A 的正常工作,影响系统的可维护性和易变更性。同时元素 A 只能工作于元素 B 存在的环境中,这也降低了元素 A 的可复用性。正因为耦合的种种弊端,我们在软件设计的时候努力追求“低耦合”。低耦合就是要求在我们的软件系统中,某元素不要过度依赖于其它元素。请注意这里的“过度”二字。系统中低耦合不能过度,比如说我们设计一个类可以不与 JDK 耦合,这可能吗?除非你不是设计的 Java 程序。再比如我设计了一个类,它不与我的系统中的任何类发生耦合。如果有这样一个类,那么它必然是低内聚(关于内聚的问题我随后讨论)。耦合与内聚常常是一个矛盾的两个方面。最佳的方案就是寻找一个合适的中间点。
哪些是耦合呢?
1 .元素 B 是元素 A 的属性,或者元素 A 引用了元素 B 的实例(这包括元素 A 调用的某个方法,其参数中包含元素 B )。
2 .元素 A 调用了元素 B 的方法。
3 .元素 A 直接或间接成为元素 B 的子类。
4 .元素 A 是接口 B 的实现。
幸运的是,目前已经有大量的框架帮助我们降低我们系统的耦合度。比如,使用 struts 我们可以应用 MVC 模型,使页面展现与业务逻辑分离,做到了页面展现与业务逻辑的低耦合。当我们的页面展现需要变更时,我们只需要修改我们的页面,而不影响我们的业务逻辑;同样,我们的业务逻辑需要变更的时候,我们只需要修改我们的 java 程序,与我们的页面无关。使用 spring 我们运用 IoC (反向控制),降低了业务逻辑中各个类的相互依赖。假如类 A 因为需要功能 F 而调用类 B ,在通常的情况下类 A 需要引用类 B ,因而类 A 就依赖于类 B 了,也就是说当类 B 不存在的时候类 A 就无法使用了。使用了 IoC ,类 A 调用的仅仅是实现了功能 F 的接口的某个类,这个类可能是类 B ,也可能是另一个类 C ,由 spring 的配置文件来决定。这样,类 A 就不再依赖于类 B 了,耦合度降低,重用性提高了。使用 hibernate 则是使我们的业务逻辑与数据持久化分离,也就是与将数据存储到数据库的操作分离。我们在业务逻辑中只需要将数据放到值对象中,然后交给 hibernate ,或者从 hibernate 那里得到值对象。至于用 Oracle 、 MySQL 还是 SQL Server ,如何执行的操作,与我无关。
但是,作为优秀的开发人员,仅仅依靠框架提供的降低软件耦合的方法是远远不够的。根据我的经验,以下一些问题我们应当引起注意:
1) 根据可能的变化设计软件
我们采用职责驱动设计,设计中尽力做到“低耦合、高内聚”的一个非常重要的前提是,我们的软件是在不断变化的。如果没有变化我们当然就不用这么费劲了;但是如果有变化,我们希望通过以上的设计,使我们在适应或者更改这样的变化的时候,付出更小的代价。这里提供了一个非常重要的信息是,我们努力降低耦合的是那些可能发生变更的地方,因为降低耦合是有代价的,是以增加资源耗费和代码复杂度为代价的。如果系统中某些元素不太可能变更,或者降低耦合所付出的代价太大,我们当然就应当选择耦合。有一次我试图将我的表现层不依赖于 struts ,但发现这样的尝试代价太大而失去意义了。对于软件可能变更的部分,我们应当努力去降低耦合,这就给我们提出一个要求是,在软件设计的时候可以预判日后的变化。根据以往的经验我认为,一个软件的业务逻辑和采用的技术框架往往是容易变化的 2 个方面。客户需求变更是我们软件设计必须考虑的问题。在 RUP 的开发过程中,为什么需要将分析设计的过程分为分析模型和设计模型,愚以为,从分析模型到设计模型的过程实际上是系统从满足直接的客户需求到优化系统结构、适应可预见的客户需求变更的一个过程。这种客户需求的变更不仅仅指对一个客户需求的变更,更是指我们的软件从适应一个客户需求到适应更多客户需求的过程。另一个方面,现在技术变更之快, EJB 、 hibernate 、 spring 、 ajax ,一个一个的技术像走马灯一样从我们脑海中滑过,我们真不知道明天我在用什么。在这样的情况下,适应变化就是我们最佳的选择。
2) 合理的职责划分
合理的职责划分,让系统中的对象各司其职,不仅是提高内聚的要求,同时也可以有效地降低耦合。比如评审计划 BUS 、评审表 BUS 、评审报告 BUS 都需要通过评审计划 DAO 去查询一些评审计划的数据,如果它们都去直接调用评审计划 DAO (如图 A ),则评审计划 BUS 、评审表 BUS 、评审报告 BUS 三个对象都与评审计划 DAO 耦合,评审计划 DAO 一旦变更将与这三个对象都有关。在这个实例中,实际上评审计划 BUS 是信息专家(关于信息专家模式我将在后面讨论),评审表 BUS 和评审报告 BUS 如果需要获得评审计划的数据,应当向评审计划 BUS 提出需求,由评审计划 BUS 提供数据(如图 B )。经过这样的调整,系统的耦合度就降低了。
3) 使用接口而不是继承
通过对耦合的分析,我们不难发现,继承就是一种耦合。如果子类 A 继承了父类 B ,不论是直接或间接的继承,子类 A 都必将依赖父类 B 。子类 A 必须使用在存在父类 B 的环境中,父类 B 不存在子类 A 就不能使用,这样将影响子类 A 的可移植性。一旦父类 B 发生任何变更,更改或去掉一个函数名,或者改变一个函数的参数,都将导致子类 A 不得不变更,甚至重写。假如父类 B 的子类数十上百个,甚至贯穿这个项目各个模块,这样的变更是灾难性的。这种情况最典型的例子是我们现在使用 hibernate 和 spring 设计 DAO 对象的方式,具体的描述参见我写的 《如何在 struts + spring + hibernate 的框架下构建低耦合高内聚的软件结构》 一文。
总之,“低耦合”给软件项目带来的优点是:易于变更、易于重用。