它的核心思想是“职责分配(Responsibility Assignment)”。GRASP提出了几个基本原则,用来解决面向对象设计的一些问题。
Craig Larman在《Applying UML and Patterns》一书中提出了GRASP设计模式的概念。作者称其为设计模式,其实,更好的理解应该为设计原则。因为,与GoF等设计模式不同的是,GoF等设计模式是针对特定问题而提出的解决方法,而GRASP则是站在面向对象设计的角度,告诉我们怎么样设计问题空间中的类与它们的行为责任,以及明确类之间的相互关系等等。GRASP可以说是GoF等设计模式的基础。
它包含了9个基本模式:
1,信息专家(Information expert)
2,创建者(Creator)
3,高内聚(High Cohesion)
4,低耦合(Low coupling)
5,控制器(Controller)
6,多态性(Polymorphism)
7,纯虚构(Pure Fabrication)
8,间接性(Indirection)
9, 变化预防(Protected Variations)
l 对象职责分配的基本原则。
l 主要应用在分析和建模上。
自己干自己的事(职责的分配)
自己干自己的能干的事(职责的分配)
自己只干自己的事(职责的内聚)
所谓面向对象设计,就是在系统设计的过程中,通过把系统分成相对独立但又互相联系的对象组合的一种设计方法。
对象具有属性和行为,对象间通过消息进行交互(协作)。
面向对象设计一般有以下几个关键步骤:
1, 发现对象。找出系统应该由哪些对象构成。
2, 对象的属性。对象具有哪些属性。
3, 对象的行为。对象具有哪些行为;或者说对象需要做什么,它的职责是什么。
4, 对象的关系。对象与对象之间的关系是什么,怎样进行交互,协作等等。
对象的发现超出了本文的讨论范围。我们简单地介绍一下对象发现的过程与手段。
一般意义上的对象是现实世界上物体的抽象。也就是说,现实世界里有什么物体,就有什么对象;物体存在什么属性,对象就有什么属性。
我们可以用“名词筛选法”来发现系统的对象。
比如,一个学生考试成绩管理系统,有以下简单的用例:
- 管理员创建题库(把题条加入题库)
- 系统根据管理员输入的某些条件随机生成试题
- 学生成绩入库与管理
我们可以通过字面意思找出名词,就可以找出“管理员“,“题条”,“题库”,“试题”,“学生”,“学生成绩”等几个对象。
一般通过这种方法(名词筛选法)就可以找出系统的绝大部分对象。
行为是对象应该执行的动作,也就是对象的职责。对象具有哪些职责呢?相对于上面所说的“名词筛选法”,可以简单地用“动词筛选法”来发现“对象的行为”。
比如,上面的“学生考试成绩管理系统”一例中,有“创建题库”,“输入(条件)”,“生成试题”,“成绩入库”,“成绩管理”等动词,也就是说,系统的对象至少具有以上这些行为(或职责)。
有时,我们可能还会发现某些“行为”的粒度过大,这时,可以通过进一步细化用例的描述,来发现更多更细的“行为”。这里不再详述。
问题1:找出对象的行为(职责)之后,怎么样分配这些行为呢?也就是说怎么确认“行为”属于哪个对象呢?
系统的所有对象不可能是一个个单独存在毫无关系的个体,它们或多或少的有着各种联系(协作关系)。
问题2:如果2个对象之间有协作关系,他们之间最好通过什么样的方式协作呢?
对象扩展特性
问题3:已经被抽象出来的对象,如何面对将来可能发生的变化呢?
GRASP提出9个基本模式,用于解决以上设计过程中遇到的各种问题。
信息专家模式(Information Expert)是GRASP模式中解决类的职责分配问题的最基本的模式。
当我们为系统发现完对象和职责之后,职责的分配原则(职责将分配给哪个对象执行)是什么?
职责的执行需要某些信息(information),把职责分配给该信息的拥有者。
换句话说,某项职责的执行需要某些资源,只有拥有这些资源的对象才有资格执行职责。
这有点类似现实世界的“有能者为之”的概念。你有建筑知识,可以去执行盖楼的职责;你有法律知识,可以去裁判案件等等。
满足了面向对象设计的封装性的设计,一般情况下都会满足Information Expert模式。因为Information Expert是对类的属性(信息),以及对类的属性的操作的封装,它符合对象封装性的概念。
信息的拥有者类同时就是信息的操作者类,可以减少不必要的类之间的关联,实现了信息封装。
各类的职责单一明确,容易理解。
为了说明问题,我们使用“学生成绩管理系统”中的用例1来说明。
用例1:
- 管理员创建题库(把题条加入题库)
再细化一下:
- 管理员创建题库(把题条加入题库):如果题库中已经存在所给的题条,则退出,否则加入题条。
这样就存在3个对象:管理员用户User,题条SubjectItem,题库SubjectLibrary
2个职责:判断(新加入的题条是否与题库某题条相等),加入(题条的加入)
这2个职责究竟应该由哪个对象执行?
我们使用Information Expert模式来分析。
1, 判断2个题条是否相等,只要判断题条的ID属性(或其它属性)是否相等就可以了。题条的ID是属于题条的,所以对它的操作应该放在题条SubjectItem里。
2, 题条的加入需要操作的数据有2部分,一部分是新加入的题条本身,另一部分是题库(加入到题库),题条是题库的一部分,所以题条的加入应该放在题库SubjectLibrary里完成。如图:
如果把以上2个职责放在第三方类中,无疑增加了它们与第三方类之间的耦合关系。
没有万能药,专家模式可能在某些情况中造成耦合和内聚。
例如,将Sale保存在数据库我们需要将与数据库处理相关的逻辑作为Sale的责任吗?
如果这样的话,我们将违反基本的结构原则:依据分离的主要的系统关注点进行设计,将应用逻辑放在一个地方,将数据库逻辑放在另外一个地方 (持久化服务子系统)
创建者模式(Creator)是GRASP模式中解决类的实例的创建职责问题的模式。
类的实例的创建职责,应该分配给什么样的类?或者说类的实例应该由谁创建?
以下条件之一为真的情况,类A的实例的创建职责就分配给类B。
1, B包含A
2, B聚集A
3, B记录A
4, B频繁使用A
5, B有A初始化数据
Creator模式提倡类的实例(对象)创建职责由聚集或包含该对象的对象创建。
注:Creator模式只是一个原则,如果类A,B之间没有包含或聚集关系,应该先考案是否有“B记录A”,或者“B有A初始化数据”的关系,然后是“B频繁使用A”的关系。另外,作为代替方案,一般的采用工厂(Factory)创建方案。
如果不遵循Creator模式,把类的实例的创建职责交给无关的类,类之间的关系变得复杂化,降低系统的可维护性和可扩展性。
一般来说,应用Creator模式,可以从上之下设计好类之间的包含或聚集关系阶层图,让每个类负责创建自己包含的类的实例。
整个结构清晰易懂
有利于类或组件的重用
防止职责的分散
降低耦合性
为了更清楚地说明Creator模式,我们举一个GUI的例子:
有一个用户窗口MainWindow,包含Menu,ToolBar,Dialog等,Dialog上布置有Textbox,Button等元素。
我们应用Creator模式,先为它们设计好具有阶层关系的类图,如下:
因为MyMenu,MyToolBar,MyDialog由MainWindow所包含,MyTextbox,MyButton被MyDialog包含,MainWindow由Main类调用。
根据Creator模式所提倡的方法,它们的实例的创建职责的分配应该是:
MainWindow的实例由Main创建;
MyMenu,MyToolbar,MyDialog的实例由MainWindow创建;
MyTextbox,MyButton的实例由MyDialog创建。
反过来,如果MyMenu,MyToolBar,MyDialog等实例的创建都放在Main类里,那么Main就跟它们产生一种“关联”关系,如果 MyMenu,MyToolBar,MyDialog等发生修改,Main也不得不跟着一起修改,也就是说大大增强了Main类跟它们之间的耦合关系;而 Main类本身,也聚集了多余的实例创建功能,降低了Main类的聚合性。
高内聚模式(High Cohesion)是GRASP模式中为降低类的复杂程度,简化控制而提出的面向对象设计的原则性模式。高内聚(High Cohesion)与低耦合(Low Coupling)模式是GRASP其他模式的根本。
怎么做才能降低类的复杂程度,简化控制?
紧密相关的功能(职责)应该分配给同一个类。
聚合度或内聚度(cohesion)是一个类中的各个职责之间相关程度和集中程度的度量。非常低的内聚:一个类单独处理很多不同模块的事务。比较低的内聚:单独处理一个模块的所有事务。
所谓内聚,是指单个物体(类)内部的功能聚集度。比如,只包含有相互关联的功能的类,具有高内聚性,同时,它的外部表现(作用,意图)也就明显;反之,如果一个类由一些不相关的功能构成,它的内聚性就低,它的外部表现就不明显,一方面很难理解它的作用和意图,另一方面,一旦需求变化,扩展性就差。
在现实世界里,高内聚(High Cohesion)表现在“各司其职”上,也就是说自己只干跟自己相关的工作,别人的工作让别人做。比如,电视机只有信息传播的功能,冰箱只有冷藏冷冻的功能,它们就是一个功能高内聚的个体。为什么不把电视机与冰箱的功能做在一起呢?因为做在一起的话,一方面,只需要电视或冰箱功能的消费者却不得不同时购买它们的整合体,而且消费者如果想换代电视机时,冰箱也只有一起换代;另一方面,如果厂家需要升级电视功能,也不得不考虑怎么整合原来的冰箱功能。也就是说功能低内聚的产品,不利于消费者使用,不利于生产者维护,不利于产品本身的升级换代。
同样,反映到软件设计上,低内聚的类存在使用难,维护升级难的缺点。
高内聚(High Cohesion)与低耦合(Low Coupling)是GRASP模式的核心概念,是其它GRASP模式的根本。
优秀的面向对象设计,一般都遵从[高内聚,低耦合]原则。
聚集相关功能,结构清晰,容易理解
只聚集相关功能,使得类的职责单一明确,从而降低类的复杂程度,使用简单
低耦合模式(Low Coupling)是GRASP模式中为降低类之间的关联程度,适应可变性而提出的面向对象设计的原则性模式。高内聚(High Cohesion)与低耦合(Low Coupling)模式是GRASP其他模式的根本。
怎么做才能降低类之间关联程度,能适应需求的变化呢?
为类分配职责时,应该尽量降低类之间的关联关系(耦合性)。亦即,应该以降低类之间的耦合关系作为职责分配的原则。
所谓耦合,是指多个物体(类)之间的物理或者意思上的关联程度。在面向对象方法中,类是最基本的元素,耦合主要指不同类之间相互关联的紧密程度。面向对象里的关联,主要指一个类对另一个类的调用,聚合(包含),参数传递等关系。
比如,所谓2个关联得非常紧密的类(高耦合),是指其中一个类发生变化(修改)时,另一个类也不得不跟着发生变化(修改)。
面向对象设计要求类之间满足“低耦合”原则,它是衡量一个设计是否优良的一个重要标准,因为“低耦合”有助于使得系统中某一部分的变化对其它部分的影响降到最低程度。
独立性,有利于重用。
适应需求变化,一旦发生变化时,可以把影响缩小到最小范围。
高内聚(High Cohesion)与低耦合(Low Coupling)是GRASP模式的核心概念,是其它GRASP模式的根本。
优秀的面向对象设计,一般都遵从[高内聚,低耦合]原则。
1, 一方面,高内聚要求把紧密关联的功能(职责)聚集在同一个类中,防止功能的扩散和类的无谓增加,从而减少类之间的关联,降低类之间的发生耦合的机率。
2, 另一方面,高内聚要求把不相关的功能分散到不同的类,类增加了,势必造成相互关联类的增加,从而增大类之间发生耦合的机率。
3, 面向对象设计,应该考虑效率,实现难度等因素,同时兼顾高内聚(High Cohesion)与低耦合(Low Coupling)性。
低耦合是一个在设计中时时需要记住的原则;它也是需要一直考虑的目标。这是设计者用来评估设计决策时运用的一个评价准则。
但是我们知道OO 软件是一个协同系统. 这意味着对象之间从某种程度上都是耦合的。因此,这是一个我们尽力去满足的原则,而不必一定遵循。
对稳定的元素和普遍的元素的高耦合一般不是一个问题。例如,一个Java J2EE应用对 Java库 (java.util, 等等)的耦合没有问题, 因为它们是稳定的,并被普遍使用。
控制器模式(Controller)是GRASP模式中解决事件处理职责问题的模式。
在UI层之外,应该由哪个类来处理(控制)系统操作(事件)呢?或者说,当触发一个系统事件时,应该把对事件的处理职责分配给UI层之外的哪个层呢?
把系统事件的处理职责分配给Controller(控制器)类。
担当Controller(控制器)类角色的候补类可能为:
- 系统全体,设备,子系统等的表现类(Facade Controller)
- 系统事件发生的用例的控制类,通常被命名为Handler,Coordinator,Session等(用例或Session的控制器)。整个系统事件都使用同一个控制器。
Controller模式相当于著名的MVC设计模式的C(Controller)部分。
类似于J2EE核心模式中的Front Controller模式(我们会在其它文章中介绍Front Controller模式)。
Controller模式提倡用一个专门的类来处理所有的系统事件。或者说Controller模式把所有系统事件的处理职责分配给一个专门的类集中处理。
应用Controller模式的系统,对系统事件进行集中处理,所以:
- 防止同类职责的分散。满足高内聚,低耦合原则。
- 有利于共通处理(前处理,后处理等)。
- 变化的高适应能力。能够把变化的修改范围控制在最小范围(控制器)之内。
MVC模式。
MVC模式Model-View-Controller头字母的缩写,中文翻译为“模型-视图-控制器” 模式(或者模型)。该模式把一个GUI应用划分为业务逻辑处理(M),画面表示(V),控制(C)三部分,并以此为基础进行设计和开发。
在设计和开发应用系统时,往往需要考虑系统的可维护性,可扩展性,可重用性等;而且,一个大规模的系统开发,往往都是多人分工合作,为了开发上的效率性考虑,一般都安排不同的专家(开发人员)负责不同的领域担当不同的工作。
MVC的构成要素:
MVC模式有Model,View,Controller三部分构成。
Model
模型。主要用来负责业务逻辑的处理,数据的保持。Model是MVC模式的核心部分,它也是一个应用需要实现的最主要的部分:进行业务逻辑的处理。
View
视图。负责数据的输出,画面的表示。
Controller
控制器。负责接收从视图发送过来的数据,同时控制Model与View部分。它的主要任务是控制Model与View,所以被称为控制器。
MVC模式输入输出流程图:
1, Controller接收用户输入
2, Controller调用Model进行业务逻辑处理(控制)
3, Controller通知/调用View进行画面描画处理(控制)
4, View根据需要适当参照Model的值
5, View进行画面描画处理
使 用MVC模式,分离模型、视图与控制器,使得这三部分功能相对独立,一方面可以让系统的设计开发工作分工明确,方便开发人员的互相合作;另一方面,按照 MVC模式划分的系统的各部分功能保持独立,有利于组件复用,例如,一个模型可以对应多个显示视图,也就是说,同一套业务逻辑只要改变视图便可对应不同的用户界面。
“浮肿的”控制器(Bloated Controllers):如果设计得不合理,控制类内聚性不强-不聚焦并处理了太多领域的责任。
症兆
(1) 一个控制类接收所有的系统事件;
(2) 控制类自己处理了完成系统事件所需要的太多任务;
(3) 控制器有太多的属性并维持了系统或领域的信息;
多态性模式(Polymorphism)是GRASP扩展模式的一种,它通过多态操作把基于类型的可变行为的定义职责分配给行为发生的类。即当相关的可选择的方法或行为随着类型变化时,将行为的职责-使用多态的操作-分配给那些行为变化的类型。
根据类型(类)的不同而发生变化的行为的定义职责,应该分配给谁?
问题比较抽象难懂,我们通过举例来解释一下。
比如物体的移动行为,不同的物体有不同的移动方法,比方说汽车与人的移动方法不一样。
在面向对象设计中,怎么样分配此类行为的定义职责呢?或者说,此类行为应该在哪定义怎么定义呢?
Polymorphism模式提倡通过多态操作把基于类型的可变行为的定义职责分配给行为发生的类。
又是一个抽象的概念。我们来解释一下。
多态性是面向对象的重要概念之一。所谓多态性,简单地说,就是具有同一接口的不同对象对相同的消息具有不同的行为。或者说同一消息作用于不同的对象,而产生不同的结果。
传统的设计方法,当类型发生变化时,利用条件判断语句对类型进行判断,然后执行不同的行为。
Polymorphism模式把各变化的“行为”定义职责分别分配给具有相同操作行为界面的通用接口的实现子类,利用多态性适应行为的可变性。
- 避免重复代码
- 避免重复的分歧条件
- 易扩展。只要实现了统一的通用接口,便可实现行为的扩展
纯虚构模式(Pure Fabrication)是GRASP扩展模式之一,它把非问题领域中的职责分配给人工定义的类。
非问题领域中的职责应该分配给谁?或者说,按照信息专家等模式分配职责时,存在某些不恰当的职责时,应该怎么做?
所谓不恰当的职责,是指难以分配的职责:在保证高内聚,低耦合的条件下,某些职责难以分配给现存的任何问题领域里的类。
Pure Fabrication模式提倡把那些非问题领域的职责分配给那些人工生成的或者容易此类职责的概念类。
我们设计对象的时候应该尽量保持与现实世界里的对象一致。这种与现实世界里的对象保持一致的从业务分析中抽象出来的类叫做“Domain Class”。它相当于上述问题领域里的类。
比如一个简单的用例:用户注册。
用户就是一个“Domain Class”,它是现实世界里的业务对象。相当于这里的“问题领域里的类”。
用户注册需要操作数据库,【数据库操作】是系统功能实现的一个必需功能,它不是现实世界里存在的业务对象,它是一个非Domain Class。如果把【数据库操作】看作一个行为职责,它就相当于这里所说的“非问题领域里的职责”。
一般来说,Domain Class与非Domain Class的功能如果聚集在一个类里,就破坏了“高内聚”原则。
- 高内聚。不必分配问题领域以外的职责给各Domain类,从而保证各Domain类内部功能上的高度聚集性。
- 低耦合。问题领域以外的职责被分配给第三方非Domain类,一方面可以降低各Domain类之间的关联程度,另一方面可以比较漂亮地整合系统的各方面的职责。
- 重用性。各Domain类由于功能上的聚集与关联度的降低,可以更容易地得到重用。
间接性模式(Indirection)是GRASP模式中解决类的关联问题的模式。
为了避免类之间的直接关联,应该给什么样的类分配“关联”责任?
当多个类之间存在复杂的消息交互(关联)时,Indirection模式提倡类之间不直接进行消息交互处理(非直接),而是导入第三方类,把责任(多个类之间的关联责任)分配给第三方类,降低类之间的耦合程度。
- 高内聚。通过把“关联”的功能分散到第三方类,原来的类可以更加关注自身功能的实现。
- 低耦合。原本关联类之间不直接关联,降低类之间的耦合性。
- 高重用性。第三方类对“关联”功能的集中处理,与原来的类对自身功能的专注,有利于类的重用。
变化预防模式(Protected Variations)是GRASP扩展模式之一,它设计稳定的接口来应对将来可能发生的变化或其它不安定的因素。
对存在于系统,子系统,或对象等元素中的各种变化或不安定的因素,为了不产生对其他元素的不利影响,在它们中间应该怎么样分配职责?
Protected Variations模式提倡在可预测的变化或不安定因素的周围,用稳定的接口来承担职责。
在面向对象设计中,面向接口编程便符合Protected Variations模式的概念。
有人把Protected Variations模式称为Don't Talk to Strangers(别跟陌生人说话)。因为它跟实现Protected Variations模式的考虑方法一致。
Don't Talk to Strangers别名Demeter法则:(LoD: the Law of Demeter),它的基本原则是:只跟直接依赖的对象通信(不要耦合没有明显通信需求的2个对象),也就是说2个对象之间,能不关联的就尽量不要关联。
所谓直接依赖的对象,例如有一个对象A,跟它直接依赖的对象有:
1, A对象本身
2, A的属性成员对象
3, 通过参数传送给A的对象(A的方法里参数)
4, A的方法内部生成的对象
为什么说LoD跟实现Protected Variations模式的考虑方法一致呢?
我们举例来说明。
假如,系统需要实现这样一个功能,把一段字符串保存到文件,打印机等输出设备。
这是一个可变的或者说存在不安定因素的功能需求,因为输出设备除了文件,打印机之外,还可能有数据库,屏幕终端,网络输出流等。
应用Protected Variations模式,我们为其定义一个能实现输出功能的稳定接口IOutputer,而具体的功能在具体的子类中实现,比如打印机输出类 PrinterOutputer,数据库输出类DatabaseOutputer,文件输出类FileOutputer等。
使用此“输出功能”的用户只要知道接口就行了。
也就是说,对于用户来说,用户的直接依赖对象只有父接口IOutputer,至于其子类诸如PrinterOutputer,DatabaseOutputer,FileOutputer等都属于陌生人。
- 提高系统对变化的应对能力。一旦系统的可预见的不安定因素发生变化(比如追加功能等),只需要生成一个已有的稳定接口的实现类就可以了,无需修改原来的类。
- 高内聚。具体的功能在各子类中实现,各类的内部功能具有高度聚集性。
- 低耦合。用户类只跟稳定接口通信,减少了跟其它陌生对象的关联的机会,降低了类之间的耦合性。