GRASP设计原则
- 介绍
- 9种基本原则
- 创建者 Creator
-
- 信息专家 Information Expert
-
- 低耦合 Low Coupling
-
- 控制器 Controller
-
- 高内聚 High Cohesion
-
- 问题
- 解决办法
- 用法
- 衡量概念之间相关度的两个指标
- 内聚的最佳实践
- 类低内聚
- 多态 Polymorphism
-
- 纯虚构 Pure Fabrication
-
- 间接 Indirection
-
- 隔离变化
-
介绍
GRASP(General Responsibility Assignment Software Pattern)是通用职责分配软件设计模式,它可以帮助设计人员理解面向对象设计的本质,并以一种有条理的、理性的、可解释的方式来运用这些原则。由《UML和模式应用》(Applying UML and Patterns)一书作者Craig Larman提出。
在面向对象设计的过程中一般的通用方式是构思对象的职责、角色和协作。通常来说,我们在编码过程中先分析问题域,从中抽象出对象解决问题。简单的面向对象和优秀的面向对象设计的区别在于如何更合理的划分对象的角色,给对象赋予合理的职责以及对象之间的交互关系。
GRASP是对象职责分配的基本原则,其核心思想是职责分配,用职责设计对象。
9种基本原则
- 创建者(Creator)
- 信息专家(Information Expert)
- 低耦合(Low coupling)
- 控制器(Controller)
- 高内聚(High Cohesion)
- 多态性(Polymorphism)
- 纯虚构(Pure Fabrication)
- 间接性(Indirection)
- 隔离变化(Protected Variations)
这些模式都是针对软件开发上的一些问题进行解决。发明这些技巧不是为了要创造新的工作方式,而是为在面向对象设计上,对经过测试的程序设计方式创建文档并且标准化。
Craig Larman提到:“软件开发最关键的设计工具不是UML或其他的技术,是明了设计原则的心智。”。因此,GRASP原则是心理层面的工具集,是面向对象软件设计学习的辅助工具。
创建者 Creator
问题
谁负责创建类的实例?
解决方法
满足下面一个选项,则由B来创建A:
- B包含,聚集A
- B记录A
- B紧密使用 A
- B拥有A的初始化数据
若有一个以上的选项适用,则首选包含或聚集A的类。
注意:A和B都是软件对象,而不是领域对象。
何时不使用?
- 出于性能目的(缓存)而重用类的实例
- 有些情况下要基于某些外部属性值从一个相似类的家族创建一个实例(抽象工厂模式)
- 委托职责向下传递
- 其他复杂情况
好处
信息专家 Information Expert
问题
为一个对象分配职责的一般性原则是什么?
解决方法
若这个类拥有完成这个职责所需要的数据,则把这个职责分配给这个类。
信息
- 一个对象拥有的状态
- 和一个对象相关的其他对象
- 一个对象派生的信息
- 等等
怎么做
- 明确地表达职责
- 在设计模型中查看相关的类
- 在领域模型中查看并创建设计类
优点
封装性:对象充分利用自身的信息,低耦合
系统行为分布到不同的类:高内聚
低耦合 Low Coupling
耦合
- 耦合的定义
一个元素和另一个元素的连接,感知和依赖程度。
- 比较
内聚:模块内的元素之间紧密程度,如一个类内部操作之间。
耦合:两个子模块之间的紧密程度,如两个类之间的操作。
- 高耦合带来的问题
- X与Y存在耦合的情况,例如:
- X拥有Y的属性
- X调用Y的服务
- X有Y的成员变量,方法参数和局部变量
- X是Y的子类
- Y是接口,X实现接口
问题
如何保证设计方案支持低依赖性,低变化影响度和增加可重用性?
解决方法
分配职责后仍然保持低耦合。
原则
- 低耦合不具备可操作性,是一个评估原则,如:若两个方案都可以的,倾向于选择低耦合的方案。继承关系中,子类与父类的耦合非常紧密,如:能用组合的不要用继承。
- 类之间存在适当的耦合是必须的,正常的。因为只有这样才产生面向对象系统,任务才能通过对象间的相互协作来完成。
- 不能单独考虑低耦合,极端情况:类之间没有耦合。这样会形成一个很差的设计,一个类来完成全部的工作。
- 低耦合与其他原则,如信息专家、高内聚必须综合考虑。
何时不使用?
若是稳定的或广为流传的,可以有高耦合,如Java的jdk,软件系统可以和jdk高耦合。
控制器 Controller
问题
在领域层,由谁负责首先接收并协调来自UI层的系统操作?
解决方法
- Facade(外观) Controller:代表整个系统的对象,如一个根对象,一个系统运行的设备或一个主要的子系统。
- Use Case or Session Controller(用例控制器、会话控制器):代表一类系统事件产生的用例场景。
外观控制器
相当于领域层对外部世界的“脸”。适用于:
- 相对较小的系统
- 有限数量的系统操作
- 在消息处理系统中,不能转发消息到可选的控制器时
会话控制器
处理系统某个明确的功能集,比如相关的一组系统事件。适用于:
- 当采用外观控制器会导致高耦合、低内聚时
- 很多系统事件跨越多个不同的处理过程
- 概念上容易理解和构建
其命名习惯: Handler 、 CoOrdinator、Session
优点
- 容易适应UI层的变化
- 领域层代码易于重用(因为UI层一般与应用关系密切)
- 有助于保证应用所需要的操作顺序
- 可以对系统的状态进行推理(UI层不保存系统状态)
臃肿控制器的解决方法
当一个控制器处理了大部分系统事件时,控制器掌握了太多的系统信息,就会形成臃肿的控制器,导致低内聚。解决方法是:
- 增加更多的控制器
- 采用会话控制器替换外观控制器
- 控制器委托任务给别的对象,而不是自己做
- 高内聚的理念
高内聚 High Cohesion
问题
如何使对象功能专注、可理解、可管理,同时又支持低耦合。
解决办法
分配职责时保证高内聚。
用法
- 用作评价工具
- 更多的是一种理念,没有具体的可操作原则。
衡量概念之间相关度的两个指标
- Cohension,内聚:模块内元素之间联系紧密的程度,例如,一个类内部的操作之间。
- Coupling,耦合:两个模块之间联系的强度。
内聚的最佳实践
1 一个类完成的功能不要太多,且这些功能都是同级别的。例如:教授的主要功能是教学,研究员的主要工作是科研。
2 如果同类别的工作太多,则会定义新的类分担任务,相互间合作。
例如:”不是一家人,不进一家门”,“人”指的是操作,职责,”门”指的是模块,类。
类低内聚
- 症状
- 做了太多相互无关的工作:不是你的职责你做了。
- 做了太多工作:是你的职责,但做的事情太多了,需要把职责在细分。
- 原因
- 大粒度的抽象:如打扫卫生可以分为扫地,擦黑板等。
- 做了太多本应该委托给其他类去做的工作。
- 缺点
- 难以理解:因为职责太多,太复杂。
- 难以重用:耦合度太高。
- 难以维护:很难修改。
- 没有稳定的时刻,总是在修改 (通常都会高耦合)。
多态 Polymorphism
问题
如何处理因类型不同而导致行为不同的一类需求?
解决办法
使用多态操作为依据类型变化的行为来分配职责。
推论
- 不要测试对象的类型或条件逻辑,并以此选择相应的行为。
- 即,不要使用条件逻辑,而是为不同的类定义相同名字的方法。
- 不同的类实现相同的接口,或有一个共同的父类(继承)。
纯虚构 Pure Fabrication
问题
依据一些原则(比如,信息专家)获得的解决方案不合适的情况下,既不想违反低耦合、高内聚,也不想违反其他的原则,如何把职责分配给对象?
解决办法
把高度内聚的职责分配给虚构出来的一个类,这个类在领域模型里没有对应的概念。
推论
这种方式在有的场合能起到支持低耦合、高内聚、重用的效果。
原则
- 使用虚构类仍然保持低耦合、高内聚,但可重用性要增加。
- 多数情况下是按照功能来定义新的类,是一种“功能为中心”的对象,如果功能相关性比较高,则满足高内聚的特性。
风险
宽泛地说,虚构对象分为两类,代表性概念为主的分解和行为性概念为主的分解,可能导致面向功能或者面向过程的分析/设计,然后用OO语言去实现。
间接 Indirection
问题
若两个对象直接连接,导致耦合太紧,如何解决?
即,把职责分配到哪里可以避免两个或多个对象之间的直接耦合?如何解耦对象以保持较高的可重用性?
解决办法
把职责分配给一个中介对象,隔离对象,使两个对象、构件或服务之间不产生直接耦合。
因为中介对象有一种特殊的作用,一般对象与中间对象之间直接耦合,相对比较简单。
与纯虚构类似,但目的不同。
隔离变化
问题
如何设计对象、系统和子系统,使得这些成分里面的变化因素、不稳定因素不会对系统的其余部分造成意想不到的影响?
即,需求一定会变化的!如何做到以系统的局部变化为代价就可以应对这一点?
解决办法
标识出能够已知或预计的变化点或者不稳定点,职责分配的时候创建一个稳定的接口,把它们与系统的其余部分隔离开来。
注意:“隔离可能的变化”是一个设计原则,如下技巧都使用隔离变化:数据封装、多态、数据驱动设计、 服务查询、配置文件、接口等都是这种机制的不同体现。
变化点的分类
- 变化点:当前系统已经存在。
- 演化点:当前系统不存在,未来可能存在的变化。
GRASP原则是一种理念,要时刻牢记,随时使用,就会造成潜移默化地加深理解,从而得到更好的设计!