1.18k likes | 1.33k Views
第六章 设计模块. 中国科学技术大学 田野. Contents. 6.1 设计方法 6.2 设计原则 6.3 面向对象的设计 6.4 在 UML 中体现面向对象的设计 6.5 面向对象设计模式 6.6 设计中其它方面的考虑 6.7 面向对象测量 6.8 设计文档 6.9 信息系统的例子 6.10 实时系统的例子 6.11 本章的意义. Objectives. 设计原则 面向对象设计的启发式方法 设计模式 异常和异常处理 文档设计. 6.1 设计方法.
E N D
第六章设计模块 中国科学技术大学 田野
Contents 6.1 设计方法 6.2 设计原则 6.3 面向对象的设计 6.4 在UML中体现面向对象的设计 6.5 面向对象设计模式 6.6 设计中其它方面的考虑 6.7 面向对象测量 6.8 设计文档 6.9 信息系统的例子 6.10 实时系统的例子 6.11 本章的意义
Objectives • 设计原则 • 面向对象设计的启发式方法 • 设计模式 • 异常和异常处理 • 文档设计
6.1 设计方法 • 我们对客户问题的解快方案已经有了抽象描述,并以软件体系结构设计的形式表现出来。同样地.我们已经有一个把设计分解成软件单元和为系统分配功能性需求的计划。 • 体系结构设计阶段的末期和模块级别设计阶段的初期没有严格界限。 • 没有“设计菜谱”来指导你如何从软件单元的规格说明走到它的模块设计 • 通往最终解决方案过程本身并不如我们制作的文档重要 • 帮助理解
6.1 设计方法重构 • 交替地使用自顶向下、自底向上的方法完成设计 • 周期性地重审和修改设计决策,被称为重构 • 目标:简化过度复杂的方案,或者由于某特殊性能的考虑而对设计进行优化
6.1 设计方法“伪造”合理的设计过程 • 在理想的、方法学的、合理的设计过程中,软件系统的设计应该从高层的规格说明推进到解决方案,并在其中使用一系列的自顶向下、无差错的设计决策,以得到层次化的模块集合 • 由于某些原因(如不正确的理解.需求的变更、重构或人为错误),从需求到模块的设计工作很少进行得很顺利 • 我们仍应该表现得好像正在按照合理的过程来设计 • 把软件单元分解成模块 • 定义模块的接口 • 描述模块间的相互依赖关系 • 文档化模块内部的设计 • 在实现这些步骤时,为必须延迟实现的设计决策插入预留位置,在以后的时间里,当细节逐步变得清晰明了、被延迟的决策已经确定时,把预留位置用新的内容来替换
6.2 设计原则 • 设计原则是把系统功能和行为分解成模块的指导方针 • 从两种角度明确了我们应该使用的标准 • 系统分解 • 确定在模块中将要提供哪些信息(以及隐藏哪些信息) • 从6个领域的原则对讨论进行约束: • 模块化 • 接口 • 信息隐藏 • 增量式开发 • 抽象 • 通用性
6.2 设计原则模块化 • 模块化,是一种把系统中各不相关的部分进行分离的原则,以便于各部分能够独立研究(又被称为关注点分离) • 如果该原则运用得当,每个模块有有自己的唯一目的,并且相对独立于其他模块 • 每个模块理解和开发将会更加简单 • 故障的定位更加简单 (因为对于每个故障,可疑的模块会减少) • 系统的修改更加简单(一个模块的变功所影响的其他模块会减少) • 为了确定一个设计是否很好地分离了关注点,使用两个概念来度量模块的独立程度:耦合度和内聚度
6.2设计原则耦合度 • 当两个模块之间有大量依赖关系时. 我们就说这两个模块是紧密耦合的 • 松散耦合的模块之间具有某种程度的依赖性,但是它们之间的相互连接比较弱 • 非耦合的模块之间没有任何相互连接,它们之间是完全独立的 • 非耦合构件 • 松散耦合 • 紧密耦合
6.2 设计原则耦合度(continued) • 模块之间的依赖方式有很多种: • 一个模块引用另一个模块 • 一个模块传递给另一个模块的数据量 • 某个模块控制其他模块的数量 • 可以根据依赖关系的范围(从完全依赖到完全独立)来测量耦合度
6.2 设计原则耦合的类型 • 内容耦合 • 公共耦合 • 控制耦合 • 标记耦合 • 数据耦合
6.2 设计原则内容耦合 • 当一个模块修改了另外一个模块的内部数据项,一个模块修改了另一个模块的代码,或一个模块内的分支转移到另外一个模块中的时候,就可能出现内容耦合
6.2 设计原则公共耦合 • 可以从公共数据存储区来访问数据,降低模块之间的耦合程度 • 对公共数据的改变意味着需要通过反向跟踪所有访问过该数据的模块来评估该改变的影响,这种依赖关系被称为公共耦合
6.2 设计原则控制耦合 • 当某个模块通过传递参数或返回代码来控制另外-个模块的活动时,我们就说这两个模块之间是控制耦合 • 受控制的模块如果没接收到来自控制模块的指示,是不可能完成其功能的
6.2 设计原则标记和数据耦合 • 如果使用一个复杂的数据结构从一个模块向另一个模块传送信息,并且传递的是该数据结构本身,那么两个模块之间的耦合就是标记耦合 • 标记耦合体现了模块之间更加复杂的接口,因为在标记耦合中,两个交互的模块之间的数据的格式和组织方式必须是匹配的。 • 如果传送的只是数据值,不是结构数据,那么模块之间就是通过数据耦合 • 数据耦合更简单,而且因数据表示的改变而出错的可能性很小
6.2 设计原则内聚度 • 内聚度是指模块的内部元素(比如. 数据、功能、内部模块)的“粘合”程度
6.2 设计原则内聚度(continued) • 巧合内聚(内聚度最低) • 模块的各个部分互不相关 • 逻辑内聚 • 一个模块中的各个部分只通过代码的逻辑结构相关联 • 时态内聚 • 设计被划分为几个用来表示不同执行状态的模块 • 模块中数据和功能仅仅因在一个任务中同时被使用而形成联系
6.2 设计原则内聚度(continued) • 逻辑内聚的例子
6.2 设计原则内聚度(continued) • 过程内聚 • 必须按照某个确定的顺序执行一系列功能,模块中的功能组合只是为了确保这个顺序 • 过程内聚和时态内聚类似,但过程内聚有另外一个优点:其功能总是涉及相关的活动和针对相关的目标 • 通信内聚 • 可以将某些功能关联起来,因为它们是操作或生成同一数据集的,这种围绕数据集构造的模块是通信内聚
6.2 设计原则内聚度(continued) • 功能内聚 (理想状况) • 条件1:在一个模块中包含了所有必需的元素, • 条件2:并且每一个处理元素对于执行单个功能来说都是必需的 • 信息内聚 • 功能内聚的基础上,将其调整为数据抽象化和基于对象的设计
6.2 设计原则接口 • 接口(interface)为系统其余部分定义了该软件单元提供的服务,以及如何获取这些服务. • 一个对象的接口是该对象所有公共操作以及这些操作的签名的集合,指定了操作名称、参数和可能的返回值 • 接口必须定义该单元所必需的信息,以能确保该单元能够正确工作 • 软件单元的接口描述了它向环境提供哪些服务,以及对环境的要求
6.2 设计原则接口(continued) • 一个软件单元可能有若干不同的接口来描述不同的环境需求或不同的服务
6.2 设计原则接口(continued) • 软件单元接口的规格说明(specification) 文档描述了软件单元外部可见的性质 • 一个接口的规格说明需向其他系统开发人员传达正确应用该软件单元的所有信息,这些信息并不仅仅局限于单元的访问功能和它们的签名,还有如下几点 • 目标:为每个访问函数的功能性建立详细的文档 • 前置条件:列出所有假设,被称为前置条件 • 协议:包括访问函数的调用顺序,两个构件交换信息的模式 • 后置条件:可见的影响被称为后置条件 • 质量属性:性能,可靠性等
6.2 设计原则信息隐藏 • 信息隐藏的目标是使得软件系统更加易于维护,它以系统分解为特征: • 每个软件单元都封装了一个将来可以改变的独立的设计决策 • 然后我们根据外部可见的性质,在接口和接口规格说明的帮助下描述各个软件单元 • 最终的软件单元封装了各种类型的信息 • 隐藏了数据表达形式的模块可能是信息内聚的 • 隐藏了算法的模块可能是功能内聚的 • 隐藏了任务执行顺序的模钱可能是过程内聚的 • 信息隐藏的一个很大的好处是使得软件单元具有低耦合度
6.2 设计原则面向对象设计中的信息隐藏 • 在面向对象设计中,我们将一个系统分解成对象和它们的抽象类型 • 每个对象对其它对象隐藏了它的数据表示 • 其它对象访问给定的一个对象的唯一途径是通过这个对象接口中声明的访问函数 • 使用这种借息隐藏技术,使得更容易改变对象的内部表示,而不影响系统的其他部分 • 数据表示并不是我们可以隐藏的唯一设计决策 • 需要扩大对象的概念,以涵盖包括数据类型在内的信息类型 • 对象之间不可能完全非耦合,因为一个对象至少要能知道另一个对象才能实现交互,意味着 • 一旦我们改变了一个对象的名称或者对象实例的个数,那么所有调用该对象的单元都要做出相应的改变 • 当被访问的对象有着特定标识时,我们无法对这种依赖做出显著的改善,但当访问任意的一个对象时,我们可能会避免这种依赖
6.2 设计原则增量式开发 • 假定一个软件设计是由软件单元和它们的接口所组成的,我们可以使用单元之间的依赖关系来设计出一个增量式设计开发进度表 • 首先,我们指定单元间的使用关系 • 它为各个软件单元和它依赖的单元之间建立关联 • 将使用关系表述为使用图,图中节点代表软件单元,有向边从使用其他单元的软件单元出发指向被使用的单元。使用图可以帮助我们逐步确定更大的系统子集,对此我们可以增量地进行实现和测试。
6.2 设计原则增量式开发(continued) • 一个系统的两种设计的使用图 • 扇入指代使用某个软件单元的软件单元数量 • 扇出指代某个软件单元使用其他软件单元的数量 高扇入模块意味着该模块做得太多,应分为更小模块。 控制高扇入模块的数量,设计1更合理
6.2 设计原则增量式开发(continued) • 使用图中存在循环 • 循环太大难以支持增量式开发 • 我们可以尝试使用夹层法来消除循环. • 循环中的一个单元被分解成两个单元,这样分解后的新单元中没有必须依赖的单元(B2),另一个也没有依赖它的单元(B1) • 反复使用夹层法,解除高耦合单元间的相互依赖或者较长的循环链
6.2 设计原则抽象 • 抽象是一种忽略一些细节来关注其他细节的模型或表示 • 关于模型中的哪部分细节被忽略是很模糊的,因为不同的目标会对应不同的抽象, 会忽略不同的细节
6.2 设计原则使用抽象 • 假定系统的某个功能是重新排列列表L中的元素,设计的最初描述如下: Sort L in nondecreasing order The next level of abstraction may be a particular algorithm: DO WHILE I is between 1 and (length of L)–1: Set LOW to index of smallest value in L(I),..., L(length of L) Interchange L(I) and L(LOW) ENDDO • 算法提供了大量附加信息,从中可以得知,用于在L上重新执行徘列操作的过程
6.2 设计原则使用抽象(continued) • 第三个和最后一个算法确切地告诉我们重排操作是如何工作的: DO WHILE I is between 1 and (length of L)-1 Set LOW to current value of I DO WHILE J is between I+1 and (length of L) IF L(LOW) is greater than L(J) THEN set LOW to current value of J ENDIF ENDDO Set TEMP to L(LOW) Set L(LOW) to L(I) Set L(I) to TEMP ENDDO
6.2 设计原则通用性 • 通用性是这样一种设计原则: 在开发软件单元时,使它尽可能地能够成为通用的软件,来加强它在将来某个系统中能够被使用的可能性. • 我们通过增加软件单元使用的上下文环境的数量来开发更加通用的软件单元,下面是几条实现规则: • 将特定的上下文环境信息参数化 • 去除前置条件 • 简化后置条件
6.2 设计原则通用性(continued) • 下面列出4个旨在提高通用性的过程接口 要求变量名匹配 变量个数必须为3 要求计算数组的长度 最通用
6.3 面向对象的设计 • 面向对象的方法是最受欢迎、最完善的设计方法 • 如果一个设计将系统分解成若干个封装了数据和函数的运行时构件,即所谓的对象(object),那么我们就说该设计是面向对象的(object oriented,OO)。对象区别于其他构件的特征: • 对象是唯一可标识的运行时实体,它们可以设计为消息或请求的目标 • 对象是可组合的,因为它的数据变量本身可能也是对象,因而封装了对象的内部变量的实现 • 对象的实现可以通过继承的方式被复用和扩展,用来定义其他对象的实现 • 面向对象的代码可以是多态的:可以对多个不同但类型相关的对象都起作用的通用代码
6.3 面向对象的设计术语 • 类是一种部分或完全实现某抽象数据类型的软件模块 • 如果一个类没有为它的某个方法提供实现,那么我们称这个类为抽象类 • 类定义还包含了用来生成新对象实例的构造方法 • 还可以使用实例变量的值来指代对象,对象是类的一个确值
6.3 面向对象的设计术语(continued) • 面向对象系统的运行时结构是一系列对象的集合,每个对象都是数据以及用来创建、读取、更改和清除数据的所有操作的内聚的集合 • 对象的数据称作属性,而对象的操作称作方法 • 一个对象可能会有多个接口,每个接口向外部提供了对数据和对象不同级别的访问 • 这些接口按照其类型形成层次化关系:若一个接口所提供的服务是另一个接口所提供服务的严格子集,那么我们称前者是后者的子类型,后者则称为前者的超类型
6.3 面向对象的设计术语(continued) Sale类的部分设计
6.3 面向对象的设计术语(continued) • 实例变量甚至还可以在程序执行过程中指向各种不同的类的对象。这种灵活性被称为动态绑定(dynamic binding) • 结构之间的关系用箭头指示,箭头末端的注释表明了关系的多重性,多重性告诉我们这种项可能存在的数量
6.3 面向对象的设计术语(continued) • 面向对象的四种结构:类、对象、接口和变量实例 • 箭头末端表示多重性
6.3 面向对象的设计术语(continued) • 可以通过组合构件类来建立新的类,这好比孩子们用积木堆成不同的结构,称为对象组合 • 还可以通过扩展或修改现有类的定义来构造新的类 • 这种构造方式称为继承,它直接复用已有的类定义来获得一个新类
6.3 面向对象的设计术语(continued) • 继承的例子
6.3 面向对象的设计术语(continued) • 面向对象的方法可以实现多态,其中,代码是根据与接口的交互来编写的,但是,代码的行为却取决于运行时和接口相关联的对象,以及该对象的方法的实现 • 继承、对象组合以及多态是面向对象设计的重要特征,它们使得设计出来的系统在许多方面都是有用的
6.3 面向对象的设计继承 vs. 对象组合 • 设计中一个关键的决策就是如何最好地组织和关联复杂的对象 • 在面向对象的系统中,构造大型对象的技术主要有两种 • 继承 • 组合 • 可以通过扩展和重载现有类的行为来创建新的类,或者通过组合简单的类来形成一个新类
6.3 面向对象的设计继承 vs. 对象组合(continued) • 每一种构造范型都优劣并存 • 在保持被复用代码的封装性方面,组合的方法优于继承,因为组合的对象仅能通过它声明的接口来访问构件 • 相比较而言,如果使用继承的方法,子类的实现在设计的时候就已经确定了并且是静态的 • 与从组合类进行对象实例化相比,继承类的对象具有最小的灵活性,因为它们从父类继承的方法不可能在运行时发生改变 • 继承最大的好处就是,通过选择性地覆盖被继承的定义,可以改变和特化继承方法的行为 • 有经验的设计人员偏好组合,因为可以很方便地实现构件对象的替换
6.3 面向对象的设计可替换性 • 理想的情况是,子类必须保持其父类的行为,这样客户端代码才能把它的实例也当成其父类的实例来同等对待 • 利斯科夫替换原则 • 子类支持父类的所有方法,并且它们的签名是兼容的 • 子类的方法必须满足父类方法的规格说明. • 前置条件规则 pre_parent ⇒ pre_sub,相同或弱于父类 • 后置条件规则 pre_parent ⇒ (post_sub ⇒ post_parent ),后置条件不少于父类 • 子类必须保留父类中声明了的所有性质 • 可替换性并不是个强制性的规定,相反地,该原则作为指南使用,帮助我们决定什么时候不检查被扩展类的客户瑞模块,但又能保证它是安全的
6.3 面向对象的设计德米特法则 • 打印出售商品列表,方法1,generateBill方法 • Bill类直接使用CustomerAccount、Sale和Item类的接口。必须知道这些接口,才能正确调用 • 方法2:在Sale类中增加printItemList,在类中添加printSaleItems() • generateBill()调用CustomerAccount中的printSaleItems() • printSaleItems()调用合适的Sale对象中的printItemList() • printItemList()调用合适的Item对象中的打印方法
6.3 面向对象的设计德米特法则(continue) • 德米特法则:通过把组合类中作用在类构件上的每个方法都包含进来,我们可以降低它们的依赖程度 • 这个设计公约的好处在于使用了组合类的客户代码仅仅需要知道组合本身,而不需要知道组合的构件 • 一般情况下,遵循德米特法则的设计具有更少的依赖关系,而类之间的依赖关系越少,软件故障往往也就越少