文章目录
模块化与信息隐藏思想
动机
好的软件指的是什么?
Parnas 1972
- 可管理、灵活、 可理解
- 特征
模块之间关联少,模块不太依赖别的模块
模块的独立替换、装配,不会波及到整个系统
Stevens 1974
- 简洁性(Simplicity):易于调试,易于分解
- 可观察性(Observability):易于修改
Beohm 1976
可维护,可扩展,可理解,可重用
发展
概述
- 软件工程整体发展背景
1960年代:“软件不是硬件”的观点
1970年代:“软件 = 数据 + 算法”的观点,瀑布过程模型,形式化方法
1980年代:重用的概念,对象的概念,人件的概念 - 模块化与信息隐藏思想的历史发展
萌芽期:Wirth 1971年论文;Parnas 1972年论文
形成期:Stevens 1974年论文;Parnas 1978年论文;Parnas 1985年论文
发展期:Eder 1992年论文;Hitz 1995年论文
反思:McConnell 1996年论文;Demarco 2002年论文
注意:其中,萌芽期与形成期的成果主要是在结构化范式方面的思考;后面的主要是在面向对象方面的研究
Wirth 1971
- 程序的优化与数据结构并行
- 程序模块化:决定了程序可以应对未来的修改和扩展
- 每一步进化都意味着一些列新的设计决策
- 小心编程不容易做到
Parnas 1972
- 什么是模块化
模块化不仅仅是将程序分成各个子程序,是职责的分配(将系统分成相对独立的模块)
如何分配职责也是设计决策
模块化设计决策层次高于模块的内部实现 - 如何进行模块化
如何进行模块化(分配模块)在于要看模块隐藏了什么信息;根据要隐藏的信息来模块化是模块化的指导原则 - 模块或程序之间存在的关系(将模块联系起来的关系)主要是使用和依赖
好的封装将处理控制的逻辑藏在模块内,其他人并不需要知道,只知道如何调度即可
Stevens 1974
- 结构化设计的奠基文章
- 首次阐述了“耦合”:模块之间联系强度的度量
- 定义了模块和内聚,首次提出耦合
- 结构化设计的流程
Step 1: 描述问题功能
Step 2: 识别交互的数据流
Step 3: 识别哪里是输入、处理、输出(参考数据流图像结构图转化的过程)
Step 4: 将数据流图转化为结构图,确定各模块间的信息传递(出入)
A为控制模块,BCD分别为输入、处理、输出模块
Step 5: 具体地对每个模块做的事进行描述(如何调用、需要哪个模块),对模块之间的关系进行细化和描述
- 讨论了两个概念
模块的控制范围:模块们之间以树状结构的形式组装起来,每个模块的子树就是其控制范围;如果要改动什么东西,在其控制范围内的改动是可以的;超出控制范围则会有麻烦(如B,C下都有改动就超出了B的控制范围)
模块的影响范围:决策影响在控制范围之内的系统是更简单的
Parnas 1978
- 模块之间的使用关联:什么时候A用B
A 使用 B如果B的执行对A完成自己的任务来说是必要的话
当满足下列所有条件后,允许A使用B
A因为使用B本质上变得简单
B因为不使用A免于变得相对复杂
有包含B而不需要A的子集
没有包含A而不包含B的子集
Parnas 1985
- 这篇论文类似于做项目之后的报告
- 归功于Module Guide:是对每个模块的说明,包括了主要、次要秘密、角色、分配模块职责时的条件(当时系统设计是和硬件一起的)
- 不同层次的Module,分别论述每层要解决的问题:一层、二层、三层……分解
- 秘密
主要秘密:模块内隐藏的信息(如潜在变更)
次要秘密:实现模块的实现决策 - 最顶层分解
屏蔽硬件细节的模块
Behavior-Hiding Module
包含软件设计决策的模块 - 意义:1985年文章之后,对结构化中的内聚、耦合、如何做模块化和信息隐藏等问题有了充分阐述
Eder 1992
- 面向对象设计中的内聚和耦合
- 对内聚与耦合的定性分析
Hitz 1995
- 对内聚与耦合进行定量分析
- 两类耦合:类层次的耦合和对象层次的耦合
McConnell 1996
- 提醒大家一个被忽视的武器:信息隐藏
- 信息隐藏
是一个基础思想,不依赖编程范式或设计、方法等
是模块内隐藏的秘密(常见的秘密就是可能发生变更等设计决策)
Demarco 2002
总结了1975年时人们的想法与2001年人们的想法,反思观念的转变
模块化与信息隐藏
模块化
概述
- 系统不是一个单体,一定是由很多交互模块组成的
- 模块化常是实现便宜、高质量软件的关键
- 系统的设计要决定
模块是什么
模块之间如何交互
模块是什么
- David Parnas: 模块是由一系列编程单元(如类、程序等)组成的工作单元
不是简单的一团代码 - 模块应该定义出良好的接口和目标,可以独立地分配给一个工程师
对外交互依赖接口,接口定义好之后就可以做分配了
为什么要对系统开发使用模块化
- 便于管理,实现分而治之
- 从演化角度,如有修改,模块化的系统容易修改
如果有变更,系统若分成小模块们(解耦效果好的),演化比较方便,容易将需求分配到模块上 - 容易理解
根据信息隐藏来做模块化
信息隐藏
信息
- 信息中有秘密
- 秘密就是容易变更的东西(设计决策等),如
数据的展示
对象的性质
模型的实现机制 - 设计领域中容易发生变更的(需要隐藏起来封装使用的)
硬件依赖(如外部软件系统)
输入输出格式(DB,Internet,UI, ……)
为标准化的语言特征和库
复杂的设计和实现领域
复杂的数据结构
复杂的逻辑
全局变量相关
数据大小限制
业务规则
隐藏
- 隐藏的东西是能预测到的变更
相对独立的变更
变化频率不同的要分开(如操作系统API:变得慢;某搜索程序:变得快——分开)
接口暴露出来,尽量使接口不变 - 我们希望接口不变,不意味着他一定不变(接口设计也与经验有关)
- 因为设计决策能影响到的应是可预测到的变更范围内的(超出预测的变更很难保证接口不变化)
信息隐藏
- 最常见秘密是你认为可能变化的设计决策
- 将变更隔离开,每个secret在一个模块里,变的时候自己变不影响到别人
- 接口与实现
用户视角:使用接口
用户要知道如何使用接口
用户说明提高了系统的易用性
接口:要什么给什么
只描述提供服务,不描述如何提供服务
实现者视角:
接口是合约、协议;供接口:给别人用的,需接口:用别人的
语法上:知道如何调用,如何定义(签名)
语义上:按照协议来设计(操作的前后置条件为何、用例为何等) - 接口设计的其他原则
显式接口:让模块之间的依赖是显式的(没有隐藏耦合)
低耦合——最小化模块之间的依赖
将接口分成小接口——接口分的小,减少依赖
高内聚 - 内聚与耦合
内聚度量模块(类、代码等)内部的pieces之间的连接特性
耦合度量模块之间交互强度
希望实现高内聚、低耦合
KWIC案例
- KWIC:
- KWIC举例
- KWIC机制说明
- KWIC举例
- 第一种模块化:主程序、子程序风格;根据功能与步骤
这种模式下循环位移算法的实现
- 第二种模块化:应用了信息隐藏
考虑到如果保存方式发生变更——就将保存方式抽象出来封装使用
Circular Shifter的定义
如果LineStorage里的存储形式发生变化,接口没变的话(即改变只在类内部)变的是接口实现,这样依赖该接口的CircularShifter没有发生变化——这就是按照设计决策、信息隐藏分模块的好处 - 两种分解方式的评价
第一种:按照做的流程和步骤划分操作,由于相互依赖全局变量带来的问题,不好
第二种:根据信息隐藏,每个模块把自己要保存信息封装在模块内部,与其他模块隔离,通过接口与对方有少量交集 - 两种方式的可修改性比较
- 两种方式的过程
第一种:先讨论全局变量之后才能平行开发(全局变量出现错误还不方便调试)
第二种:交互接口考虑好之后就可以并行开发;按接口编程 - 一些总结
每个模块是职责的划分,不仅仅是子程序
职责就是设计决策,这是不让别的模块知道的隐藏的秘密
实现灵活性
尽量不用操作步骤划分
模块是各自有职责的、放在一起能够构成完整系统的模块
每个模块相对独立,模块化工作是分解成相对独立的模块
信息隐藏是模块化的依据
封装
封装不仅是语言特性 (get、set方法等),还可以封装是数据与行为在一起,体现共同职责,职责就是信息隐藏的secret;封装做的好,就抽象出职责,让系统隐藏住职责的实现——这样的封装也是高层思想
结构化的模块化
耦合
概述
- 模块化的选择
以CPU内部工作模块化为例
一种模块化方式:按照与或非门来划分
另一种模块化方式:按照功能划分
- 模块之间,模块的划分不仅要考虑每个模块内部,还要考虑到模块之间连接(联系的复杂度衡量:数量、程度)
- 结构化的耦合
结构化编程要考虑的耦合
耦合有多复杂
关联是关于模块本身的还是关于模块内部某个小部分的
关联时中间被传输了什么数据
耦合的强度1: 链接有多复杂
- 还要考虑有没有连接连到了公共模块(连接到公共模块会产生很多问题)
- 原则一:全局变量是危险的
假设两个包,各有N和M个类
如果N模块与M模块相关联,那么会有(N1 + N2) * (M1 + M2)个可能关联;
如果N模块只与M模块部分相关联,即N1个包对应M1个包,N2个包对应M2个包,那么可能的连接就有(N1 * M1 + N2 * M2)个,少很多
结论就是:如果模块连接到了公共变量,而且还不是真正全部都要依赖这个全局变量的话,就凭白增强了模块之间的联系 - 模块连接到全局变量的坏处
与公共部分的联系的错误可能大量传染
不好理解
难以重用
全局变量是危险的 - 原则二:显式一点
这是同一个行为的两种表示
这就涉及到可修改性与显式显示的折衷
选择操作属性——更加显式一些
选择操作更明确的东西(如类、字典等)——更加可修改、更灵活
如果对可修改性的要求没有那么高,通常选择偏向显式的 - 原则三:不要重复
例子是打印发票,上面是ascii格式的发票,下面是html格式的发票
改进方法是通过接口将重复的部分抽象出来
左侧是不同部分,使用实现的方法;右侧是共同部分代码
耦合的强度2: 连接的是模块本身还是模块内部
- 连接到模块本身的连接的耦合性比连接到模块内部的耦合性小
左边的是连接到模块内部,右边的是连接到模块本身
理想状态:所有的关系都建立直接连接,通过I/O讲清楚
不连接到内部,耦合会弱一点 - 原则四:按接口编程
好的定义的系统,内部用定义好的接口连接传递参数
不要内部,内部应该是被藏起来的
耦合强度3: 传递的是什么
- 通过连接上传递的是什么数据,做出几种对耦合的分类(本表从上往下耦合性依次降低,最下面的是最好的)
类型 | 解释 | 例子 |
---|---|---|
内容耦合 | 一个模块直接修改或者依赖于另一个模块的内容 | 程序跳转GOTO;某些语言机制支持直接更改另一个模块的代码;改变另一个模块的内部数据 |
公共耦合 | 模块之间共享全局的数据 | 全局变量 |
重复耦合 | 模块之间有同样逻辑的重复代码 | 逻辑代码被复制到两个地方 |
控制耦合 | 一个模块给另一个模块传递控制信息 | 传递:显示星期天;传递模块和接受模块必须共享一个共享的内部结构和逻辑(这是增加耦合的原因之一) |
印记耦合 | 共享一个数据结构,但是却只用了其中一部分 | 传递了整个记录给另一个模块,另一个模块只需要一个字段 |
数据耦合 | 两个模块的所有参数是同类型的数据项(用什么数据传什么数据) | 传递一个整数给一个计算平方根的函数 |
- 控制耦合传递的控制信号是一个flag
- 从控制耦合转化到数据耦合
控制耦合
简化耦合
将两件事分开,由控制者自己调用2件事,只有data耦合
转换举例:switch结构里,case后自己做事和case后调用方法,在方法里做事;委托做事的被调用方法就是图中的GETPCOMM
内聚
- 实现独立模块的方法
减少不同模块之间关系
增加同一模块对应关系 - 对内聚性的度量、分类(本表从上到下耦合性依次增高,最下面的最好)
类型 | 解释 | 例子 |
---|---|---|
偶然内聚 | 模块执行多个完全不相关的操作 | 把下列方法放在一个模块中:修车、烤面包、遛狗、看电影 |
逻辑内聚 | 模块执行一系列相关操作,每个操作的调用有其他模块来决定 | 把下列方法放在一个模块中:开车去、坐火车去、坐飞机去 |
时间内聚 | 模块执行一系列与时间有关的操作 | 把下列放在一个模块中:起床、刷牙、洗脸、吃早餐 |
过程内聚 | 模块执行一些与步骤顺序有关的操作 | 把下列方法放在一个模块中:守门员传球给后卫、后卫传球给中场球员、中场球员传球给前锋、前锋射门 |
通信内聚 | 模块执行一些与步骤有关的操作,并且这些操作在相同的数据上进行 | 把下列方法放在一个模块中:查书名、查书作者、查书出版商 |
功能内聚 | 模块只执行一个操作或达到一个单一目的 | 下列内容都作为独立模块:计算平方根、决定最短路径、压缩数据 |
信息内聚 | 模块进行许多操作,个个都有各自的入口点,每个操作的代码相对独立,而且所有操作都在相同的数据结构上完成 | 比如数据结构中的栈,它包含相应数据和操作;所有操作都是针对相同的数据结构 |
模块化思想的应用
低耦合处理
- 软件体系结构的分层设计中:不同层的模块之间仅仅能够通过程序调用与数据传递实现交互,不能共享数据(例如Data层建立一个数据对象并将引用传递给Logic层使用)否则会导致公共耦合(正确方法是传递数据实体(复制出来的))
- 软件体系结构的逻辑包设计中:依据功能的特点将三个层次进一步划分为更小的包,而不是只使用Presentation、Logic和Model三个包,可以通过包分割实现接口最小化,这能去除不必要的耦合
- 软件体系结构的物理包设计中:将不同包的重复内容独立为单独的包以消除重复,避免产生隐式的重复耦合
- 详细设计中对象创建者的选择:如果两个对象A和B之间已经有较高的耦合度了,那么使用A创建B或者反之就不会带来额外的耦合度——不增加新的耦合
- 详细设计中选择控制风格:解除界面与逻辑对象的直接耦合(通过控制器与接口)
高内聚处理
- 软件体系结构的分层设计中:三个层次都是高内聚的,一个处理交互任务,一个处理业务逻辑,一个处理数据持久化
- 软件体系结构的逻辑包设计中:将三个层次进一步划分为更小的包,可以实现每个更小的包都是高内聚的
- 详细设计中抽象类的职责:要求状态与方法紧密联系就是为了达到高内聚(信息内聚)
- 详细设计中使用控制风格:控制风格分离了控制逻辑,可以实现业务逻辑对象的高内聚(功能内聚);因为封装了控制逻辑,所以控制器对象承载了不可避免的顺序内聚、通信内聚和逻辑内聚,这就要求控制器对象必须是受控的,也是他们为什么倾向于对外委托而不是自己进行业务计算的原因(将控制部分拿出去,做委托,不去管怎么做,每个人只做自己的,很多事是委托的,很多控制部分分散开来)
结构化的信息隐藏
Module Guide
- 主要内容
模块的主要秘密:描述这个模块所要实现的用户需求,是设计者对用户需求的实现的一次职责分配
模块的次要秘密:描述这个模块在实现职责的时候所涉及的具体实现细节
模块的角色:描述了独立的模块在整个系统中所承担的角色,所起的作用,以及与哪些模块有相关联的关系
模块的对外接口:模块提供给别的模块的接口 - 循环位移模块的模块说明
- Module Guide能显著提升开发效率
信息隐藏思想的应用
- 在软件体系结构设计的分层设计中:每层各自职责
- 在软件体系结构设计的物理包设计中:消除重复可避免重复耦合,同时意味着可以避免同一个设计决策出现在多个地方——这意味着该决策没有被真正地隐藏
- 在软件体系结构设计的物理包设计中:建立独立的安全包、通信包、数据库连接包,是为了封装各自的设计决策——安全处理、网络通信与数据库处理
- 在软件体系结构与详细设计中:严格要求定义模块与类的接口,可以便利开发,更是为了实现信息隐藏
- 在详细设计中使用控制风格:专门用控制器对象封装关于业务逻辑设计决策,而不是将其拆散分布到整个网络对象中去