通用职责分配软件模式
Understanding responsibilities is key to good object-oriented design.
--Martin Fowler
GRASP 定义了9个基本的OO设计原则或基本设计构件.
There are nine GRASP patterns:
Creator, Controller, Pure Fabrication,
Information Expert, High Cohesion, Indirection,
Low Coupling, Polymorphism, Protected Variations.
One person’s pattern is another person’s building block.
某人的模式是其他人的原始构造模块.
解决的问题: 对象应该由谁来创建?
解决方案:
如果以下条件为真时(越多越好), 将创建类A实例的职责分配给B:
· B “包含” 或组成聚集了A
· B 记录A
· B 紧密的适用A
· B 具有A的初始化数据
如果有一个以上的选项适用, 通常首选聚集或包含A的类B
组合聚集部分, 容器容纳内容, 记录者进行记录, 所有这些都是类图中类之间极为常见的关系. 创建者模式建议, 封装的容器或记录器类是创建其所容纳或记录的事务的很好的候选者. 当然这只是一个准则.
禁忌
对象的创建往往有相当的复杂性, 最好的方法是吧创建职责委派给工厂的辅助类,而不是直接使用创建者
问题: 给对象分配职责的基本原则是什么?
一个设计模型也许要定义数百或数千个类, 一个应用程序也许需要实现数百或者数千个职责. 在对象设计中, 当定义好对象间的交互后, 我们就可以对类的职责分配作出选择. 如果选择的好, 系统就会易于理解, 维护和扩展, 而我们的选择也能为未来的应用提供更多复用构件的机会.
解决方案: 把职责分配给具有完成该职责所需信息的那个类
职责所需要履行职责的信息,即关于其他对象的信息,对象自身的状态,对象自身周围的环境,对象能够导出的信息,等等. 在本例中, 为了能够检索和表示任何square, 某个对象必须知道所有Square.
专家模式通常导致这样一种设计, 软件对象所做的操作通常是作用与它们在真实世界中所代表的非生命体的那些操作. Peter Coad 称之为 “DIY” 策略. 例如, 在真实世界中, 不借助电子装置的帮助, 销售本身无法告诉你它的总额, 销售是一种非生命体, 销售的总额是人算出来的. 但是在面向对象的软件领域, 所有对象都是”活的” 或者 “有生命的”, 并且它们可以承担职责, 完成任务. 从根本上说, 它们只完成那些与它们所知信息有关的事情.
问题: 为什么是这个类去创建而不是其他类?
怎样降低依赖性, 减少变化带来的影响, 提高重用性.
耦合原则适用与软件开发的许多方面, 它实际上是构件软件的最重要的目标之一.
解决方案: 分配职责以使(不必要的) 耦合保持在较低的水平. 用该原则对可选方案进行评估.
我们用低耦合原则来评价现有设计,或者评价在新的可选方案(其他方面都等价的方案)之间作出选择,我们应该首选耦合更低的设计.
在更高的目标层次上考虑, 为什么期望低耦合呢? 换言之, 为什么我们要减少变化产生的影响呢? 因为低耦合往往能够减少修改软件所需的时间, 工作量和缺陷. 这只是个简要的回到,但是它们对于构建和维护软件而言具有重大意义.
简要的说, 耦合(coupling) 是元素与其他元素的连接,感知和依赖的程度的度量. 如果存在耦合或依赖, 那么当被依赖的元素发生变化时, 则依赖者也会受到影响. 例如, 子类和超类是强耦合的. 调用对象B的操作的对象A与对象B的服务之间具有耦合作用.
高耦合的设计, 会遇到以下问题:
l 由于相关类的变化而导致本体的被迫变化
l 难以单独的理解
l 由于使用高耦合类时需要它所依赖的类, 因此很难重用
在实践中, 耦合程度不能脱离专家, 高内聚等其他原则孤立的考虑. 不过, 它的确是改进设计所需要考虑的因素之一.
禁忌
高耦合对于稳定和普遍使用的元素而言并不是问题. 例如, J2EE应用能够安全的将自己与JDK耦合, 因为java库是稳定的, 普遍使用的.
高耦合本身并不是问题所在, 问题是与某些方面不稳定的元素之间的耦合, 这些方面包括接口, 实现等
问题: 在UI层之上首先接收和协调(“控制”)系统操作的对象是什么?
解决方案: 把职责分配给能代表下列选择之一的对象:
· 代表全部”系统”, “根对象”, 运行软件的设备或主要的子系统(façade controller的所有变体)
· 代表发生系统操作的用例场景(用例或会话控制器(session controller)), 通常命名为
Option 1: 代表全部”系统” 或 “根对象”
Option 2: 代表运行软件的设备
Option 3: 代表用例或者会话. 如(SomeHandler 或者someSession)
控制器设计的常见缺陷是分配职责过多. 这时, 控制器会低内聚, 从而违反了高内聚原则.
准则: 正常情况下, 控制器应当吧需要完成的工作委派给其他的对象. 控制器只是协调或控制这些活动, 本身并不完成大量工作.
臃肿的控制器
设计不良的控制器内聚性低, 即没有重点, 并且要处理过多领域的职责, 这种控制器叫做臃肿的控制器. 臃肿的迹象有:
l 只有一个控制器类来接收系统中全部的系统事件, 而且有很多系统事件. 如果选择了外观控制器就会碰到这种情况.
l 为了处理系统事件, 由控制器完成诸多必要的任务, 而不是把工作委派出去. 通常就会违反信息专家和高内聚模式.
l 控制器有很多属性, 并且它维护关于系统或领域的重要信息( 这些职责本应分配给其他对象 ), 或者它要复制在其它地方可以找到的信息.
解决控制器臃肿的办法:
l 增加控制器.
l 设计控制器, 使它把完成的每个系统操作的职责委派给其它对象.
再强调一次: 控制器模式的重要推论是, UI对象和UI层不应具有处理系统事件的职责.
从对象设计的角度上说, 内聚(或者更为专业的说, 是功能内聚)是对元素职责的相关性和集中度的度量.
在左侧的方案中, MonopolyGame对象自己完成全部工作, 而在右侧方案中, 它为playGame请求工作进行了委派和协调.
内聚是软件设计中的一种基本品质, 内聚可以非正式的用于度量软件元素操作在功能上的相关程度, 也用于度量软件元素完成的工作量.
有100个方法和2000行源代码的Big对象, 要比只有10个方法和200行代码的Small对象所完成的任务多很多. 如果Big对象的100个方法覆盖了众多不同的职责领域, 那么Big对象比Small对象的功能内聚性更低. 概括的讲, 代码的数量及其相关性都是对象内聚程度的指示器.
很明显, 低内聚不只是意味着对象仅仅依靠本身工作, 实际上, 具有2000行代码的低内聚对象或许需要和大量其他对象进行写作. 下面是一个关键点, 所有的交互也都会产生高耦合. 低内聚和高耦合通常是齐头并进的.
在实践中, 内聚程度不能脱离其它职责及其它原则(如信息专家和低耦合)单独的考虑.
与低耦合一样, 在所有的设计决策期间, 高内聚是要时刻牢记的原则, 它是一个需要不断考虑的基本原则. 它是评估所有设计决策时, 设计者要使用的评价原则.
Grady Booch认为, 当构件元素(如类) “能够共同协作并提供某种良好界定的行为”, 则存在高功能性内聚.
根据经验, 高内聚的类方法数目较少, 功能性有较强的关联, 而且不需要做太多的工作. 如果任务规模较大的华, 它就与其它对象协作, 共同完成这项任务.
高内聚的类优势明显, 因为它易于维护,理解和复用. 高度相关的功能性和少量的操作相结合, 也可以简化维护和改进的工作. 细粒度的, 高度相关的功能性也可以提高复用的潜力.
高内聚模式是真实世界的类比. 显而易见, 如果一个人承担了过多不相关的工作, 特别是本应委派给别人的工作, 那么此人一定没有很高的工作效率. 从某些还没有学会如何分派任务的经理身上可以发现这种情况, 因此正承受着低内聚所带来的困难,变得”分身乏术”.
另一个经典原则: 模块化设计
模块化是将系统分解成一组内聚的, 松散耦合的模块的特性.
不良内聚导致不良耦合, 反之亦然. 我把内聚和耦合称为软件工程中的阴和阳.
因为它们是互相依赖的.
OOD 非魔力地带
在对象设计中, 不需要任何不合道理的决断, 职责的分配和协作的选择都是能够被合理解释和学习的. 事实上, OO设计更接近于科学而非艺术, 尽管存在巨大的创造性和优雅设计的空间.
准则 (guidelines)
在编码时, 至少首先要编写启动初始化的程序. 但是在OO设计建模的过程中, 要最后考虑启动初始化. 知道发现那些是真正需要被创建和初始化的. 然后, 再对初始化进行设计以支持其他用例实现的需要.
当存在多个可选设计时, 应当深入的观察可选设计所存在的内聚和耦合, 以及未来可能存在的进化压力. 选择具有良好内聚,耦合和在未来出现变化时能保持稳定的设计.
命令-查询分离原则(Command-Query Separation Principle)
In particular, the roll method is void. It has no return value. For example:
// style #1; used in the official solution
public void roll()
{
faceValue = // random num generation
}
public int getFaceValue()
{
return faceValue;
}
为什么不将两个方法合并起来, 使roll方法返回新的faceValue呢? 如下所示:
// style #2; why is this poor?
public int roll()
{
faceValue = // random num generation
return faceValue;
你可以发现大量使用风格2 的例子, 但是这种方式并不合适, 因为它违反了命令-查询分离原则(Command-Query Separation Principle), CQS是针对方法的经典OO设计原则. 该原则指出, 任何方法都是如下情况之一:
l 执行动作(更新, 调整, ...)的命令方法, 这种方法通常具有改变对象状态等副作用, 并且是void的(没有返回值).
l 向调用者返回数据的查询, 这种方法没有副作用, 不会永久性的改变任何对象的状态.
关键是, 一个方法不应该同时属于以上两种类型.
Roll()方法是命令, 它具有改变Die对象的faceValue属性状态的副作用. 因此, 它不应该同时返回新的faceValue, 否则该方法也会成为查询, 从而违反了”必须为void”的规则.
CQS被公认为计算机科学理论中最有价值的原则, 因为遵循该原则, 你能够更容易的推测出程序的状态, 在查询状态时不会同时发生变更. 这样使得设计更便于理解和预见. 例如, 如果应用一直遵循CQS, 那么你会知道查询或者getter方法不会做出任何修改, 而命令也不会有任何返回. 这是个简单的模式. 这通常是要严格遵循的, 因为如果突然采取其他方法, 将会产生令人不快的意外, 从而违反软件开发中最小意外(Least Surprise)的原则
可见性
l 属性可见性
l 参数可见性
l 局部可见性
l 全局可见性
第二部分的连接:
通用职责分配软件模式(GRASP)学习笔记(二)