1、第六 章 详细设计阶段的类结构设计 详细设计阶段的一个十分重要的问题,就是进行类设计。类设计直接对应于实现设计,它的设计质量直接影响着软件的质量,所以这个阶段是十分重要的。 这就给我们提出了一个问题,类是如何确定的,如何合理的规划类,这就要给我们提出一些原则,或者是一些模式。 设计模式是有关中小尺度的对象和框架的设计。应用在实现架构模式定义的大尺度的连接解决方案中。也适合于任何局部的详细设计。设计模式也称之为微观架构模式。 第一 节 类结构设计中的通用职责分配软件模式 GRASP 模式( General Responsibility Assignment Software Patterns 通
2、用职责分配软件模式)能够帮助我们理解基本的对象设计技术,并且用一种系统的、可推理的、可说明的方式来应用设计理论。 一、根据职责设计对象 职责:职责与一个对象的义务相关联,职责主要分为两种类型: 1)了解型( knowing) 了解私有的封装数据; 了解相关联的相关对象; 了解能够派生或者计算的事物。 2)行为型( doing) 自身执行一些行为,如建造一个对象或者进行一个计算; 在其它对象中进行初始化操作; 在其它对象中控制或者协调各项活动。 职责是对象设计过程中,被分配给对象的类的。 我们常常能从领域模型推理出了解型相关的职责,这是因为领域模型实际上展示了对象的属性和相互关联。 二、职责和交
3、互图 在 UML 中,职责分配到何处(通过操作来实现)这样的问题,贯穿了交互图生成的整个过程。 例如: 所以,当交互图创建的时候,实际上已经为对象分配了职责,这体现到交互图就是发送消息到不同的对象。 三、在职责分配中 的 通用原则 总结一 下: 巧妙的职责分配在对象设计中非常重要。 决定职责如何分配的行为常常在创建交互图的之后发生,当然也会贯穿于编程过程。 模式是已经命名的问题 /解决方案组合,它把与职责分配有关的好的建议和原则汇编成文。 四、信息专家模式 解决方案: 将职责分配给拥有履行一个职责所必需信息的类,也就是信息专家。 问题: 在开始分配职责的时候,首先要清晰的陈述职责。 假定某个类
4、需要知道一次销售的总额。 根据专家模式,我们应该寻找一个对象类,它具有计算总额所需要的信息。 关键: 使用概念模型(现实世界领域的概念类)还是设计模型(软件类)来分析所具有所需信息的类呢? 答: 1 如果设计模型中存在相关的类,先在设计模型中查看。 2 如果设计模相中不存在相关的类,则查看概念模型,试着应用或者扩展概念模型,得出相应的概念类。 我们下面来讨论一下这个例子。 假定有如下概念模型。 到底谁是信息专家呢? 如果我们需要确定销售总额。 可以看出来,一个 Sale 类的实例,将包括“销售线项目”和“产品规格说明”的全部信息。也就是说, Sale 类是一个关于销售总额的合适的信 息专家。
5、而 SalesLineItem 可以确定子销售额,这就是确定子销售额的信息专家。 进一步, ProductSpecification 能确定价格等,它就是“产品规格说明”的信息专家。 上面已经提到,在创建交互图语境的时候,常常出现职责分配的问题。 设想我们正在绘设计模型图,并且在为对象分配职责,从软件的角度,我们关注一下: 为了得到总额信息,需要向 Sale 发出请求总额请求,于是 Sale 得到了 getTotal 方法。 而销售需要取得数量信息,就要向“销售线项目”发出请求,这就在 SalesLineItem 得到了 getSubtotal 方法。 而销售线项目需要向“产品规格说明”取得价
6、格信息,这就在 ProductSpecification 类得到了 getPrice 方法。 这样的思考,我们就在概念模型的基础上,得到了设计模型。 注意: 职责的实现需要信息,而信息往往分布在不同的对象中,这就意味着需要许多“部分”的信息专家来协作完成一个任务。 信息专家模式于现实世界具有相似性,它往往导致这样的设计:软件对象完成它所代表的现实世界对象的机械操作。 但是,某些情况下专家模式所描 述的解决方案并不合适,这主要会造成耦合性和内聚性的一些问题。后面我们会加以讨论。 五、创建者模式 解决方案: 如果符合下面一个或者多个条件,则可以把创建类 A 的职责分配给类 B。 1,类 B 聚和类
7、 A 的对象。 2,类 B 包含类 A 的对象。 3,类 B 记录类 A 的对象的实例。 4,类 B 密切使用类 A 的对象。 5,类 B 初始化数据并在创建类 A 的实例的时候传递给类 A(因此,类 B 是创建类 A实例的一个专家)。 如果符合多个条件,类 B 聚合或者包含类 A 的条件优先。 问题: 谁应该负责产生类的实例? 创建对象是面向对象系统最普遍的活动之一,因此,拥有一个分配创建对象职责的通用原则是非常有用的。如果职责分配合理,设计就能降低耦合度,提高设计的清晰度、封装性和重用性。 讨论: 创建者模式指导怎样分配和创建对象(一个非常重要的任务)相关的职责。 通过下面的交互图,我们立
8、刻就能发现 Sale 具备 Payment 创建者的职责。 创建者模式的一个基本目的,就是找到一个在任何情况下都与被创建对象相关联的创建者,选择这样的类作为创建者能支持低耦合。 限制: 创建过程经常非常复杂 ,在这种情况下,最好的办法是把创建委托给一个工厂,而不是使用创建者模式所建议的类。 六、低耦合模式 解决方案: 分配一个职责,是的保持低耦合度。 问题: 怎样支持低的依赖性,减少变更带来的影响,提高重用性? 耦合( coupling)是测量一个元素连接、了解或者依赖其它元素强弱的尺度。具有低耦合的的元素不过多的依赖其它的元素,“过多”这个词和元素所处的语境有关,需要进行考查。 元素包括类、
9、子系统、系统等。 具有高耦合性地类过多的依赖其它的类,设计这种高耦合的类是不受欢迎的。因为它可能出现以下问题: 相关类的变化强制局部变化。 当元素分离出来的时候很难理解 因为使用高耦合类的时候需要它所依赖的类,所以很难重用。 示例: 我们来看一下订单处理子系统中的一个例子,有如下三个类。 Payment(付款) Register(登记) Sale(销售) 要求:创建一个 Payment 类的实例,并且与 Sale 相关联。 哪个类更适合完成这项工作呢? 创建者模式认为, Register 记录了现实世界中的一次 Payment,因此 建议用 Register 作为创建者。 第一方案: 由 Re
10、gister 构造一个 Payment 对象。 再由 Register 把构造的 Payment 实例通过 addPayment 消息发送给 Sale 对象。 第二方案: 由 Register 向 Sale 提供付款信息(通过 makePayment 消息),再由 Sale 创建 Payment对象。 两种方案到底那种支持低的耦合度呢? 第一方案, Register 构造一个 Payment 对象,增加了 Register 与 Payment 对象的耦合度。 第二方案, Payment 对象是由 Sale 创建的,因此并没有增加 Register 与 Payment 对象的耦合度。 单纯从耦合
11、度来考虑,第二种方案更优。 在实际工作中,耦合度往往和其它模式是矛盾的。但耦合性是提高设计质量必须考虑的一个因素。 讨论: 在确定设计方案的过程中,低耦合是一个应该时刻铭记于心的原则。它是一个应该时常考虑的设计目标,在设计者评估设计方案的时候,低耦合也是一个评估原则。 低耦合使类的设计更独立,减少类的变更带来的不良影响,但是,我们会时时 发现低耦合的要求,是和其它面向对象的设计要求是矛盾的,这就不能把它看成唯一的原则,而是众多原则中的一个重要的原则。 比如继承性必然导致高的耦合性,但不用继承性,这就失去了面向对象设计最重要的特点。 没有绝对的尺度来衡量耦合度,关键是开发者能够估计出来,当前的耦
12、合度会不会导致问题。事实上越是表面上简单而且一般化的类,往往具有强的可重用性和低的耦合度。 低耦合度的需要,导致了一个著名的设计原则,那就是优先使用组合而不是继承。但这样又会导致许多臃肿、复杂而且设计低劣的类的产生。 所以,一个优秀的设计师,关键是用一种深入理解和权衡利弊的态度来面对设计。 设计师的灵魂不是记住了多少原则,而是能灵活合理的使用这些原则,这就需要在大量的设计实践中总结经验,特别是在失败中总结教训,来形成自己的设计理念。 设计师水平的差距,正在于此。 七、高内聚模式 解决方案: 分配一个职责,使得保持高的内聚。 问题: 怎么样才能使得复杂性可以管理? 从对象设计的角度,内聚是一个元
13、素的职责被关联和关注的强弱尺度。如果一个元素具有很多紧密相关的职责,而且只完成有限的功能,那这个元素就是高度内聚的。这些元素包括类、子系统等。 一个具有低内聚的类会执行许多互不相关的事物,或者完成太多的功能,这样的类是不可取的,因为它们会导致以下问题: 1,难于理解。 2,难于重用。 3,难于维护。 4,系统脆弱,常常受到变化带来的困扰。 低内聚类常常代表抽象化的“大粒度对象”,或者承担着本来可以委托给其它对象的职责。 示例: 我们还是来看一下刚刚讨论过的订单处理子系统的 例子,有如下三个类。 Payment(付款) Register(登记) Sale(销售) 要求:创建一个 Payment
14、类的实例,并且与 Sale 相关联。 哪个类更适合完成这项工作呢? 创建者模式认为, Register 记录了现实世界中的一次 Payment,因此建议用 Register 作为创建者。 第一方案: 由 Register 构造一个 Payment 对象。 再由 Register 把构造的 Payment 实例通过 addPayment 消息发送给 Sale 对象。 第二方案: 由 Register 向 Sale 提供付款信息(通过 makePayment 消息),再由 Sale 创建 Payment对象。 在第一个方案中,由于 Register 要执行多个任务,在任务很多的时候,就会显得十分臃
15、肿,这种要执行多个任务的类,内聚是比较低的。 在第二种方案里面,由于创建 Payment 对象的任务,委托给了 Sale,每个类的任务都比较简单而且单一,这就实现了高的内聚性。 从开发技巧的角度,至少有一个开发者要去考虑内聚所产生的影响。 一般来说,高的内聚往往导致低的耦合度。 讨论: 和低耦合性模式一样,高内聚模式在制定设计方案的过程中,一个应该时刻铭记于心的原则。 同样,它往往会和其它的设计原则相抵触,因此必须综合考虑。 Grady Booch 是建模的大师级人物,它在描述高内聚的定义的时候是这样说的:“一个组件(比如类)的所有元素,共同协作提供一些良好受限的行为。” 根据经验,一个具有高
16、内聚的类,具有数目相对较少的方法,和紧密相关的功能。它并不完成太多的工作,当需要实现的任务过大的时候,可以和其它的对象协作来分担过大的工作量。 一个类具有高内聚是非常有利的,因为它对于理解、 维护和重用都相对比较容易。 限制: 少数情况下,接受低内聚是合理的。 比如,把 SQL 专家编写的语句综合在一个类里面,这就可以使程序设计专家不必要特别关注 SQL 语句该怎么写。 又比如,远程对象处理,利用很多细粒度的接口与客户联系,造成网络流量大幅度增加而降低性能,就不如把能力封装起来,做一个粗粒度的接口给客户,大部分工作在远程对象内部完成,减少远程调用的次数。 第二 节 设计模式与软件架构 一、设计
17、模式 在模块设计阶段,最关键的问题是,用户需求是变化的,我们的设计如何适应这种变化呢? 1,如果我们试图发现事情怎样变化,那我们将永远停留在分析阶段。 2,如果我们编写的软件能面向未来,那将永远处在设计阶段。 3,我们的时间和预算不允许我们面向未来设计软件。过分的分析和过分的设计,事实上被称之为“分析瘫痪”。 如果我们预料到变化将要发生,而且也预料到将会在哪里发生。这样就形成了几个原则: 1,针对接口编程而不是针对实现编程。 2,优先使用对象组合,而不是类的继承。 3,考虑您的设计哪些是可变的,注意,不是考虑什么会迫使您的设计改变,而是考虑要素变化的时候,不会引起重新设计。 也就是说,封装变化
18、的 概念是模块设计的主题。 解决这个问题,最著名的要数 GoF 的 23 种模式,在 GoF 中,把设计模式分为结构型、创建型和行为型三大类。 本课程假定学员已经熟悉这 23 个模式,因此主要从设计的角度讨论如何正确选用恰当的设计模式。 整个讨论依据三个原则: 1)开放 -封闭原则 2)从场景进行设计的原则 3)包容变化的原则 下面的讨论会有一些代码例子,尽管在详细设计的时候,并不考虑代码实现的,但任何架构设计思想如果没有代码实现做基础,将成为无木之本,所以后面的几个例子我们还是把代码实现表示出来,举这些例子的目的并不是提供样板 ,而是希望更深入的描述想法。 另外,所用的例子大部分使用 C#来
19、编写,这主要因为希望表达比较简单,但这不是必要的,可以用任何面向对象的语言( Java、 C+)来讨论这些问题。 二、 封装变化与面向接口编程 设计模式分为结构型、构造型和行为型三种问题域,我们来看一下行为型设计模式, 行为型设计模式的要点之一是“封装变化”,这类模式充分体现了面向对象的设计的抽象性。在这类模式中,“动作”或者叫“行为”,被抽象后封装为对象或者为方法接口。通过这种抽象,将使“动作”的对象和动作本身分开,从而达到降低耦合性的效果。这样一来,使行为对象可以容易的被维护,而且可以通过类的继承实现扩展。 行为型模式大多数涉及两种对象,即封装可变化特征的新对象,和使用这些新对象的已经有的对象。二者之间通过对象组合在一起工作。如果不使用这些模式,这些新对象的功能就会变成这些已有对象的难以分割的一部分。因此,大多数行为型 模式具有如下结构。 下面是上述结构的代码片断: public abstract class 行为类接口 public abstract void 行为 (); public class 具体行为 1:行为类接口 public override void 行为 () public class 行为使用者