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