本文主要内容来自 SpriCoder的博客,更换了更清晰的图片并对原文的疏漏做了补充和修正。
内聚和耦合概念重要
- 内聚:内聚表达的是一个模块内部的联系的紧密型:包括信息内聚、功能内聚、通信内聚、过程内聚、时间内聚、逻辑内聚和偶然内聚。
1 |
|
- 耦合:耦合描述的是两个模块之间关系的复杂程度:包括内容耦合,公共耦合,重复耦合,控制耦合,印记耦合,数据耦合
1 |
|
模块化与信息隐藏思想
设计好的软件
- 什么是好的软件代表的?
Parnas 1972
- 管理人员(可管理性)
- 产品灵活性(灵活性)
- 可理解性(可理解性)
- 特征
- 允许编写一个模块,而几乎不了解另一个模块中的代码
- 允许在不重新组装整个系统的情况下重新组装和更换模块。
Stevens 1974
- 简洁性(Simplicity)
- 易于调试
- 易于分解
- 可观察性(Observability)
- 易于修改
Beohm 1976
- 可维护性
- 可扩展性
- 可理解性
- 可重用性
动机
- 模块化是关于如何将软件程序划分(分解)为不同模块的思想
- 信息隐藏是更多地从模块的外部(抽象)来进行思考
模块化与信息隐藏思想的发展
背景
- 1960s
- 软件和硬件不同
- 软件制作
- 1970s
- 软件是数据 + 算法
- 瀑布模型
- 形式化方法
- 1980s
- 重用
- 对象
- 人件
历史发展
- 萌芽:Wirth1971; Parnas1972;
- 形成:Stevens1974; Parnas1978; Parnas1985;
- 发展:Eder1992; Hitz1995;
- 反思:McConnell1996; Demarco2002
Wirth1971 - 萌芽
- 软件通过逐步求精发展
- 核心思想:逐步求精
- 该程序是在一系列优化步骤中逐步开发的
- 尽可能分解决策
- 解开看似相互依赖的方面
- 尽可能推迟有关代表细节的决定
- 对程序和数据结构的描述的修饰应并行进行。
- 以这种方式获得的模块化程度将决定程序可以适应目的变化或扩展或环境变化的难易程度。
- 每个优化都基于一组设计标准隐含了许多设计决策。
- 仔细编程并不是一件容易的事。
Parnas 1972 - 萌芽
- 关于将系统分解为模块的标准
- 什么是模块化?
- 模块:成为职责分配而不是子程序
- 模块化:包括在独立模块上的工作开始之前必须做出的设计决策(“系统级”决策)
- 分解标准
- 信息隐藏
- 分解中的每个模块都以其对设计决策的了解为特征,而对其他所有决策则都不了解。
- 层次结构
- 如果可以在模块或程序之间定义某种关系
- 该关系是偏序的
- 我们关注的关系是“使用”或“依赖”
- 数据结构,其内部链接,访问过程和修改过程是单个模块的一部分(封装的思想)
- 操作系统和类似程序的队列中使用的控制块格式必须在“控制块模块”中隐藏
- 在此之前都是想法,而不是方法
Stevens 1974 - 形成
- "Structured design" 结构化设计
- 模块:一组一个或多个连续的程序语句,其名称具有系统其他部分可以调用的名称,并且最好具有其自己独特的变量名称集
- 耦合:通过一个模块到另一个模块的连接建立的关联强度的度量
- 凝聚力:巧合;逻辑;沟通;顺序;功能性
- 模块的控制范围
- 决策的效力范围
- 当决策的影响范围在包含决策的模块的控制范围内时,系统会更简单
Stevens 1974:Designing the structure 设计结构
- 步骤 1:勾画出问题的功能图
- 步骤 2:确定外部概念性数据流。
- 步骤 3:确定问题中的主要外部概念数据流
- 步骤 4:使用源模块针对每个概念输入流设计源结构,该模块存在于大多数抽象输入数据的位置;简单的下沉模块
- 步骤 5:对于每个模块,确定产生该模块返回的表单所需的最后一次转换
- 担心的是一个地方的变更会导致其他部分的变更
Parnas 1978 - 形成
- “设计易于伸缩的软件”
- 迈向更好结构的步骤
- 需求定义:首先确定子集
- 信息隐藏:接口和模块定义
- 虚拟机概念
- 设计“用途”结构
- “依赖”关系
- 如果 A 可能需要正确执行 B 才能完成 A 规范中描述的任务,则 A 使用 B
- 满足以下所有条件时,允许 A“使用”B:
- A 本质上更简单,因为它使用 B
- B 实际上不复杂,因为它不允许使用 A
- A 有一个有用的子集,包含 B,不需要 A
- 没有可以想象的有用的子集包含 A 但不包含 B
Parnas 1985 - 形成
- “复杂系统的模块化结构”
- 模块说明
- 主要隐藏
- 角色
- 分配模块特定职责的标准
- 模块层次结构
- 顶层分解
- 二级分解
- 第三级
- 隐藏
- 主要隐藏:模块中包含的隐藏信息。
- 次要隐藏:用于实施模块的实施决策。
- 顶层分解
- 硬件隐藏模块
- 行为隐藏模块
- 软件决策模块
Eder 1992 - 发展
- “面向对象系统中的耦合和内聚”
- “良好”设计准则
- 面向对象的耦合:相互作用耦合;组件耦合;继承耦合
- 面向对象的凝聚力:方法凝聚力;班级凝聚力;继承凝聚力
Hitz 1995 - 发展
- “在面向对象的系统中测量耦合和内聚”
- 类级耦合
- 对象级耦合
- 耦合和内聚综合指标的框架
- 定量分析面向对象系统
McConnell 1996 - 反思
- "缺少行动:信息隐藏"
- 信息隐藏
- 是软件工程研讨会的设计思路之一
- 不需要或不依赖任何特定的设计方法
- 其特点是“隐藏”
- 最常见的隐藏是您认为可能会更改的设计决策
- 询问需要隐藏哪些内容,可以支持所有级别的良好设计决策
Demarco2002 - 反思
“Structured Analysis”
内容:描述了 75 年自己写的文章的内容,什么到现在仍然是对的
第五条:接口隔离原则
模块化
- 计算机系统不是整体的:它们通常由多个交互模块组成。
- 长期以来,模块化一直被视为廉价,高质量软件的关键。
- 系统设计的目标是决定:
- 什么是模块;
- 模块应该是什么?
- 模块之间如何交互。
什么是模块?
- 通用视图:一段代码。有局限性。
- 编译单元,包括相关的声明和接口
- 大卫·帕纳斯:一个工作单元。
- 编程单元(程序,类等)的集合
- 在整个系统中具有明确定义的界面和目的,
- 可以独立分配给开发人员
- 课本总结:模块是一个词汇上邻接的程序语句序列,由边界元素限制范围,有一个聚合标识符。
为什么要将系统进行模块化?
- 管理:分而治之
- 演进:分离系统的各个部分,以便将一个部分的更改与其他部分的更改隔离开
- 直接性原则(将需求明确分配到模块,理想情况下一个需求(或多个)映射到一个模块)
- 连续性/局部性原则(需求的微小变化仅触发对一个模块的更改)
- 理解:促使我们的系统更加容易被理解
- 作为思维大小的块的组成,例如 \(7 \pm 2\) 规则
- 一部分只有一个问题,例如本地性原则,封装,关注点分离
- 关键问题:模块化使用什么标准?->信息隐藏
结构化设计中的耦合
模块化
- 两种思路
- 可以根据数据总线进行切割
- 可以根据门电路进行划分
模块之间
- 连接:连接是对其他地方定义的某些标签或地址的引用
- 联系(两个模块之间)的复杂度
- 数量
- 程度
结构化的耦合
- 耦合是对从一个模块到另一个模块的连接所建立的关联强度的度量。
- 连接有多复杂
- 连接是指模块本身还是模块内部的东西
- 正在发送或接收的内容
耦合的强度 1 - 连接有多复杂
- 连接模块:对常见的环境:全局变量,范围
- 对于其他模块
原则一:全局变量是被认为是有害的
连接到全局环境
- 假设 N1 + N2 = N,M1 + M2 = M
- 如果 M 个模块共享 N 个元素
- 有潜在的 N * M 个连接
- (N1 + N2) _ (M1 + M2)= N1 _ M1 + N2 _ M2 + N1 _ M2 + N2 * M1
- 如果 M1 模块有 N1 个共享元素,而 M2 模块有 N2 个共享元素(也就是 N1 对应 M1,而 N2 对应 M2):存在潜在的(N1 _ M1 + N2 _ M2)接
- 如果 M 个模块共享 N 个元素
- 封装减少耦合:抽象和分解
- 将潜在的共享元素细分成组
- 将每个组的访问权限限制为最小的模块子集
常见环境的缺陷
- 连接将每个模块共享给每个其他这样的模块
- 一个模块中的错误和更改可以传播到其他模块
- 理解一个模块需要其他人的帮助
- 更难于重用
- 通用环境中的每个元素都会增加整个系统的复杂性
原则二:如果没有特殊要求,让代码清晰一点
- 明确的
- 可修改的
明确的和可修改的
- 属性和字典
- 明确的电话和事件
- 显式子类和数据驱动的代码
原则三:不要重复
- 如何消除重复呢?先写接口
- 也就是我们只需要 new 一个 printer 就行
耦合的强度 2 - 连接是指模块本身还是模块内部的东西
- Connections that address or refer to a module as a whole by its name yield lower coupling than connections referring to the internal elements of another module 以其名称寻址或整体引用一个模块的连接产生的耦合比引用另一个模块内部元素的连接产生的耦合低(整体连接的耦合程度高于模块内部元素连接的耦合)
- 关联到内部是不合适的,应该是关联到接口
- 接口耦合度低,而如果是具体关联耦合度高
原则四:面向接口编程
结构良好的系统
- 通过已定义的接口通过传递的参数进行通讯
耦合的强度 3 - 应该对外提供什么和接收什么
- 数据耦合:发送数据正好的耦合
- 传递必要数据的连接
- 印记耦合:发送数据比刚好需要的多
- 连接传递的数据超出了必要
- 控制耦合:除了数据还有控制信号
- 传递数据和控制元素的连接
- 内容耦合:数据和控制元素的耦合
- 另一个模块修改一个模块的代码
- 目标模块非常依赖于修改模块。
- 显然,印记耦合可以实现更多的数据耦合
- 控制耦合比数据耦合还耦合更多:信息隐藏
更简单一点
- 在上层已经处理好需要处理啥
- 不需要传递控制信号
结构化方法中的耦合度
- 内容耦合
- 公共耦合
- 重复耦合
- 控制耦合:传递的人和接收的人都需要管理控制信号
- 印记耦合
- 数据耦合
- 没有耦合是最好的:但是是做不到的
- 从上向下耦合度降低
- 几乎没有没有耦合的系统,知识要降低耦合
- 重复耦合及以上不允许存在
- 控制耦合和印记耦合都要尽量消除
注:内容耦合、重复耦合和公共耦合是不能接受的。数据耦合是最好的,而控制耦合和印记耦合是可以接受的
结构化设计中的内聚
- 偶然内聚:没什么关系(避免)
- 逻辑内聚:将一系列操作放在一起,由其他模块进行调用
- 时间内聚:操作和时间有关系
- 过程内聚:更强调按照一定步骤进行调用
- 通信内聚:需要操作相同的数据
- 功能内聚:只执行一个单一操作和功能
- 信息内聚:数据和行为是在一起的
- 事件、过程、通信尽量做
- 最好能做到功能、信息内聚
内聚
- 实现独立模块的方式
- 减少不在同一模块中的元素之间的关系
- 增加同一模块中元素之间的关系
- 绑定-凝聚力的度量
内聚度(绑定类型)
- 从低内聚到高内聚
- 巧合的
- 逻辑上
- 暂时的
- 沟通的
- 功能性
- 信息内聚
KWIC 案例
- 一共有 4 种 Java 的实现方式,注意复习
KWIC
简称 KWIC,又称上下文关键词索引,由 IBM 的卢恩首创,是最早出现的机编索引, 1960 年首次用于美国化学文摘社出版的《化学题录》(Chemical Titles)。
KWlC 索引的的编制特点是:
- 使用禁用词表选择标题中具有检索意义的词为关键词,并将其作为确定索引条目的依据;
- 关键词的排检点设于标题的中部,所有索引条目按关键词的字顺竖向排列;
- 保留文献篇名中关键词前后的上下文,如文献名称过长,可以以轮排的形式移至条目的前部或后部;款目后跟随该信息资源的位置。
上述条目均按关键词的字顺排列在相应位置,检索时先在检索入口处查找与检索课题有关的关键词,再通过阅读上下文寻找符合检索要求的文献。可以按排检点为中心对同一关键词有关的资源集中检索查找,是这一索引的优点;不足是将索引的排检点设置在中部不符合用户使用习惯。
对很多的关键词进行关键词排序
KWIC 来实现系统的模块化
- KWIC 索引系统接受:
- 一组有序的线
- 每行是一组有序的单词
- 每个单词都是一组有序的字符
- 每行都“循环移位”并通过以下方式复制:
- 反复删除第一个单词
- 将其附加在行的末尾
- 输出按字母顺序排列的所有行的所有循环移位的列表
- 通过下标就可以排序数组,下标是 title 的中间
根据功能进行设计
循环位移算法的实现
信息隐藏
- 基本思想:每个模块都隐藏了重要设计决策的实现,因此只有该模块的组成部分才知道详细信息:特别是如果存在所有可能的设计更改的列表-隐藏假设列表
- 所有设计决策彼此独立
- 两种常见的信息隐藏:
- 一是根据需求分配的职责,因为实践表明,需求是经常变化的,频率和幅度都很大;
- 二是内部实现机制,常见的变化主题包括硬件依赖,输入输出形式,非标准语言特征和库,负责的设计和实现,复杂的数据结构,复杂的逻辑,全局变量。数据大小限制等。
根据设计决策进行设计
- 模块之间的交互是通过接口实现的
CircularShifter 的定义
- 有无更改调用的方法:不用
- 有无更改调用的方法之中的实现:可能更改
分解标准
- 第一种模块化:处理过程中的每个主要步骤都是一个模块
- 第二种模块化:
- 信息隐藏:每个模块都有一个或多个“隐藏”
- 行:字符/行的存储方式
- 循环位移:旋转算法,旋转存储
- 字符表化:alpha 的算法,alpha 的惰性
- 每个模块都以其对设计决策的了解为特征,而对其他所有决策则都不了解。
- 信息隐藏:每个模块都有一个或多个“隐藏”
- 预期不到的变更是无法进行规避的
可修改性比较
- 按照算法分解比按照决策分解的修改范围大的多
独立的发展
- 模块化 1
- 必须先设计所有数据结构,然后才能进行并行工作
- 需要复杂的描述
- 模块化 2
- 必须先设计接口,然后才能开始并行工作
- 仅简单描述
可理解性
- 第二种模块化是更好的
- 主观性判断
- 更低的模块之间的耦合:面向接口编程
模块化
- 一个职责设计而不是一个子程序
- 由特定于自己的设计决策表示,而其他模块则未知
- 在实现上支持灵活性
- 不要代表过程中的步骤
- 低耦合,高内聚
结论
- 模块
- 可以被放置到一起的去形成一个完成系统的模块
- 职责分配,子过程,内存加载,功能部分
- 模块化:设计独立的模块
- 封装
- 语言便利性
- 面向对象三大基本思想:数据和行为在一起,体现共同职责
- 信息隐藏
- 设计原则
- 信息隐藏是更高的设计原则
结构化的模块化
- 1-2 道大题是关于设计模式的
MSCS 中的模块思想的应用
低耦合处理
- 软件体系结构的分层设计中:
- 不同层的模块之间仅能通过程序调用与数据传递实现交互,不能共享数据(例如 Model 层建立一个数据对象并将引用传递给 Logic 层使用)否则会导致公共耦合。
- 软件体系结构的逻辑包设计中:
- 依据功能的特点将三个层次进一步划分为更小的包,而不是只使用 Presentation、Logic 和 Model 三个包,可以通过包分割实现接口最小化,这能去除不必要的耦合。
软件体系结构的物理包设计中
- 将不同包的重复内容独立为单独的包以消除重复,避免产生隐式的重复耦合;
详细设计中对象创建者的选择:
- 如果两个对象 A、B 间已有比较高的耦合度了, 那么使用 A 创建 B 或者反之就不会带来额外的耦合度。这就是表 12-4 内容的核心思想——不增加新的耦合。
详细设计中选择控制风格:
- 解除界面与逻辑对象的直接耦合。
高内聚处理
- 软件体系结构的分层设计中:
- 三个层次都是高内聚的,一个处理交互任务, 一个处理业务逻辑,一个处理数据持久化。
- 软件体系结构的逻辑包设计中:
- 将三个层次进一步划分为更小的包,可以实现每个更小的包都是高内聚的。
- 详细设计中抽象类的职责:
- 要求状态与方法紧密联系就是为了达到高内聚(信息内聚)。
- 详细设计中使用控制风格:
- 控制风格分离了控制逻辑,可以实现业务逻辑对象的高内聚(功能内聚)。因为封装了控制逻辑,所以控制器对象承载了不可避免的顺序内聚、通信内聚和逻辑内聚,这就要求控制器对象必须是受控的,也是它们为什么倾向于对外委托而不是自己进行业务计算的原因。
结构化的信息隐藏
Information 信息
- 信息隐藏
- 什么需要隐藏?变更
- 数据表示
- 设备的属性(必需属性除外)
- 实施世界模型
- 支持政策的机制
- 信息隐藏的核心是将每个模块都隐藏一个重要的设计决策
最有可能修改的设计领域
- 硬件依赖:外部软件系统
- 输入输出格式:DB, Internet, UI, …
- 非标准语言特性和依赖路径:Platform:os, middleware, framework…
- 困难的设计和实现部分
- 通常是被设计的很不好的部分或者是需要被重新设计和重新实现的部分
- Complex…, monitor, exception, log, …
- 复杂的数据结构,被多个类使用的数据结构,或者是不让你满意的数据结构
- Separate model from logic 逻辑分块
- 复杂逻辑,他们被认为就像数据结构一样复杂。
- 算法,时间表,时间紧迫,实现紧迫
- 全局变量:常常并没有并真正使用,但是常常受益于权限访问的隐藏:Data Access Routines
- 数组声明的数据量打下和循环限制。
- 以及商业规则:比如法律、规则、正则和被转换成计算机系统的过程。
隐藏
- 尝试本地化未来的变化
- 隐藏可能会独立更改的系统详细信息
- 可能具有不同变化率的独立零件
- 暴露接口的假设不太可能改变
- 容易变化的隐藏,不容易变化的展示
信息隐藏
- 最常见的隐藏是您认为可能会更改的设计决策。
- 然后,您可以通过将每个设计秘密分配给自己的类,子例程或其他设计单元来分离它们。
- 接下来,您隔离(封装)每个机密,这样,如果它确实发生了更改,则更改不会影响程序的其余部分。
接口与实现
- 模块的用户和实施者对此有不同的看法。
- 界面:用户对模块的看法。
- 仅描述用户使用模块需要知道的内容
- 使其更易于理解和使用
- 描述了该模块提供的服务,但没有描述如何提供这些服务
什么是接口
- 作为合同的接口:模块发布的任何内容
- 提供的接口:模块的客户端可以依赖于所需的接口:该模块可以依赖于其他模块
- 语法接口
- 如何调用操作
- 操作签名列表
- 有时也是有效的呼叫操作命令
- 如何调用操作
- 语义接口
- 这些操作是做什么的,例如
- 前置条件和后置条件
- 用例
- 这些操作是做什么的,例如
进一步原则
- 显式接口:使模块之间的所有依赖关系明确(无隐藏的耦合)
- 低耦合-接口少:最小化模块之间的依赖性
- 小接口
- 保障小接口
- 将许多参数组合到结构/对象中
- 将大型接口分为几个接口
- 面向接口编程
- 保障小接口
- 高内聚:一个模块应该封装一些定义良好的,连贯的功能(稍后会详细介绍)
耦合与内聚
- 内聚力是模块之间的一致性的度量。
- 耦合是模块之间的交互程度。
- 您需要高凝聚力和低耦合。
Module Guide
模块的主要秘密
- 主要秘密描述的是这个模块所要实现的用户需求。是设计者对用户需求的实现的一次职责分配。有了这个描述以后,我们可以利用它检查我们是否完成所有的用户需求,还可以利用它和需求优先级来决定开发的次序。
模块的次要秘密
- 次要秘密描述的是这个模块在实现职责时候所涉及的具体的实现细节。包括数据结构,算法,硬件平台等信息。
模块的角色
- 描述了独立的模块在整个系统中所承担的角色,所起的作用。以及与哪些模块有相关联的关系。
模块的对外接口
- 模块提供给别的模块的接口。
循环位移模块的模块说明
复杂系统的快速示例
- A-7E 飞机
- 极其复杂的机上飞行计划
- 内存有限
- 实时约束
Parnas’s Experience
- "当我们试图在没有指南的情况下工作时,……责任要么以两个模块要么没有。 有了模块指南,设计的进一步进展显示出相对较少的疏忽。"
- 集成测试仅用了一周
- 仅发现了 9 个错误。
- 错误隔离到单个模块的位置。
- 所有的 bug 都被很快的修复了
思想的应用(信息隐藏处理)
在软件体系结构设计的分层设计中:
- 经验表明软件系统的界面是最经常变化的, 其次是业务逻辑,最稳定的是业务数据。这就是分层风格建立 Prensentation、 Logic 和 Model 三个层次的原因,它们体现了决策变化的划分类型,它们之间的依赖关系符合各自的稳定性。
在软件体系结构设计的物理包设计中:
- 消除重复可以避免重复耦合,同时可以避免同一个设计决策出现在多个地方—— 这意味着该决策没有被真正地隐藏(这也是控制耦合比数据耦合差的原因)。
在软件体系结构设计的物理包设计中:
- 建立独立的安全包、通信包和数据库连接包,是为了封装各自的设计决策——安全处理、 网络通信与数据库处理。
在软件体系结构设计与详细设计中:
- 严格要求定义模块与类的接口,可以便利开发,更是为了实现信息隐藏。
在详细设计中使用控制风格:
- 专门用控制器对象封装关于业务逻辑的设计决策, 而不是将其拆散分布到整个对象网络中去。
内聚的例子
- 内聚程度从低到高如下
偶然内聚
- 模块执行多个完全不相关的操作
逻辑内聚
- 模块执行一系列相关操作,每个操作的调用由其他模块来决定
时间内聚
- 模块执行一系列与时间有关的操作
过程内聚
- 模块执行一些与步骤顺序有关的操作
通信内聚
- 模块执行一系列与步骤有关的操作,并且这些操作在相同的数据上进行
- 和过程内聚的唯一不同:是否在同一块数据结构上操作
功能内聚
- 模块只执行一个操作或达到一个单一目的
信息内聚
- 模块进行许多操作,各个都有各自的入口点
- 每个操作的代码都相互独立
- 所有操作都在相同的数据结构上完成
耦合的例子
- 耦合程度从低到高如下
数据耦合
- 两个模块的所有参数是同类型的数据项
印记耦合
- 共享一个数据结构,但是却只用了其中一部分
- 下面传递 d.month 就行
- 传递 i.date.month 就行
- 其实只需要 emailID 即可
控制耦合
- 一个模块给另一个模块传递控制信息
重复耦合
- 模块之间有同样(逻辑上同样)的重复代码
公共耦合
- 模块之间共享全局的数据(全局变量,全局数据结构)
内容耦合
- 一个模块直接修改或者依赖于另一个模块的内容
- 直接修改了 date 部分的 month 的值
- 直接修改了另一个模块对象的成员变量的值
KWIC 的四种不同实现
- 复习本地的四种实现的 Java 代码
主程序和子程序(MS)
- 使用全局变量,减少过多的参数传递
面向对象(OO)
- 中间有一些对象进行存储
- 对象写作
Pipe Filter
- 数据进来通过过滤器
- 通过管道串联过滤器
- 好处:
- 如果有一个过滤器无法使用了直接替换就行
- 增加是容易的
- 各自过滤器是独立工作的,不在意其他人怎么样
- 管道不一定是方法调用(独立进程),要不和结构化没有区别
- 4 个线程各自跑起来,用 pipe 连接起来
基于事件
- 有一个事件机制,有订阅者和分发者
- GUI:点击 Button 之后,触发事件
- 框架帮你完成事件机制
- 明确什么监听什么
- 观察者模式