软件工程与计算II

SE-II-ch12-详细设计

软件工程与计算II
目录

# 12-详细设计

# 1. 详细设计基础

详细设计的出发点:软件详细设计是在软件体系结构设计之后进行,以需求开发的结果(需求规格说明和需求分析模型)和软件体系结构的结果(软件体系结构设计方案与原型)为出发点

# 1.1. 什么是详细设计

  1. 中层设计是对特定的模块的,以及针对特定模块的对象/类的低级设计

  1. 高层设计反映的是系统高层抽象的构件层次,描述系统的高层结构、关注点和设计决策。
  2. 中层设计反映的是组成模块的内部结构,例如数据定义、函数定义、类定义、类结构等。
  3. 低层设计则是深入模块或者类的内部,关注具体的数据结构、算法、类型、语法和控制逻辑等。
  1. 软件体系结构定义了模块的规范(对外抽象出来的接口):就是模块之间交互需要知道的信息
  2. 详细设计通过细节设计机制实现模块
    1. 中级:(子调制)-> OO->类指定
    2. 低级:DS. + ALG. ->实现类
  3. 详细设计要求设计者考虑模块的美观,功能和许多其他方面
    1. 详细设计中的质量要求:修改,维护,性能……

# 1.2. 详细设计的输入

# 1.3. 从需求、体系结构设计到详细设计

  1. 具体的模块的设计是详细设计
  2. 是对体系结构设计的更加精确的描述

# 1.4. 详细设计是从哪里开始的

  1. 详细设计的目的是实现所有功能性需求和非功能性需求

# 1.4.1. 详细设计的上下文

  1. 模块的规格:导出/导入接口
  2. 职责分配:
    1. 有些职责来自RE(SRS 需求规格):典型的用例,领域模型,序列图,状态图
    2. 其他一些来自实施决策
  3. 在详细设计文档中需要明确定义:
    1. 模块结构及其接口(如果有更细的模块分解)
    2. 类结构、类协作、类接口(面向对象分析方法)
    3. 控制结构与函数接口(结构化分析方法)
    4. 重要的数据结构和算法逻辑(如果必要的话)

# 1.4.2. 软件体系结构:构件之间的接口

# 1.4.3. 详细设计的输出

# 2. 结构化详细设计

# 2.1. 结构化设计的思想

  1. 分解是降低复杂度的一种方法
  2. 按算法的分解:自然的分解想法
  3. 从数据流图向结构图的转换

# 2.2. 降低复杂度的方法

  1. 分解:同一层次
  2. 抽象:从低层次抽象出高层次

# 2.3. 如何描述一个系统?

  1. 一系列相互关联的过程
  2. 将输入转化为输出
  3. DFD:数据流图
    1. 数据流(箭头)
    2. 过程(圆圈)
    3. 数据存储(平行线)
    4. 外部实体(矩形)

# 2.4. 按算法分解

  1. 分而治之

# 2.5. 结构化设计

  1. 结构化设计的重心:从数据流图到结构图
  2. 上述转化过程:
    1. 寻找到输入的最高抽象点和输出的最高抽象点
    2. 根据输入、输出的最高抽象点,对模块进行划分
    3. 然后在一次对每个模块寻找最高抽象点,再进行模块分解,从而逐步求精得到树状的结构图
  3. 详细参考课本(201页)

# 3. 面向对象详细设计

# 3.1. 面向对象设计的思想:职责-静态设计模型(重要)

# 3.1.1. 职责

  1. 职责是执行任务(行为职责)或维护某些数据(数据职责)的义务。
    1. 行为职责通常由行为来履行。
    2. 数据职责通常由属性来完成。
    3. 可能会涉及到类之间的协作

# 3.1.2. 职责驱动的分解

  1. 职责可以在不同的抽象层次上陈述。
  2. 职责可以分解。
  3. 可以将高级职责分配给高级组件。
  4. 职责分解可以作为分解组件的基础:职责既反映了行为义务,也反映了数据义务,因此职责驱动的分解可能与功能分解不同。

# 3.1.3. 职责启发法

  1. 很好地分配职责有助于实现高内聚和低耦合。
  2. 确保模块职责不重叠。
  3. 仅当操作和数据有助于完成模块的职责时,才将其放置在模块中。

# 3.1.4. 委托

  1. 委托是一种策略,其中一个模块(委托人)将职责交给另一个模块(委托人)。
  2. 代理帮你完成联系和收集的情况

# 3.2. 面向对象设计的思想:协作-动态设计模型(重要)

# 3.2.1. 什么是协作

  1. “程序中的对象必须协作;否则,程序将仅由一个可以执行所有操作的大对象组成”——丽贝卡·维尔夫斯·布洛克等,《设计面向对象的软件》,Prentice Hall,1990年:内聚性好一定意味着比较零散(类比较多)
  2. 同等重要的(作为继承)是相互负责地协作的对象社会的发明。这些社会形成了我所谓的系统机制,并代表了战略性架构决策,因为它们超越了各个类。 -[The C ++ Journal,Vol.2,No.1 1992年,“与Grady Booch的访谈”]:每个对象都是相对自治的个体。
  3. 一个应用程序可以分解为许多不同的行为。
  4. 每个此类行为都是通过应用程序对象之间的独特协作来实现的:对象和对象之间的实践
  5. 每次协作,无论大小,都保证实现应用程序的行为
  6. 将面向对象的应用程序想象成通过关系连接的对象网络。
  7. 协作是通过网络追求特定行为的消息模式
  8. 协作分布在对象网络中,因此在任何地方都不存在

# 3.3. 协同设计的需求

  1. 毕竟,我们正在尝试实现的是应用程序操作。
  2. 如果实现它们的协作设计不当,则应用程序将不准确或脆弱

# 4. 面向对象详细设计的过程

  1. 面向对象:对象内部是容易理解的,之间的调用的理解是困难
  2. 结构化:模块内部是困难的,之间的调用是容易的
  3. 概念类图的类和设计类图的类是不同的:
    • 因为设计类图中有的类是辅助类
  4. 设计模型重构
    1. 根据模块化的思想进行重构,目标是高内聚、低耦合
    2. 根据信息隐藏的思想,目标是隐藏职责与变更

# 4.1. 通过职责建立静态设计模型

# 4.1.1. 抽象对象的职责

  1. 类表达了对对象族的本质特征的抽象,提供了构建一个对象的所需要的蓝图
  2. 职责分类
    1. 数据职责:对象的状态
    2. 行为职责:对象的行为

  1. + 是 public,- 是 private

# 4.1.2. 抽象类之间的关系

  1. 整体存在则部分存在,部分存在则整体存在
  2. 上图需要好好背诵和记忆:重点掌握类图的画法见书本p205 如果两者相互关联,那么是一条直线,不需要箭头

# 4.1.3. GRASP原则

General Responsibility Assignment Software Patterns 通用职责分配的软件模式

  1. 不是"设计模式",而是对象设计的基本原理
  2. 专注于对象设计的最重要方面之一:为类分配职责
  3. 强调适用性:并不是一个普适的
  4. 常见的一些特点:
    1. 低耦合:分配一个职责要保证低耦合度
    2. 高内聚:分配一个职责的时候要保持类的高聚合度
    3. 信息专家:将一个职责分配给专家-履行职责所必须的信息的类
    4. 创建者:创建规则在后面
    5. 控制者:控制规则在后面(避免大多数信息由一个类发出、组件相对较小、行为职责和数据绑定、职责单一)

# 4.1.3.1. 拇指原则

  1. 当存在替代设计选择时,请仔细研究替代方案的内聚和耦合含义,并可能对替代方案的未来发展压力。
  2. 选择具有良好内聚性,耦合性和稳定性的替代方案。

# 4.1.3.2. 信息专家

  1. 问题:在面向对象设计中分配职责的最基本原则是什么?
  2. 解决方案:将具有完成任务所必需的信息的类分配给类。
  3. 维护信息封装
  4. 促进低耦合
  5. 促进高内聚
  6. 具有数据的类应当有相应的职责
# 4.1.3.2.1. 信息专家的例子
  1. 谁负责求典型的销售点应用程序中的销售总额?(求总价) Sale

  1. 计算总计需要所有SalesLineItem实例及其小计。而这是只有销售(Sale)知道的
  2. 这就是为什么Sale是信息专家。
  3. 因此(通过全部的情况进行开展的)

  1. 但是每个订单项都需要小计(数量乘以价格)。
  2. 根据专家的说法,SalesLineItem是专家,知道数量并且与知道价格的产品规格相关联。

因此,职责分配给3个类别。

# 4.1.3.2.2. Eg.Case Study: 智能热水器
  1. 智能控制水温
    1. 周末水温⾼
    2. 夜晚水温低
    3. ⽣病等特殊情况水温高
    4. 度假水温低
  2. 概念模型
      1. 热水器控制器
        1. 模式
        2. 低温
        3. 高温
        4. 周末
      2. 时钟
    1. 接口:
    2. WaterHeaterController和Clock怎么交互?
      1. 轮询
      2. 通知
  3. 怎么知道当前时间是该升温还是降温?
    1. Controller 自己保存特殊时间并计算(比较当前时间和特殊时间):Bad:多个职责
    2. 由SpecialTime类保存特殊时间;Controller调⽤getSpecialTime()得到特殊时间,再计算
      1. Bad:数据职责与行为职责的分离
      2. SpecialTime是信息专家,对外给接口
    3. 由SpecialTime类保存特殊时间,并提供isSpecialTime();Controller调用方法
      1. Good:单一职责
    4. 谁有信息谁是专家,数据和功能不要分开
    5. 为什么同样是get方法
      1. 一个是合理的:商品那个,那个是因为商品和单价是分开的,所以是合理的
      2. 一个是不合理的:现在这个,因为只有一个数据就可以完成计算,SpecialTime自己就能完成计算,不需要把时间给Controller再让它去比较是不是特殊时间
      3. 一个是简单的get方法,不完全的数据和行为
      4. 另一个是只需要这一个数据就可以了,并且行为封装在一起是合理的
      5. 类之间的关系的影响

# 4.1.4. 添加辅助类

在需求分析阶段,我们分析用例表述,得到许多概念类。这些概念类也是我们设计模型中间的候选类。但是候选类往往不可能实现所有的功能,所以可能还需要添加一些辅助类:

  1. 接口类
  2. 记录类(数据类)【日志】
  3. 启动类:从各种地方的初始化,进行转发和分派
  4. 控制器类
  5. 实现数据类型的类
  6. 容器类

# 4.1.5. 添加辅助类后的设计模型/类图

# 4.2. 通过协作创建动态设计模型

# 4.2.1. 抽象对象之间协作

  1. 方法一:从小到大,将对象的小职责聚合形成大职责;
  2. 方法二:从大到小,将大职责分配给各个小对象
  3. 这两种方法,⼀般是**同时运⽤**的,共同来完成对协作的抽象。
  4. 顺序图
    • 可以⽤顺序图表示对象之间的协作。顺序图是交互图的⼀种,它表达了对象之间如何通 过消息的传递来完成⽐较⼤的职责。
    • 包含两部分:对象本身和对象之间的信息流
  5. 信息分为:图示见课本206页
    1. 同步消息(实线三角箭头)
    2. 异步消息(实线鱼骨箭头)
    3. 同步消息返回(虚线鱼骨箭头)

  1. 对象结束之后可以在底下画一个X表示结束
  2. 状态图
    1. 除了顺序图,我们还可以通过状态图来表达软件的动态模型。UML 状态图(State Diagram)
    2. 主要⽤于描述⼀个复杂对象在其⽣存期间的 动态⾏为,表现为⼀个对象所经历的状态序列, 引起状态转移的事件(Event),以及因状态转移⽽伴随的动作(Action)。⼀般可以⽤状态机对⼀个对象的⽣命周期建模,UML状态图 ⽤于显示状态机(State Machine Diagram),重点在于描述 UML 状态图的控制流。⽽协作 是:⽤复杂对象的状态图中的 Event 体现出对象之间消息的传递;⽤ Action 体现消息引发的对象状态的改变(⾏为)

# 4.2.2. 明确对象的创建

# 4.2.2.1. 创建者模式

  1. 问题:谁负责创建某个类的新实例?
  2. 解决方案:根据潜在的创建者类与要实例化的类之间的关系,确定哪个类应创建类的实例。
  3. 问题:谁负责创建对象?
  4. 回答:如果有以下情况,则分配给B类创建A类实例的职责【B创建A】:
    1. B 聚集了 A 对象
    2. B 包含了 A 对象
    3. B 记录了 A 的实例
    4. B 要经常使用 A 对象
    5. 当 A 的实例被创建B具有传递给A的初始化数据(也就是 B 是创建 A 的实例这项任务的信息专家)
    6. 在有选择的地方,更喜欢B聚合或包含A对象

  • 第一个(组合关系):部分由整体创建
  • 第二个(单向被关联):比如访问数据库,你要访问的时候,我就给一个访问对象来使用,不用的时候归还就行。
  • 第三个(持有必要数据):B是创建对象这个任务的信息专家;另外根据业务的情况决定什么时候被创建,有时候B可以创建但是不知道什么时机来创建,如果C知道,那么我们可能让C创建对象,然后B进行初始化
  • 第四个(聚合关系):关系比较多,要看时机等什么时候合适。如果有多个聚合关系,那么根据整体的高内聚和低耦合来决定谁创建
  • 第五个(其他情况):根据高内聚和低耦合原则

# 4.2.2.2. 创建例子

  1. 谁负责创建SalesLineItem对象? 销售:往往是一旦有sale就会创建
  2. 找到聚合或者包含了SalesLineItem的物体类

  1. 创建者模式建议是 Sale
  2. 合作图是

# 4.2.2.3. 创作者摘要

  1. 通过创建负责创建需要引用的对象的类的实例来促进低耦合
  2. 通过自己创建对象,它们避免依赖于另一个类为它们创建对象.

# 4.2.2.4. 谁创建Square / Piece / Player?

  1. Piece的创建(那个关联性最强,就是用哪一个来创建)【player和piece是聚合关系,一个player对应一个piece,square和piece是简单的关联】

    • Player?√
    • Board?

    【但如果pieces有很多,让player去选择,那么显然pieces是由game创建,同时player也由game创建】

  2. Squares的创建:Board创建

  3. Player的创建:用Game创建

# 4.2.3. 控制器

  1. 问题:如何分配处理系统事件的职责?
  2. 解决方案:如果程序从其图形界面以外的其他来源接收事件,请添加事件类以将事件源与实际处理事件的对象分离

# 4.2.3.1. 控制方式

  1. 将处理系统事件消息的职责分配给代表以下选项之一的类:
    1. 整个组织的业务(外观(facade)控制器)。
    2. 整个系统(外观控制器)。
    3. 在问题域中真实操作解决问题的人(角色控制器)。
    4. 自动化解决用例的模块(用例控制器)。

# 4.2.3.2. 控制者

  1. 购买项目用例中的系统事件
    1. 输入部分
    2. 结束售卖
    3. 结账
  2. 谁负责输入
  3. 控制者有四种
    1. 整个系统 Post
    2. 整个业务 商店
    3. 在现实生活中活跃在任务中的收银员
    4. 在系统中机器处理这个部分

  1. 不好的设计:按了按钮就会直接进行响应,展示层直接调用业务逻辑,展示层和业务逻辑耦连,一旦业务逻辑改变,展示层也要变

  1. 好的设计:使用一个controller【有上面介绍的4种controller选择】来处理事件,由它来调用业务逻辑
  2. 用POST方法申请Sale
  3. 谁设定enterItem接口?和需求有关
  4. 界面变更和逻辑变更的频率时不同的,需要分开,Controller存在的必要性

# 4.2.3.3. 控制器总结

  1. Controller本身不是面向对象的,它包含很多复杂的逻辑
  2. 使用控制器对象可使外部事件源和内部事件处理程序彼此独立于他们的类型和行为
  3. 控制器对象可能变得高耦合和职责上低内聚

# 4.2.3.4. 什么是棋盘游戏的控制者?

# 4.2.4. 选择合适的控制风格(重要)

  1. 集中式控制风格
  2. 委托式控制风格
  3. 分散式控制风格

  1. 系统行为的逻辑在对象(组件)网络之间分布的方式。
  2. 分散的:系统行为的逻辑通过对象网络"广泛传播"
  3. 集中式:一个额外的控制器记录系统行为的所有逻辑。

# 4.2.4.1. 控件控制情况

  1. 做出决定并指导他人行动的对象是控制器。
  2. 他们总是与他人合作有两个原因:
    1. 收集信息以便做出决定
    2. 并呼吁其他人采取行动。
  3. 他们的重点通常是决策而不是执行后续操作:他们的最终职责通常会转移给对控制器负责的较大任务有更多特定职责的其他人

# 4.2.4.2. 控制器协作情况

  1. 控件样式是一种将所有系统行为分布在对象(组件)网络之间的方式。
    1. 集中式:几个控制器记录所有系统行为的逻辑
    2. 委托式:通过对象网络分配决策,由几个控制器进行主要决策
    3. 分散式:所有系统行为都通过对象网络广泛传播

# 4.2.4.3. 集中式控制风格

  1. 容易找到做出决定的地方
  2. 易于查看如何制定决策以及如何更改决策流程
  3. 控制器可能会变得的庞大,复杂且难以理解,维护,测试等。
  4. 控制器可以将其他组件视为数据存储库
    1. 增加耦合
    2. 破坏信息隐藏

  • 都是他在调用别人

  • 部分去中心化的中心模式(如上)

  • 上图例子:通过一些部分特别的方式读取输入

  • 控制器只负责状态转移,不管具体的状态处理

  • 更加分散的设计:进行分发,只负责协调

# 4.2.4.4. 控制的启发1

  1. 避免大多数消息都来自单个组件的交互设计。
  2. 保持组件较小。
  3. 确保并非仅将全部职责分配给几个组件。
  4. 确保操作职责与数据职责一致。

# 4.2.4.5. 委托式控制风格

  1. 作出决策的对象不只有一个,职责的分解决定了控制对象的层次。

# 4.2.4.6. 分散式控制风格

  1. 其特点是拥有许多组件,几乎没有数据,职责也很少。
  2. 很难理解控制流。
  3. 组件无法独自完成很多工作,从而增加了耦合。
  4. 隐藏信息是很难的。
  5. 内聚性通常很差。
  6. 很少有模块化原则可以满足。
  7. 完全靠对象自治的方式来实现自己的职责。

# 4.2.4.7. 控制启发二

  1. 避免要求每个组件发送许多消息的交互。

# 例子

# 5. 为类间协作开发集成测试用例

软件体系结构的多个模块因为被独立开发而需要进行集成测试。同样的道理,每个类也是被独立开发的,也可能会产生集成的问题,也需要进行集成测试——类间协作的集成测试

所以在详细设计之后,需要以详细设计模型为基础为类间协作开发集成测试用例。

# 5.1. 详细设计的集成测试

  1. 类间协作的集成测试
    1. 重点针对复杂逻辑(交互⽐较多)
    2. ⾃顶向下或者⾃底向上的集成
  2. Mock Object
    1. 类间协作的桩程序通常被称为 Mock Object, 它不同于体系结构集成的 stub 类型桩程序。 Mock Object 要求自身的测试代码更简单,可以不用测试就能保证正确性。
    2. 【之前的模块间因此driver和stub。详细设计阶段是在模块内部,使用mock object即虚拟的对象来替代还没有写好的对象,写法和之前的stub类似,只不过作用不太一样】
    3. 不是stub

# 5.2. 类间协作的集成测试

# 6. 详细设计文档描述和评审

  1. 所有模块都应该尽量详细

# 6.1. 详细设计验证

  1. 评审:应该很好的展开
  2. 度量
    1. 模块化度量
  3. 测试
    1. 协作测试

  1. 设计的信息程度对后继开发人员是否足够?就是给不同人应该差不太多。

# 7. 第三阶段

  1. 制品合理性