GRASP:基于职责设计对象

1 分钟读完

参考书籍《UML和模式应用第三版》

GRASP是通用职责分配软件模式(General Responsibility Assignment Software Patterns)的缩写,之所以用这个名字,是为了表明掌握(grasping)这些原则对于成功设计面向对象软件非常重要。

对象技术初学者在编码或绘制交互图和类图时应该理解并应用GRASP的基本思想,以便尽快掌握这些基本原则,他们是设计OO系统的基础。

GRASP共有9个模式:

  1. 创建者(Creator)
  2. 信息专家(Information Expert)
  3. 低耦合(Low Coupling)
  4. 控制器(Controller)
  5. 高内聚(High Cohesion)
  6. 多态性(Polymorphism)
  7. 纯虚构(Pure Fabrication)
  8. 间接性(Indirection)
  9. 防止变异(Protected Variations)

创建者(Creator)

问题:谁应该负责创建某类的新实例?

创建对象是面向对象系统中最常见的活动之一,因此,应该有一些通用的原则以用于创建职责的分配,如果分配的好,设计就能够支持低耦合,提高清晰度、封装性和可复用性。

答案

如果一下条件之一(越多越好)为真时,将创建类A实例的职责分配给类B:

  • B“包含”或组成聚集A
  • B记录A
  • B直接使用A
  • B具有A的初始化数据,并且在创建A时会将这些数据传递给A。因此对于A的创建而言,B是专家。

此时,B是对象A的创建者。如果有一个以上的选项适用,通常首选聚集或包含A的类B。

示例

在NextGen POS应用中,谁应当负责创建SalesLineItem实例?按照创建者模式,我们应当寻找聚合、包含SalesLineItem实例的类。参考下图所示的部分领域模型:

部分领域模型

图1:部分领域模型

因为Sale包含(实际上是聚集)许多SalesLineItem对象,所以根据创建者模式,Sale是具有创建SalesLineItem实例职责的良好候选者。这样便产生了如下图的对象交互设计图:

创建SalesLineItem

图2:创建SalesLineItem

这项职责的分配要求在Sale中定义MakeLineItem方法。再次强调,在绘制交互图时考虑和决定这些职责的分配。然后在类图的方法栏中概括职责分配结果,方法是职责的具体实现

禁忌

对象的创建通常具有相当的复杂性,例如为了性能而使用回收实例,基于某些外部特性值有条件地创建一个或一族类的实例,等等。在这些情况下,最好的方法是把创建职责委派给称为具体工厂(Concrete Factory)或抽象工厂(Abstract Factory)的辅助类,而不是使用创建者模式所建议的类。

优点

  • 支持低耦合,这意味着它具有较低的维护依赖性和较高的复用性。

相关模式和原则

  • 低耦合
  • 具体工厂和抽象工厂
  • 整体一部分描述定义了聚合对象的模式,它支持对构件的封装。

信息专家(Information Expert)

问题:给对象分配职责的基本原则是什么?

一个设计模型也许要定义数百或数千个类,一个应用程序也许要实现数百或数千个职责。在对象设计中,当定义好对象之间的交互后,我们就可以对软件类的职责分配做出选择。如果选择的好,系统就会易于理解、维护和扩展,而我们的选择也能为未来的应用提供更多复用构件的机会。

解决方案

把职责分配给信息专家,它具有实现这个职责所必需的信息。

示例

在NexTGen POS应用中,某个类需要知道销售的总额。

分配职责应当从清晰地描述职责开始

根据上述建议,对职责的描述是:谁应当负责了解销售的总额?

按照“信息专家(Information Expert)”的建议,我们应当寻找具有确定总额所需信息的那个对象类。

现在,关键的问题是,我们需要查看领域模型或设计模型来分析具有所需信息的类?领域模型描述的是真实世界领域内的概念类,设计模型描述的是软件类。

答案:

  1. 如果在设计模型中存在相关的类,首先查看设计模型。
  2. 否则查看领域模型,并尝试利用(或扩充)它的表示,以激发相应设计类的创建。

例如,假设我们刚刚开始设计工作,并且没有(或只有规模很小的)设计模型。因此我们希望在领域模型里寻找信息专家,也许这个信息专家就是真实世界的Sale。然后,我们在设计模型中加入同样称为Sale的软件类,把获取总额的职责分配给Sale类,并以名为getTotal的方法来表示。这种方法支持低表示差异,使对象的软件设计与我们的真实领域的组织方式更加接近。

为了详细阐述这个例子,我们参考下图的部分领域模型。

部分领域模型

图3:部分领域模型

确定总额需要哪些信息?我们应该知道销售的所有SalesLineItemm实例及其小计之和。Sale实例包含了上述信息。按照信息专家建议的准则,Sale是适合这一职责的对象类,它是适合这项工作的信息专家。

如上所述,在创建交互图的语境下,会经常出现这种职责问题。为了给对象分配职责,假设我们通过绘图来开始工作。下图的部分交互图和类图说明了某些决策。

部分交互图和类图

图4:部分交互图和类图

目前这些图还没有完成。为了确定商品的小计,我们需要哪些信息呢?答案是SalesLineItem.quantity和ProductDescription.price。SalesLineItem知道其数量和与其关联的ProductDescription。因此,根据专家模式,应该由SalesLineItem确定小计,它就是信息专家。

根据交互图,这意味着Sale应当向每个SalesLineItem发送getSubTotal消息,并对其得到的结果求和,如下图所示:

计算Sale的总额

图5:计算Sale的总额

为了实现获知并回答小计的职责,SalesLineItem必须知道产品的价格。

ProductDescription是回答价格的信息专家,因此SalseLineItem向它发送询问产品价格的消息,如下图所示:

计算Sale的总额

图6:计算Sale的总额

综上,为了实现获知和回答销售总额的职责,我们给三个对象的设计类分配了三个职责,如下表:

设计类 职责
Sale 知道销售的总额
SalesLineItem 知道商品的小计
ProductDescription 知道产品的价格

表1:设计类的职责

我们在绘制交互图时应该考虑并决定这些职责。然后我们可以在类图的方法分栏中总结响应的方法。

我们分配每个职责的原则是信息专家,即把职责分配给具有完成此职责所需信息的对象。

禁忌

在某些情况下,信息专家模式建议的方案也许并不合适,通常这是由于耦合与内聚问题所产生的。

请考虑将Sale存入数据库的职责应该分配给谁。按照信息专家模式,当然应该分给Sale,可以这样做的话,基本每个类都要处理数据库操作逻辑了,这行就降低了类的内聚,而且也提高了跟数据库操作服务的耦合,同时会导致在大量持久类中重复出现类似的数据库逻辑。

所有这些问题都表明这种做法违反了基本架构原则,即设计要分离主要的系统关注。将应用逻辑置于一处、数据库逻辑置于另一处(如单独的持久性服务子系统)等,而不是在同一构件中把不同的关注关注起来。

优点

  • 因为对象使用自身信息来完成任务,所以信息的封装性得以维持。这样就支持了低耦合,进而形成更为健壮的、可维护的系统。低耦合也是一种GRASP模式。
  • 行为分布在那些具有所需信息的类之间,因此提倡定义内聚性更强的“轻量级”的类,这样易于理解和维护。该模式通常支持高内聚。

相关模式和原则

  • 低耦合
  • 高内聚

也成为;类似于

“把职责与数据置于一处”,“知其责,行其事”,“DIY”,“把服务与其属性置于一处”

##低耦合##

问题:怎样降低依赖性,减少变化带来的影响,提高重用性?

“耦合(Coupling)”是对某元素与其他元素之间的连接、感知和依赖程度的度量。具有低(或弱)耦合的元素不会过度依赖于其他元素;“过度”是与语境相关的,但我们必须对此进行检查。这些元素包括类、子系统、系统等。

具有高(或强)耦合的类依赖于许多其他的类,这样的类或许不是我们所需要的。有些类会遇到以下问题:

  • 由于相关类的变化而导致本体的被迫变化
  • 难以单独地理解
  • 由于使用高耦合类时需要它所依赖的类,因此很难重用。

解决方案

分配职责,使耦合性尽可能低。利用这一原则来评估可选方案。

示例

考虑如下NextGen案例研究的局部类实现:

假设我们需要创建Payment实例并使它与Sale关联,哪个类应该复杂此事呢?因为在真实世界领域中,Register记录了Palyment,所以创建者模式建议将Register作为创建Payment的候选者。Register实例会把addPayment消息发给Sale,并把新的Payment作为参数传递给它。如下图所示:

图7:Register创建Payment

这种职责分配使Register类和Payment类之间产生了耦合,即Register类要知道Payment类。

下图是另外一种方案:

图8:Sale创建Payment

根据职责分配,哪个设计支持低耦合?在这两个例子中,我们都假设Sale最终都必须耦合于Payment。在第一个设计方案中,Register创建Payment,在Register和Payment之间增加耦合;在第二个设计方案中,Sale负责创建Payment,其中没有增加耦合。如果单独地从耦合的角度来看,第二个方案是首先,因为保持了总体上的低耦合。这个例子说明两个不同的模式(低耦合和创建者)为何会导致不同的方案。

在实践中,耦合程度不能脱离专家、高内聚等其它原则孤立地考虑。不过,它的确是改进设计所要考虑的因素之一。

讨论

低耦合并不是鼓励类之间没有耦合,因为软件的任务仍然是通过被连接的对象之间的协作完成的。

禁忌

高耦合对于稳定和普遍使用的元素而言并不是问题。例如,J2EE应用能安全地将自己与Java库耦合,因为Java库是稳定、普遍使用的。

权衡

必须在降低耦合和封装事物之间做出选择

优点

  • 不受其它构件变化的影响
  • 易于单独理解
  • 便于复用

背景

耦合和内聚是设计中真正的基本原则,应当受到重视,并为所有软件开发者所应用。

相关模式

  • 防止变异

##控制器##

问题:在UI层之上首先接收和协调(控制)系统操作的第一个对象是什么?

“控制器(Controller)”是UI层之上的第一个对象,它负责接收和处理系统操作消息。

解决方案

把职责分配给能代表一下选择之一的类:

  • 代表整个“系统”、“根对象”、运行软件的设备或主要子系统,这些是外观控制器的所有变体。
  • 代表用例场景,在该场景中发生系统事件,通常命名为<UseCaseName>Handler<UseCaseName>Coordinator<UseCaseName>Session
  • 对于同一用例场景的所有系统事件使用相同的控制器类。
  • 通俗地说,会话是参与者进行交谈的实例。会话可以具有任意长度,但通常按用例来组织。

推论:注意“窗口(Window)”、“视图(View)”、或“文档(Document)”类不在此列表内。这些类不应该完成与系统事件相关的任务。通常情况下,他们接收这些事件,将其委派给控制器。

讨论

  • UI层不应当包含应用逻辑,UI层对象必须把工作请求委派给其他层。当“其他层”是领域层时,控制器模式总结了常见选择,作为一个OO开发者,需要选择作为代表来接收工作请求的领域对象。
  • 控制器设计常见的缺陷是分配的职责过多,这时,控制器会具有不良(低)内聚,从而违反了高内聚原则。
  • 正常情况下,控制器应当把需要完成的工作委派给其他的对象。控制器只是协调或控制这些活动,本身并不完成大量工作。
  • 控制器分为外观控制器和用例控制器。
  • 用例控制器不是领域对象,是支持系统的人工构造物。当把职责分配给外观控制器会导致低内聚或高耦合的设计时,通常是外观控制器的职责过多而变得“臃肿”时,就需要考虑使用用例控制器。
  • 控制器模式的重要结果是,UI对象(窗口或按钮对象)和UI层不应具有实现事件的职责。系统操作应当在对象的应用逻辑层或领域层进行处理。

GRASP控制器和Web-MVC控制器的不同

Web-MVC控制器是UI层的一部分,并且控制UI层的交互页面流。GRASP控制器是领域层的一部分,它控制或协调工作请求的处理,根本不需要知道所用的UI技术是什么。

相关模式

  • 命令(Command)
  • 外观(Facade)
  • 层(Layer),POSA模式。
  • 纯虚构(Pure Fabrication),GRASP模式。

高内聚

问题:怎样保持对象是有重点的、可理解的、可管理的,并且能够支持低耦合?

从对象设计的角度上说,“内聚”(或更为专业地说,是功能内聚)是对元素职责的相关性和集中度的度量。如果元素具有高度相关的职责,而且没有过多工作,那么该元素具有高内聚性。

解决方案

分配职责可保持较高的内聚性。

内聚性较低的类要做许多互不相关的工作,需要完成大量工作。这样的类是不合理的,它们会导致以下问题:

  • 难以理解
  • 难以复用
  • 难以维护
  • 脆弱,经常会受到变化的影响

内聚性低的类通常表示了大粒度的抽象,或承担了本应委托给其它对象的职责。

禁忌

在少数情况下,可以接受较低内聚。

优点

  • 能够更加轻松、清楚地理解设计
  • 简化了维护和改进工作
  • 通常支持低耦合
  • 由于内聚的类可以用于某个特定的目的,因此细粒度、相关性强的功能的重用性增强。

多态

问题:如何处理基于类型的选择?如何创建可插拔的软件构件?

基于类型的选择——条件变化是程序的一个基本主题。如果使用if-then或case语句的条件逻辑来设计程序,那么当出现新的变化时,则需要修改这些case逻辑——通常遍布各处。这种方法很难方便地扩展有新变化的程序,因为可能需要修改程序的多个地方——任何存在条件逻辑的地方。

可插拔软件构件——客户-服务器关系中的可视化插件2,如何才能够替换服务器构件,而不对客户端产生影响呢?

解决方案

当相关选择或行为随类型(类)有所不同时,使用多态操作为变化的行为类型分配职责。

推论:不要测试对象的类型,也不要使用条件逻辑来执行基于类型的不同选择。

准则

  • 除非在超类中具有默认的行为,否则将超类的多态方法声明为{abstract}
  • 合适使用接口进行设计?当你需要支持多态但是又不想约束特定的类层次结构时,可以使用接口。

讨论

当对象A持续需要对象B中的数据时,意味着:

  1. 对象A不应该持有该数据
  2. 对象B而不是对象A应该具有这一职责(基于专家模式)

禁忌

有时,开发者会针对某些未知的可能性变化进行“未来验证”的推测,由此而使用接口和多态来设计系统。如果这种变化是基于立即或十分可能变化的原因而明确存在的,那么通过多态来增加灵活性一定是合理的。

优点

  • 易于增加新变化所需要的扩展
  • 无需影响客户便能引入新的实现

相关模式

  • 防止变异
  • 适配器(Adapter)、命令(Command)、组合(Composite)、代理(Proxy)、状态(State)和策略(Strategy)模式。

别称和类似模式

选择消息,不要询问什么类型。

##纯虚构##

##间接性##

##防止变异##