[课业] 22 | 软工 | 模块化与信息隐藏

模块化与信息隐藏思想

动机

好的软件指的是什么?

Parnas 1972

  1. 可管理、灵活、 可理解
  2. 特征
    模块之间关联少,模块不太依赖别的模块
    模块的独立替换、装配,不会波及到整个系统

Stevens 1974

  1. 简洁性(Simplicity):易于调试,易于分解
  2. 可观察性(Observability):易于修改

Beohm 1976

可维护,可扩展,可理解,可重用

发展

概述

  1. 软件工程整体发展背景
    1960年代:“软件不是硬件”的观点
    1970年代:“软件 = 数据 + 算法”的观点,瀑布过程模型,形式化方法
    1980年代:重用的概念,对象的概念,人件的概念
  2. 模块化与信息隐藏思想的历史发展
    萌芽期:Wirth 1971年论文;Parnas 1972年论文
    形成期:Stevens 1974年论文;Parnas 1978年论文;Parnas 1985年论文
    发展期:Eder 1992年论文;Hitz 1995年论文
    反思:McConnell 1996年论文;Demarco 2002年论文
    注意:其中,萌芽期与形成期的成果主要是在结构化范式方面的思考;后面的主要是在面向对象方面的研究

Wirth 1971

  1. 程序的优化与数据结构并行
  2. 程序模块化:决定了程序可以应对未来的修改和扩展
  3. 每一步进化都意味着一些列新的设计决策
  4. 小心编程不容易做到

Parnas 1972

  1. 什么是模块化
    模块化不仅仅是将程序分成各个子程序,是职责的分配(将系统分成相对独立的模块)
    如何分配职责也是设计决策
    模块化设计决策层次高于模块的内部实现
  2. 如何进行模块化
    如何进行模块化(分配模块)在于要看模块隐藏了什么信息;根据要隐藏的信息来模块化是模块化的指导原则
  3. 模块或程序之间存在的关系(将模块联系起来的关系)主要是使用依赖
    好的封装将处理控制的逻辑藏在模块内,其他人并不需要知道,只知道如何调度即可

Stevens 1974

  1. 结构化设计的奠基文章
  2. 首次阐述了“耦合”:模块之间联系强度的度量
  3. 定义了模块和内聚,首次提出耦合
  4. 结构化设计的流程
    Step 1: 描述问题功能

    Step 2: 识别交互的数据流
    Step 3: 识别哪里是输入、处理、输出(参考数据流图像结构图转化的过程)

    Step 4: 将数据流图转化为结构图,确定各模块间的信息传递(出入)

    A为控制模块,BCD分别为输入、处理、输出模块
    Step 5: 具体地对每个模块做的事进行描述(如何调用、需要哪个模块),对模块之间的关系进行细化和描述
  5. 讨论了两个概念
    模块的控制范围:模块们之间以树状结构的形式组装起来,每个模块的子树就是其控制范围;如果要改动什么东西,在其控制范围内的改动是可以的;超出控制范围则会有麻烦(如B,C下都有改动就超出了B的控制范围)
    模块的影响范围:决策影响在控制范围之内的系统是更简单的

Parnas 1978

  1. 模块之间的使用关联:什么时候A用B
    A 使用 B如果B的执行对A完成自己的任务来说是必要的话
    当满足下列所有条件后,允许A使用B
    A因为使用B本质上变得简单
    B因为不使用A免于变得相对复杂
    有包含B而不需要A的子集
    没有包含A而不包含B的子集

Parnas 1985

  1. 这篇论文类似于做项目之后的报告
  2. 归功于Module Guide:是对每个模块的说明,包括了主要、次要秘密、角色、分配模块职责时的条件(当时系统设计是和硬件一起的)
  3. 不同层次的Module,分别论述每层要解决的问题:一层、二层、三层……分解
  4. 秘密
    主要秘密:模块内隐藏的信息(如潜在变更)
    次要秘密:实现模块的实现决策
  5. 最顶层分解
    屏蔽硬件细节的模块
    Behavior-Hiding Module
    包含软件设计决策的模块
  6. 意义:1985年文章之后,对结构化中的内聚、耦合、如何做模块化和信息隐藏等问题有了充分阐述

Eder 1992

  1. 面向对象设计中的内聚和耦合
  2. 对内聚与耦合的定性分析

Hitz 1995

  1. 对内聚与耦合进行定量分析
  2. 两类耦合:类层次的耦合和对象层次的耦合

McConnell 1996

  1. 提醒大家一个被忽视的武器:信息隐藏
  2. 信息隐藏
    是一个基础思想,不依赖编程范式或设计、方法等
    是模块内隐藏的秘密(常见的秘密就是可能发生变更等设计决策)

Demarco 2002

总结了1975年时人们的想法与2001年人们的想法,反思观念的转变

模块化与信息隐藏

模块化

概述
  1. 系统不是一个单体,一定是由很多交互模块组成的
  2. 模块化常是实现便宜、高质量软件的关键
  3. 系统的设计要决定
    模块是什么
    模块之间如何交互
模块是什么
  1. David Parnas: 模块是由一系列编程单元(如类、程序等)组成的工作单元
    不是简单的一团代码
  2. 模块应该定义出良好的接口和目标可以独立地分配给一个工程师
    对外交互依赖接口,接口定义好之后就可以做分配了
为什么要对系统开发使用模块化
  1. 便于管理,实现分而治之
  2. 从演化角度,如有修改,模块化的系统容易修改
    如果有变更,系统若分成小模块们(解耦效果好的),演化比较方便,容易将需求分配到模块上
  3. 容易理解

根据信息隐藏来做模块化

信息隐藏

信息
  1. 信息中有秘密
  2. 秘密就是容易变更的东西(设计决策等),如
    数据的展示
    对象的性质
    模型的实现机制
  3. 设计领域中容易发生变更的(需要隐藏起来封装使用的)
    硬件依赖(如外部软件系统)
    输入输出格式(DB,Internet,UI, ……)
    为标准化的语言特征和库
    复杂的设计和实现领域
    复杂的数据结构
    复杂的逻辑
    全局变量相关
    数据大小限制
    业务规则
隐藏
  1. 隐藏的东西是能预测到的变更
    相对独立的变更
    变化频率不同的要分开(如操作系统API:变得慢;某搜索程序:变得快——分开)
    接口暴露出来,尽量使接口不变
  2. 我们希望接口不变,不意味着他一定不变(接口设计也与经验有关)
  3. 因为设计决策能影响到的应是可预测到的变更范围内的(超出预测的变更很难保证接口不变化)
信息隐藏
  1. 最常见秘密是你认为可能变化的设计决策
  2. 将变更隔离开,每个secret在一个模块里,变的时候自己变不影响到别人
  3. 接口与实现
    用户视角:使用接口
    用户要知道如何使用接口
    用户说明提高了系统的易用性
    接口:要什么给什么
    只描述提供服务,不描述如何提供服务
    实现者视角:
    接口是合约、协议;供接口:给别人用的,需接口:用别人的
    语法上:知道如何调用,如何定义(签名)
    语义上:按照协议来设计(操作的前后置条件为何、用例为何等)
  4. 接口设计的其他原则
    显式接口:让模块之间的依赖是显式的(没有隐藏耦合)
    低耦合——最小化模块之间的依赖
    将接口分成小接口——接口分的小,减少依赖
    高内聚
  5. 内聚与耦合
    内聚度量模块(类、代码等)内部的pieces之间的连接特性
    耦合度量模块之间交互强度
    希望实现高内聚、低耦合

KWIC案例

  1. KWIC:
  2. KWIC举例
  3. KWIC机制说明
  4. KWIC举例
  5. 第一种模块化:主程序、子程序风格;根据功能与步骤

    这种模式下循环位移算法的实现
  6. 第二种模块化:应用了信息隐藏
    考虑到如果保存方式发生变更——就将保存方式抽象出来封装使用
    Circular Shifter的定义


    如果LineStorage里的存储形式发生变化,接口没变的话(即改变只在类内部)变的是接口实现,这样依赖该接口的CircularShifter没有发生变化——这就是按照设计决策、信息隐藏分模块的好处
  7. 两种分解方式的评价
    第一种:按照做的流程和步骤划分操作,由于相互依赖全局变量带来的问题,不好
    第二种:根据信息隐藏,每个模块把自己要保存信息封装在模块内部,与其他模块隔离,通过接口与对方有少量交集
  8. 两种方式的可修改性比较
  9. 两种方式的过程
    第一种:先讨论全局变量之后才能平行开发(全局变量出现错误还不方便调试)
    第二种:交互接口考虑好之后就可以并行开发;按接口编程
  10. 一些总结
    每个模块是职责的划分,不仅仅是子程序
    职责就是设计决策,这是不让别的模块知道的隐藏的秘密
    实现灵活性
    尽量不用操作步骤划分
    模块是各自有职责的、放在一起能够构成完整系统的模块
    每个模块相对独立,模块化工作是分解成相对独立的模块
    信息隐藏是模块化的依据
    封装
        封装不仅是语言特性 (get、set方法等),还可以封装是数据与行为在一起,体现共同职责,职责就是信息隐藏的secret;封装做的好,就抽象出职责,让系统隐藏住职责的实现——这样的封装也是高层思想

结构化的模块化

耦合

概述

  1. 模块化的选择
    以CPU内部工作模块化为例

    一种模块化方式:按照与或非门来划分

    另一种模块化方式:按照功能划分
  2. 模块之间,模块的划分不仅要考虑每个模块内部,还要考虑到模块之间连接(联系的复杂度衡量:数量、程度)
  3. 结构化的耦合
    结构化编程要考虑的耦合
    耦合有多复杂
    关联是关于模块本身的还是关于模块内部某个小部分的
    关联时中间被传输了什么数据

耦合的强度1: 链接有多复杂

  1. 还要考虑有没有连接连到了公共模块(连接到公共模块会产生很多问题)
  2. 原则一全局变量是危险的
    假设两个包,各有N和M个类
    如果N模块与M模块相关联,那么会有(N1 + N2) * (M1 + M2)个可能关联;
    如果N模块只与M模块部分相关联,即N1个包对应M1个包,N2个包对应M2个包,那么可能的连接就有(N1 * M1 + N2 * M2)个,少很多
    结论就是:如果模块连接到了公共变量,而且还不是真正全部都要依赖这个全局变量的话,就凭白增强了模块之间的联系
  3. 模块连接到全局变量的坏处
    与公共部分的联系的错误可能大量传染
    不好理解
    难以重用
    全局变量是危险的
  4. 原则二显式一点
    这是同一个行为的两种表示


    这就涉及到可修改性与显式显示的折衷
    选择操作属性——更加显式一些
    选择操作更明确的东西(如类、字典等)——更加可修改、更灵活
    如果对可修改性的要求没有那么高,通常选择偏向显式的
  5. 原则三:不要重复
    例子是打印发票,上面是ascii格式的发票,下面是html格式的发票

    改进方法是通过接口将重复的部分抽象出来

    左侧是不同部分,使用实现的方法;右侧是共同部分代码

耦合的强度2: 连接的是模块本身还是模块内部

  1. 连接到模块本身的连接的耦合性比连接到模块内部的耦合性小

    左边的是连接到模块内部,右边的是连接到模块本身
    理想状态:所有的关系都建立直接连接,通过I/O讲清楚
    不连接到内部,耦合会弱一点
  2. 原则四:按接口编程
    好的定义的系统,内部用定义好的接口连接传递参数
    不要内部,内部应该是被藏起来的

耦合强度3: 传递的是什么

  1. 通过连接上传递的是什么数据,做出几种对耦合的分类(本表从上往下耦合性依次降低,最下面的是最好的)
类型解释例子
内容耦合一个模块直接修改或者依赖于另一个模块的内容程序跳转GOTO;某些语言机制支持直接更改另一个模块的代码;改变另一个模块的内部数据
公共耦合模块之间共享全局的数据全局变量
重复耦合模块之间有同样逻辑的重复代码逻辑代码被复制到两个地方
控制耦合一个模块给另一个模块传递控制信息传递:显示星期天;传递模块和接受模块必须共享一个共享的内部结构和逻辑(这是增加耦合的原因之一)
印记耦合共享一个数据结构,但是却只用了其中一部分传递了整个记录给另一个模块,另一个模块只需要一个字段
数据耦合两个模块的所有参数是同类型的数据项(用什么数据传什么数据)传递一个整数给一个计算平方根的函数
  1. 控制耦合传递的控制信号是一个flag
  2. 从控制耦合转化到数据耦合
    控制耦合

    简化耦合

    将两件事分开,由控制者自己调用2件事,只有data耦合
    转换举例:switch结构里,case后自己做事和case后调用方法,在方法里做事;委托做事的被调用方法就是图中的GETPCOMM

内聚

  1. 实现独立模块的方法
    减少不同模块之间关系
    增加同一模块对应关系
  2. 对内聚性的度量、分类(本表从上到下耦合性依次增高,最下面的最好)
类型解释例子
偶然内聚模块执行多个完全不相关的操作把下列方法放在一个模块中:修车、烤面包、遛狗、看电影
逻辑内聚模块执行一系列相关操作,每个操作的调用有其他模块来决定把下列方法放在一个模块中:开车去、坐火车去、坐飞机去
时间内聚模块执行一系列与时间有关的操作把下列放在一个模块中:起床、刷牙、洗脸、吃早餐
过程内聚模块执行一些与步骤顺序有关的操作把下列方法放在一个模块中:守门员传球给后卫、后卫传球给中场球员、中场球员传球给前锋、前锋射门
通信内聚模块执行一些与步骤有关的操作,并且这些操作在相同的数据上进行把下列方法放在一个模块中:查书名、查书作者、查书出版商
功能内聚模块只执行一个操作或达到一个单一目的下列内容都作为独立模块:计算平方根、决定最短路径、压缩数据
信息内聚模块进行许多操作,个个都有各自的入口点,每个操作的代码相对独立,而且所有操作都在相同的数据结构上完成比如数据结构中的栈,它包含相应数据和操作;所有操作都是针对相同的数据结构

模块化思想的应用

低耦合处理

  1. 软件体系结构的分层设计中:不同层的模块之间仅仅能够通过程序调用与数据传递实现交互,不能共享数据(例如Data层建立一个数据对象并将引用传递给Logic层使用)否则会导致公共耦合(正确方法是传递数据实体(复制出来的))
  2. 软件体系结构的逻辑包设计中:依据功能的特点将三个层次进一步划分为更小的包,而不是只使用Presentation、Logic和Model三个包,可以通过包分割实现接口最小化,这能去除不必要的耦合
  3. 软件体系结构的物理包设计中:将不同包的重复内容独立为单独的包以消除重复,避免产生隐式的重复耦合
  4. 详细设计中对象创建者的选择:如果两个对象A和B之间已经有较高的耦合度了,那么使用A创建B或者反之就不会带来额外的耦合度——不增加新的耦合
  5. 详细设计中选择控制风格:解除界面与逻辑对象的直接耦合(通过控制器与接口)

高内聚处理

  1. 软件体系结构的分层设计中:三个层次都是高内聚的,一个处理交互任务,一个处理业务逻辑,一个处理数据持久化
  2. 软件体系结构的逻辑包设计中:将三个层次进一步划分为更小的包,可以实现每个更小的包都是高内聚的
  3. 详细设计中抽象类的职责:要求状态与方法紧密联系就是为了达到高内聚(信息内聚)
  4. 详细设计中使用控制风格:控制风格分离了控制逻辑,可以实现业务逻辑对象的高内聚(功能内聚);因为封装了控制逻辑,所以控制器对象承载了不可避免的顺序内聚、通信内聚和逻辑内聚,这就要求控制器对象必须是受控的,也是他们为什么倾向于对外委托而不是自己进行业务计算的原因(将控制部分拿出去,做委托,不去管怎么做,每个人只做自己的,很多事是委托的,很多控制部分分散开来)

结构化的信息隐藏

Module Guide

  1. 主要内容
    模块的主要秘密:描述这个模块所要实现的用户需求,是设计者对用户需求的实现的一次职责分配
    模块的次要秘密:描述这个模块在实现职责的时候所涉及的具体实现细节
    模块的角色:描述了独立的模块在整个系统中所承担的角色,所起的作用,以及与哪些模块有相关联的关系
    模块的对外接口:模块提供给别的模块的接口
  2. 循环位移模块的模块说明
  3. Module Guide能显著提升开发效率

信息隐藏思想的应用

  1. 在软件体系结构设计的分层设计中:每层各自职责
  2. 在软件体系结构设计的物理包设计中:消除重复可避免重复耦合,同时意味着可以避免同一个设计决策出现在多个地方——这意味着该决策没有被真正地隐藏
  3. 在软件体系结构设计的物理包设计中:建立独立的安全包、通信包、数据库连接包,是为了封装各自的设计决策——安全处理、网络通信与数据库处理
  4. 在软件体系结构与详细设计中:严格要求定义模块与类的接口,可以便利开发,更是为了实现信息隐藏
  5. 在详细设计中使用控制风格:专门用控制器对象封装关于业务逻辑设计决策,而不是将其拆散分布到整个网络对象中去
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值