欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

谈谈分析模型的那些事儿 之 职责驱动设计

程序员文章站 2022-03-02 19:32:25
...

前面讲了为什么我们要使用分析模型,现在我们看设计分析模型的基本原则。

分配职责和职责驱动设计

我们在开始分析模型的时候,首先要弄清楚一个非常重要的原则,就是以职责为中心。OO分析设计的核心原则之一,就是软件系统中的所有元素都必须具有高度相关的职责,也就是说,软件系统中所有的模块、包、对象类,都应当拥有一个清晰的职责,并且与它相关的所有元素(即模块中的所有包、包中的所有对象类、对象类中的所有属性和行为)都必须与这个职责具有高度的相关性。因此,分析模型的首要设计原则就是职责驱动设计(Responsibility-Drive Design)。依据职责驱动设计的原则,我们在分析系统的时候,必须为每一个模块、包、对象类,特别是对象类,定义一个明确的职责。在这个原则下,我们最核心的问题是,我们在定义每一个对象类时都应当为其定义职责,在定义它的行为和属性时,都应当与其职责高度相关。

如何进行职责驱动设计呢?大师Craig Larman在他的经典著作《UML和模式应用》中,提出了GRASP软件设计模式。GRASPGeneral Responsibility Assignment Software Patterns),翻译过来就是通用职责分配设计模式,它包括创建者(Creator)、控制器(Controller)、信息专家(Information Expert)、低耦合(Low Coupling)、高内聚(High Cohesion)、多态(Polymorphism)、纯虚构(Pure Fabrication)、间接性(Indirection)和防止变异(Protected Variations)九部分。GRASP这九部分,与其说是模式,不如说是原则更加准确。现在跟大家探讨一下低耦合、高内聚、信息专家和创建者四个原则。

1.低耦合与高内聚

这两个词可能是你早已耳熟能详的了吧,我们在看spring的书籍、MVC的数据、设计模式的书籍,无处不提到“低耦合、高内聚”,它已经成为软件设计质量的重要标准之一。那么,什么是低耦合,什么又是高内聚呢?

耦合就是对某元素与其它元素之间的连接、感知和依赖的量度。这里所说的元素,即可以是功能、对象类,也可以指系统、子系统、模块。假如一个元素X去连接元素Y,或者通过自己的方法可以感知Y,或者当Y不存在的时候就不能正常工作,那么就说元素X与元素Y耦合。那么,哪些是耦合呢?

1.元素Y是元素X的属性,或者元素X引用了元素Y的实例(这包括元素X调用的某个方法,其参数中包含元素Y)。

2.元素X调用了元素Y的方法。

3.元素X直接或间接成为元素Y的子类。

4.元素X是接口Y的实现。

耦合带来的问题是,当元素Y发生变更或不存在时,都将影响元素X的正常工作,影响系统的可维护性和易变更性。同时元素X只能工作于元素Y存在的环境中,这也降低了元素X的可复用性。正因为耦合的种种弊端,我们在软件设计的时候努力追求“低耦合”。低耦合就是要求在我们的软件系统中,某元素不要过度依赖于其它元素。请注意这里的“过度”二字。系统中低耦合不能过度,比如说我们设计一个类可以不与JDK耦合,这可能吗?除非你不是设计的Java程序。再比如我设计了一个类,它不与我的系统中的任何类发生耦合。如果有这样一个类,那么它必然是低内聚。耦合与内聚常常是一个矛盾的两个方面,内聚要求类与类之间出现耦合,过低的耦合必然造成过低的内聚。最佳的方案就是寻找一个合适的中间点。那么,什么是内聚呢?

内聚,更为专业的说法叫功能内聚,是对软件系统中元素职责相关性和集中度的度量。如果元素具有高度相关的职责,除了职责范围内的任务,没有其它过多的工作,那么该元素就具有高内聚性,反之则为低内聚性。高内聚要求软件系统中的各个元素具有较高的协作性,因为在我们在完成软件需求中的一个功能,可能需要做各种事情,但是具有高内聚性的一个元素,只完成它职责范围内的事情,而把那些不在它职责范围内的事情拿去请求别人来完成。这就好像,如果我是一个项目经理,我的职责是监控和协调我的项目各个阶段的工作。当我的项目进入需求分析阶段,我会请求需求分析员来完成;当我的项目进入开发阶段,我会请求软件开发人员来完成……如果在开发阶段,我做了开发工作,我就不是一个高内聚的元素,因为开发工作不是我的职责。

高内聚从本质上提高了软件的可读性、可复用性和可维护性,因此,高内聚已经成为软件设计中的一种基本品质。但是,高内聚从内在要求软件中的所有元素必须充分协作,从而必然造成元素之间的相互感知,也就是相互耦合。高内聚与低耦合成为了一对矛盾,就要求我们在设计过程中必须寻找那个最佳的中间点,既满足高内聚,又能很好的低耦合。

2.信息专家

前面我们说了,我们分析和设计系统的基本原则是职责驱动设计,那么职责分配的原则是什么呢?信息专家模式回答了我们。信息专家模式(又称为专家模式)告诉我们,在OO分析中,应当将职责分配给软件系统中的这样一个对象类,它拥有实现这个职责所必须的信息。我们称这个对象类叫“信息专家”。

在一个软件系统中有许多的功能,每个功能是由无数的行为共同协作完成的。而职责是一组高度相关的行为的集合。我们把软件系统中一组组高度相关的行为归集为一个个的职责,并且将每个职责分配给这个职责相应的信息专家,既可以实现高内聚,又可以实现低耦合。为什么这样说呢?将一组高度相关的行为分配给一个对象类,其本身就是高内聚。而为了完成某个职责,必须访问那些实现这个职责所需的信息。如果这个职责没有分配给信息专家,势必造成因访问这些信息时产生的耦合;反之,如果将这个职责分配给信息专家,这种访问信息而产生的耦合就随之消失,从而降低了系统的耦合度。

信息专家所表达的道理虽然浅显,但在我们的项目中,没有遵照信息专家模式而出现的糟糕设计随处可见,拿一个我经历过的实例来说吧。我曾经参与开发过一个公司内部评审系统。这个系统分为三个用例:制订评审计划、执行评审(执行评审在软件中的表现就是填写评审表)、制作评审报告。在制订评审计划时,需要详细填写评审的内容、参与的评委。在执行评审的时候,每个评委需要为所有评审的内容,在各自的评审表中填写评审意见。根据以上的需求,为了实现“执行评审”这个用例,我们势必要做“读取评审内容”和“读取所有评委”这两个行为。现在的问题是,这两个行为应当分配给谁?稍加分析我们可以发现,评审内容和评委是评审计划的一部分,因此“读取评审内容”和“读取所有评委”都是“读取评审计划”这个职责的所在范围。按照信息专家模式的要求,“评审计划”类拥有评审计划的所有信息,因此应当将“读取评审计划”这个职责分配给“评审计划”类,即“读取评审内容”和“读取所有评委”都是“评审计划”类的行为。然而,现实却不是这样。

制订评审计划、执行评审和制造评审报告被分配给了三个程序员。完成“执行评审”任务的程序员甲,为了完成他的功能必须执行“读取评审内容”和“读取所有评委”的行为,他应当要求完成“制订评审计划”的程序员乙来实现这两个行为,并且为程序员甲调用。但是,程序员乙认为自己已经很忙了,为什么要答应程序员甲的要求呢?正因为如此,程序员乙没有在“评审计划”类中实现“读取评审内容”和“读取所有评委”的行为,而程序员甲只能在“评审表”中实现这两个行为,也就是按照事先约定好的逻辑,直接读取数据库。当程序员乙因为某个业务变更修改了“评审计划”的业务逻辑或表结构时,直接导致的就是“执行评审”的功能无法执行。其根本原因就是“评审表”类执行了与它职责无关的读取评审计划中相关信息的工作,从而造成与“评审计划”的业务耦合。这样的问题在软件开发项目中非常常见,解决的办法应当是,程序员开始编程前,应当有个分析员对整个系统进行规划。有了分析员的规划,自然会将“读取评审内容”和“读取所有评委”的行为分配给“评审计划”。

3.谁来创建我

当我们分析清楚客户需求设计出用例模型以后,当我们分析清楚客户的业务环境制作出领域模型以后,当我们综合用例模型、领域模型和我们的聪明才智设计出一个又一个的类和它们各自的方法以后,当就在一切都准备就绪只欠东风的关键时刻,一个对象发出了撕心裂肺的怒吼——谁来创建我?!!!一个对象,不管拥有多么强大的功能,不管进行了多么精巧的设计,如果不能被创建,就如同韩信不能做将军,孙膑不能当军师,勾践不能回越国,刘备不能得荆州,一切一切的雄才武略都如废纸一张。既然创建对于对象如此重要,我们就来好好探讨一下GRASP中关于对象创建的问题。

创建对象是面向对象系统中常见的活动之一,然而创建往往伴随着耦合。我们应当追求低耦合,但是又必须要创建对象。怎么办呢?最佳的办法就是让那些必须与创建的对象耦合的类,去完成创建的工作。创建者模式为我们提出了以下建议:

如果要将创建A的职责分配给B,那么应当满足以下条件(越多越好):

  l B包含或聚合A

  l B记录A

  l B调用A

  l B拥有A的初始化数据,并且在创建A的时候要传递给A,即BA的专家。

如果有多个类满足以上条件,首选满足条件12的类。从以上条件可以看出,即使B没有创建AB已经与A耦合了,所以B创建了A,也不会提高系统的耦合度,因此是最佳的选择。

但是,在软件系统中还有许多的例外情况不按照创建者模式创建对象。当创建一个对象,或者与它相关的整个聚合,其业务逻辑变得非常复杂时,为了不过多地暴露其内部结构,保证系统的封装性,可能选择工厂模式创建对象。在这种情况下,可能涉及到创建对象的复杂组装过程(这个问题还会在后面继续讨论)。另一种情况是,系统中的服务类对象,为了提高系统运行效率而采用单例的方式,为其它对象提供服务。像这样的对象类,我们往往采用,在系统启动,或第一次有对象访问它们时,由系统对其进行创建,例如spring框架中的那些bean