SE概论
SE基础
SE定义
- 应用系统的、规范的、可量化的方法来开发、运行和维护软件,即将工程应用到软件
- 对1)中各种方法的研究
科学与工艺的区别
-
科学是运用范畴、定理、定律等思维形式反映现实世界各种现象的本质规律的知识体系。
- 它重在把握事物的规律性,并按照这些固定的规律指导活动顺利和正确进行
-
工艺是在科学王国之外依赖于人类天性和创造性的知识
- 没有什么固定的规律,但是人们在长期的实践活动中却可以发现和总结出一些经验,他们被称为实践方法或原则
软件开发活动
- 主要活动包括:需求开发、软件设计、软件构造、软件测试、软件交付与维护
需求开发
- 主要制品:
- 软件需求规格说明书SRS
- 需求分析模型
软件设计
- 主要制品:
- 软件设计描述SDD文档
- 软件设计模型
软件构造
软件测试
软件交付与维护
软件开发过程
####过程模型
- 重量级过程方法RUP(Rational Unified Process)
- RUP的思想是在一个基本的过程框架下组织和使用最佳实践方法,强调在基本过程框架和最佳实践方法的基础上进行过程定制与裁剪,RUP还拥有全程的软件过程工具支持
- 轻量级过程方法
- 极限编程XP
- 特征驱动开发
- 敏捷agile方法
过程改进
- 为了在评价一个企业软件能力成熟度的基础之上,指导企业改进软件过程,提升过程管理能力,CMU SEI将SW-CMM改进为CMMI
- 五个等级
能力成熟度模型CMM
- CMM有助于建立一个有规律的、成熟的软件过程,CMM策略力图改进软件过程的管理,而在技术上的改进是其必然的结果
- CMM为软件的过程能力提供了一个阶梯式的框架,它基于以往的软件工程的经验教训,提供了一个基于过程改进的框架图
- CMMI
- CMM模型在软件开发中的应用有时会出现问题,形成能力成熟度模型集成CMMI项目是为了解决使用多个模型进行软件开发过程的问题,因此CMMI模型已经取代了CMM模型
- 成熟度的五个等级
- 初始级Initial
- 无序的、甚至是混乱的
- 管理级(可重复级)Repeatable
- 有纪律的
- 定义级Defined
- 标准、一致的过程
- 定量管理级Managed
- 度量的和可预测的
- 优化级Optimizing
- 预防过程和产品缺陷,关注于过程持续改进
- 初始级Initial
软件开发过程模型
软件开发的典型阶段
- 软件开发的典型阶段有
- 软件需求工程
- 软件设计
- 软件构造
- 软件测试
- 软件交付与软件维护
- 软件需求工程可以进一步划分为两个阶段
- 系统需求开发
- 系统需求开发是为了获得整个系统的期望目标(即完成业务需求处理),整个系统包括软件系统、硬件系统和人力配置
- 软件需求开发
- 软件需求开发是以承载的系统需求为出发点,建立软件系统的解决方案,使其满足系统的需求
- 系统需求开发
软件生命周期模型
- 典型的软件生命周期模型是 * 需求工程、软件设计、软件实现、软件测试、软件交付、软件维护
软件过程模型
- 与简略的软件生命周期模型不同,软件过程模型可以被看作是网络化的活动组织
- 软件过程模型是对软件生命周期模型更详细哼准确的描述
构建-修复模型
- 特点
- 没有考虑最基本的生命周期
- 没有对需求真实性、设计结构重量、代码组织质量、质量保障等软件开发的复杂因素进行关注点分解处理
瀑布模型
- 瀑布模型按照软件生命周期模型将软件开发活动组织为需求开发、软件设计、软件实现、软件测试、软件交付和软件维护等基本活动,并规定它们自上而下、相互衔接的次序,按照“从一个阶段到另一个阶段的有序的转换序列”的方式来组织开发活动
- 允许活动出现反复和迭代
- 特点
- 要求每个活动的结果必须要进行验证
- 重视模型与文档
- 文档驱动
- 局限性
- 对文档的期望过高
- 对开发互动的线性顺序假设
- 客户、用户参与不够
- 里程碑粒度过粗
- 要求一个阶段完成之后才进行验证
- 特定情况下使用瀑布模型
- 需求成熟稳定,没有不确定的内容,也不会变化
- 所需技术成熟可靠,没有不确定的技术难点
- 复杂度适中,不至于产生太大的文档负担和过粗的里程碑
增量迭代模型
- 迭代式、渐进交付和并行开发共同促使了增量迭代模型的产生和普及
- 增量迭代模型在项目开始时,通过系统需求开发和核心体系结构设计活动完成项目对前景和范围的界定,然后再将后续开发活动组织多个迭代、并行的瀑布式开发活动
- 实际开发中,第一个迭代完成的是产品的核心部分,满足基本需求,但是很多附带特性需要后续迭代完成
- 需求驱动的
演化模型
- 与增量迭代模型相比
- 相同点是它们都使用迭代式组织开发活动并且都适合大规模软件开发
- 不同点是增量迭代模型适用于比较成熟、稳定的领域,而演化模型主要用在需求变更比较频繁或不确定性较多的领域
- 初始开发迭代中,主要任务是澄清和明确系统的核心需求,建立和交付核心系统
- 需求驱动的
- 特点
- 使用了迭代式开发
- 并行开发
- 渐进交付
原型模型
螺旋模型
- 基本思想是尽早解决比较高的风险
- 风险驱动的
- 风险解决的基本思路是:
- 确定目标、解决方案和约束
- 评估方案、发现风险
- 寻找风险解决方法
- 落实风险解决方案
- 螺旋模型是风险解决迭代和瀑布模型的综合
- 特点
- 可以降低风险,减少项目因风险造成的损失
- 缺点是由于原型自身带来的风险
Rational统一过程
- RUP(Rational Unified Process)
- 二维坐标描述
- 横轴是时间,是过程展开的生命周期特征,体现开发过程的动态结构
- 纵轴是内容,自然的逻辑活动,体现开发过程的静态结构
- RUP没有使用经典的软件生命周期,而是把软件开发的生命周期定义为
- 初始
- 初始阶段定义项目前景、范围和业务用例
- 细化
- 细化阶段设计软件体系结构、构建核心体系结构原型
- 构造
- 构造阶段完成软件系统的详细设计和实现
- 交付
- 交付阶段将软件产品交付给用户
- 核心实践方法——体现了RUP的基础思想
- 迭代式开发
- 管理需求
- 基于组件
- 可视化建模
- 验证软件质量
- 控制软件变更
- RUP是重量级过程
- 初始
敏捷过程
- 敏捷思想——价值观
- 个体和互动高于流程和工具
- 工作的软件高于详尽的文档
- 客户合作高于合同谈判
- 响应变化高于遵循计划
- 敏捷开发以用户的需求进化为核心,采用迭代、循序渐进的方法进行软件开发
- 在敏捷开发中,软件项目在构建初期被切分成多个子项目,各个子项目的成果都经过测试,具备可视、可集成和可运行使用的特征。
- 换言之,就是把一个大项目划分成多个相互联系,但也可独立运行的小项目,并分别完成,在此过程中软件一直处于可使用状态
项目启动
项目管理基础
软件项目管理
- 软件项目管理包括项目启动、项目计划、项目执行、项目跟踪与控制、项目收尾5个过程组
团队组织与管理
团队定义
- 为了一致的目的、绩效标准、方法而共担责任并且技能互补的少数人
- 特点
- 有共同的目标
- 共担责任
- 技能互补
- 小规模团体
- 有明确的结构
几种典型的团队结构
- 主程序员团队
- 民主团队
- 敏捷开发
- 开放团队
- 黑箱管理方式
团队建设
- 建立团队章程
- 持续成功
- 和谐沟通
- 避免团队杀手
软件质量保障
软件质量
质量属性
- 人们通常会使用选用系统的某些质量要素进行量化处理,建立质量特征,这些特征被称为质量属性
- 为了根据质量属性描述和评价系统的整体质量,人们从很多质量属性的定义中选择一些能够相互配合,相互联系的特征集,被称为质量模型
- 常见的质量模型
- 功能性
- 完备性、正确性、安全性、兼容性、互操作性
- 可靠性
- 无缺陷性、容错性、可用性
- 易用性
- 可理解性、易学习性、可操作性、通信性
- 效率
- 时间经济型、资源经济性
- 可维护性
- 可修正性、扩展性、可测试性
- 可移植性
- 硬件独立性、软件独立性、可安装性、可复用性
- 功能性
质量保障
- 质量验证的方法:评审、测试和质量度量三种
- 主要质量保障活动
里程碑 | 质量保障活动 |
---|---|
需求开发 | 需求评审、需求度量 |
体系结构 | 体系结构评审、集成测试 |
详细设计 | 详细设计评审、设计度量、集成测试 |
实现(构造) | 代码评审、代码度量 |
测试 | 测试、测试度量 |
软件配置管理
- 配置管理定义
- 用技术的和管理的指导和监督方法,来标识和说明配置项的功能和物理特征,控制对这些特征的变更记录和报告变更处理及其实现状态,并验证与需求规格的一致性
- 配置管理通过将软件开发的重要制品及其变更纳入管理和监控,保证了在不影响开发活动协同的情况下有效处理变更
- 配置项
- 需要配置管理的软件开发制品,包括最终制品和中间制品,都成为配置项
- 基线
- 经过了评审和验证,可以作为后续开发工作基础而进入协同工作过程,需要纳入配置管理和执行变更控制的制品称为该配置项的基线
- 基线的建立意味着一个里程碑,标志着生产基线制品活动的成功结束和后续协同开发活动的开始
- IEEE定义:已经经过正式评审的规格说明或制品,可以作为进一步开发的基础,并且只有通过正式的变更控制过程才能变更
- 配置管理活动
- 标识配置项
- 版本管理
- 变更控制
- 配置审计
- 状态报告
- 软件发布管理
需求开发阶段
需求工程基础
- 需求工程定义
- 需求工程就是所有需求处理活动的总和,它收集信息、分析问题、整合观点、记录需求并验证其正确性、最终描述出软件被应用后与其环境互动形成的期望效应
需求工程活动
需求开发过程模型
需求获取
- 目标分析
- 用户需求获取
- 面谈
- 集体获取方法
- 头脑风暴
- 原型
需求分析
- 边界分析
- 系统的用例图和上下文图通常用来定义系统的边界
- 需求建模
需求规格说明
- 定制文档模版
- 编写文档
需求验证
- 需求规格说明文档至少要满足以下几个标准
- 文档内每条需求都正确、准确的反映了用户的意图
- 文档记录的需求集在整体上具有完整性和一致性
- 文档的组织方式和需求的书写方式具有可读性和可修改性
需求基础
需求定义
- 需求就是用户的一种期望,软件系统通过满足用户的期望来解决用户的问题。
- IEEE定义
- 用户为了解决问题或达到某些目标所需要的条件或能力
- 系统或系统部件为了满足合同、标准、规范或其他正式文档所规定的要求而需要具备的条件或能力
- 对1)或2)中的一个条件或一种能力的文档话表述
需求的层次性
- 业务需求
- 系统建立的战略出发点,表达为高层次的目标,它描述了组织为什么要开发系统
- 用户需求
- 执行实际工作的用户对系统所能完成的具体任务的期望,描述了系统能够帮助用户做些什么
- 系统级需求
- 用户对系统行为的期望,每个系统级需求反映了一次外界与系统的交互行为,或系统的一个实现细节
- 一系列的系统级需求联系起来可以满足一个用户需求,帮助用户完成任务从而满足业务需求
- 将用户需求转换成系统级需求的过程就是需求工程中最为重要的需求分析活动
区分需求、问题域与规格说明
- 需求是一种期望,源于现实又高于现实
- 问题域是对现实世界运行规律的一种反映
- 规格说明是软件产品的方案描述,他不是需求但是实现需求,不是问题域但是要与问题域互动
软件需求分类
- 功能需求
- 性能需求
- 速度
- 容量
- 吞吐量
- 负载
- 实时性
- 质量属性
- 可靠性
- 可用性
- 安全性
- 可维护性
- 可移植性
- 易用性
- 对外接口
- 包括用户界面、硬件接口、软件接口、网络通信接口
- 约束
- 编程语言、硬件设施等
- 常见的约束有三类
- 系统开发及运行的环境
- 问题域内的相关标准
- 商业规则
- 数据需求
- 数据需求是需求在数据库、文件或者其他介质中存储的数据描述
软件过程
软件设计
设计基础
软件设计的核心思想
- 分解——在横向上把系统分割成几个简单的子系统
- 抽象——纵向上聚焦子系统的接口,抽象可以分离接口和实现
软件设计的方法
- 结构化设计
- 面向对象设计
- 数据结构为中心设计
- 基于构件的设计
- 形式化设计方法
软件设计的模型
- 静态模型:类图、对象图、构件图、部署图
- 动态模型:交互图(顺序图和通信图)、状态图、活动图
软件设计描述
- 设计视图——基于软件视角
- 逻辑设计视角
- 组合设计视角
- 信息视角
- 接口视角
- 结构视角
- 依赖视角
- 常见的设计视角——上下文、组合、逻辑、依赖、信息、模式、接口、结构、交互等
体系结构视图(视角)
- 4+1视图模型
- 逻辑视图
- 显示了系统中对象和对象类的一些主要抽象
- 通过这些逻辑视图,将系统需求和实体关联起来应该是可能的
- 进程视图
- 显示了运行时系统是如何组织为一组交互的进程
- 这种视图对非功能系统特征的判断是非常有效的,比如性能和可用性
- 开发视图
- 显示了软件是如何为了开发而被分解的,即将软件分解成可以由单独的开发人员或开发团队实现的组件
- 这种视图对软件的管理者和程序员有用
- 物理视图
- 它显示了系统硬件和系统中软件组织是如何分布在处理器上的
- 这种视图对系统工程师规划系统部署是非常有用的
- 4+1模型从逻辑、开发、过程、物理和场景5个不同的视角来描述软件体系结构的全部内容
- 逻辑视角
- 描述系统的功能需求及他们之间的相互关系
- 过程视角
- 过程角度侧重于系统的运行性
- 从系统的行为出发,考察各个构件之间的协作/交互/通讯关系,反映系统的行为结构
- 该视图给予系统的需求场景,同时遵循框架视图的制约,是系统核心结构之一
- 开发视角
- 开发角度负责软件模块的组织和管理
- 该视图以构件为着眼点,是系统开发的核心结构之一,是框架视图的设计模式,是对框架视图的细化
- 物理视角
- 物理角度解决系统的拓扑结构、系统安装及通信问题等
- 描述系统的软件和硬件之间的映射关系并反映其分布特性,展示软件在生命周期的不同阶段中所必须的物理环境或硬件配置以及分布状况
- 场景
- 场景则对应于用户需求或系统功能实例的抽象,设计者通过分析如何满足每个场景所要求的约束来分析软件的体系结构
- 场景是整个体系结构设计的依据
- 逻辑视角
- 逻辑视图
体系结构基础
体系结构
- 定义:部件、连接件、配置;一个软件系统的体系结构规定了系统的计算部件和部件之的交互
- 部件——承载系统的主要功能
- 连接件——定义了部件间的交互
- 配置——定义了部件以及连接件之间的关联方式
体系结构风格
主程序/子程序
- 主要实现机制是模块实现,类似于结构化但基于部件与连接件建立的高层结构
- 模块粒度更粗,在模块内部可以结构化也可面向对象
- 优点
- 结构清晰、易于理解
- 强控制性
- 缺点
- 强耦合,非常依赖于交互方的接口规格,系统难以修改和复用
- 程序调用的连接方式限制了各部件之间的数据交互
面向对象
- 将系统组织为多个独立的对象,每个对象封装内部数据,并基于数据对外提供服务
- 主要实现机制是模块实现
- 优点
- 内部实现的可修改性
- 易开发、易理解、易复用
- 缺点
- 无法消除接口的耦合性
- 无法消除标识(identity)的耦合性,即一个对象要与其他对象交互,必须知道其他对象的标识
- 副作用:难以实现正确性
分层风格
- 约束
- 从最底层到最高层,上层调用下层
- 层次间遵守交互协议
- 跨层次连接禁止
- 逆向连接禁止
- 通常是不限粒度的模块实现
- 优点
- 设计机制清晰、易于理解
- 支持并行开发
- 更好的可复用性和内部修改性
- 缺点
- 交互协议难以修改
- 性能损失:禁止跨层调用
- 难以确定层次数量和粒度
MVC
- 模型-视图-控制
- 优点
- 易开发
- 视图和控制的可修改性
- 网络系统开发
- 缺点
- 复杂性
- 模型修改困难
体系结构设计过程
主要流程
- 分析关键需求和项目约束
- 概要功能性需求
- 非功能性需求
- 质量
- 性能
- 约束
- 接口
- 选择体系结构风格
- 进行软件体系结构逻辑(抽象)设计
- 依赖逻辑设计进行软件体系结构物理(实现)设计
- 逻辑设计从开发(开发包、物理模块)、发布(进程)、部署(网络部署)三个角度实现
- 包设计原则
- Data层置于服务器端,Logic层无法依赖Data层
- RMI技术:将Data层开发包分解置于客户端dataservice接口包和置于服务器端的Data层开发包
- socket通信传递数据
- 分层风格的典型设计中,不希望高层直接依赖于低层,而是为底层建立接口包,依赖倒置原则
- 消除循环依赖现象,可使用依赖倒置原则将循环依赖变为单向依赖
- Data层置于服务器端,Logic层无法依赖Data层
- 完善体系结构设计
- 定义构件接口
- 迭代3-6
体系结构集成与测试
常见集成策略
- 大爆炸式——适用于一个维护型项目或被测试系统较小的情况
- 增量式
- 自顶向下——适用于控制结构清晰稳定、高层接口变化较小、底层接口未定义或经常可能被修改、控制组件有较大的技术风险的软件系统
- 对分层次的架构,先集成和测试上层的模块,下层的模块使用stub,不断加入下层模块
- 优点
- 按DFS可以首先实现和验证一个完整的功能需求
- 只需要最顶端的一个驱动driver
- 利于故障定位
- 缺点
- stub开发量大
- 底层验证被推迟,且底层组件测试不充分
- 自底向上——适用于底层接口较稳定、高层接口变化频繁、底层组件较早被完成
- 从最底层的模块集成测试起,测试的时候上层的模块使用伪装的相同接口的驱动driver来替换
- 优点
- 底层组件较早验证
- 底层组件开发可以并行
- stub工作量少
- 利于故障定位
- 缺点
- driver工作量大
- 高层的验证被推迟,高层错误不能被及时发现
- 三明治式
- 持续集成
- 提倡尽早集成和频繁集成
- 自顶向下——适用于控制结构清晰稳定、高层接口变化较小、底层接口未定义或经常可能被修改、控制组件有较大的技术风险的软件系统
详细设计基础
结构化设计
面向对象设计
面向对象的设计过程
- 设计模型建立
- 通过职责建立静态设计模型
- 抽象类的职责
- 抽象类之间的关系
- 添加辅助类
- 通过协作建立动态设计模型
- 抽象对象之间协作
- 明确对象的创建
- 选择合适的控制风格
- 通过职责建立静态设计模型
- 设计模型重构
- 根据模块化思想重构,目标为高内聚、低耦合
- 根据信息隐藏的思想重构,目标为隐藏职责与变更
- 利用设计模式重构
通过职责建立静态模型
通过协作建立动态模型
详细设计中的模块化与信息隐藏
模块化
KWIC系统的例子
- 系统完成四个步骤:输入、循环位移、字母排序、输出
- 按算法分解——5个模块
- 主体控制模块Master control
- 输入模块Input
- 循环移位模块Circular shift
- 排序模块Alphabetizer
- 输出模块Output
- 数据是全局变量,共享给所有的模块
public void circularShift(){
ArrayList word_indices = new ArranList();
ArrayList line_indices = new ArrayList():
//...
}
结构化设计中的耦合
耦合
| 类型 | 耦合性 | 解释 | 例子 | | — | — | — | — | | 内容耦合 | 最高 | 一个模块直接修改或者依赖于另一个模块 | goto语句 | | 公共耦合 | 不能接受 | 模块之间共享全局变量 | 全局变量 | | 重复耦合 | 不能接受 | 模块之间有同样逻辑的重复代码 | 逻辑代码被复制到两个地方 | | 控制耦合 | 可以接受 | 一个模块给另一个模块传递控制信息 | 传递“显示星期天”。传递模块和接收模块必须共享一个共同的内部结构和逻辑 | | 印记耦合 | 可以接受 | 共享一个数据结构,但只用了其中一部分 | 传递了整个记录,但另一个模块只需要一个字段 | | 数据耦合 | 最低 | 两个模块的所有参数是同类型的数据项 | 传递一个整数给一个计算平方根的函数 |
内聚
类型 | 内聚性 | 解释 | 例子 |
---|---|---|---|
偶然内聚 | 最低 | 不相关操作 | |
逻辑内聚 | 不能接受 | 一系列相关 | 开车去、坐火车去、坐飞机去 |
时间内聚 | 可接受无法避免 | 初始化时 | 起床、刷牙、洗脸、吃早餐 |
过程内聚 | 可接受无法避免 | 与步骤顺序有关 | |
通信内聚 | 可接受无法避免 | 与步骤有关,且这些操作都在相同的数据上进行 | 查书名、查书的作者、查书的出版商 |
功能内聚 | 好 | 只执行一个操作或达到一个单一目的 | 下列内容作为独立模块:计算平方根、决定最短路径、压缩数据 |
信息内聚 | 最高 | 模块有许多操作,各有各的入口点,每个操作的代码相对独立,所有操作在相同的数据结构上完成 | 数据结构中的栈,包含相应的数据和操作,所有的操作都是针对相同的数据结构 |
信息隐藏
KWIC系统的第二种设计方案
- Lines模块隐藏的决策是字母和行的存储
- Input模块隐藏的决策是输入源及格式
- CircularShift模块隐藏的决策时位移的算法和位移后字符的存储
- Alphabetizer模块隐藏的决策时字母排序的算法
- Output模块隐藏的决策是输出目的地及其格式
- MasterControl模块隐藏的决策是整个任务的执行顺序
信息与隐藏
信息是什么
- 模块的秘密,对模块来说容易变化的地方,分为两类
- 一是根据需求分配的职责
- 二是内部实现机制
模块说明
- 模块说明中主要记录两个方面的四个主题:
- 内部角度说明模块所隐藏的秘密
- 来自需求的主要秘密
- 源于现实的次要秘密
- 外部角度说明模块承担的角色role和提供的接口facility
- 内部角度说明模块所隐藏的秘密
详细设计中面向对象方法下的模块化
面向对象中的模块
类之间的关系
- 除了结构化中的耦合关系,面向对象方法中还有其他复杂关系
- 关联——访问耦合
- 如果某个类关联另一个类,那么他就持有另一个类的引用,则这个类所有的对象都具有向另一个类的对象发送消息的能力
- 继承——继承耦合
- 子类可以访问父类的成员方法和成员变量
访问耦合
- 如果类A拥有对类B的引用,则A可以访问B
类型 | 耦合性 | 解释 | 例子 |
---|---|---|---|
隐式访问 | 最高 | B既没有出现在A的规格中,也没在实现中出现 | CascadingMessage |
实现中访问 | B的引用是A方法中的局部变量 | 通过引入局部变量,避免Cascading Message;方法中创建一个对象,将其饮用赋予方法的局部变量,并使用 | |
成员变量访问 | B的引用是A的成员变量 | 类的规格中包含所有需接口和供接口(需要特殊语言机制) | |
参数变量访问 | B的引用是A的方法的参数变量 | 类的规格中包含所有需接口和供接口(需要特殊语言机制) | |
无访问 | 最低 | 理论最优 |
- Cascading Message指在Client类中出现连续的方法调用“a.methodA.mehodB()”
Class A{
public B methodA(){
}
}
Class B{
public void methodB(){
}
}
Class Client{
public static void main(String args){
A a = new A();
a.methodA.methodB();
//B b = a.methodA;
//b.methodB();
}
}
降低访问耦合的方法
- 针对接口编程
- 契约式设计
- 前置条件:方法调用前,校验传入参数的正确性,正确才能执行
- 后置条件:一旦通过前置条件,方法必须执行,并确保执行结果符合契约,称为后置条件
- 不变式:对象本身有一套对自身状态进行校验的检查条件,以确保该对象的本质不发生改变,称为不变式
- 契约式设计
- 接口最小化/接口分离原则
- 访问耦合的合理范围/迪米特法则
- 每个单元对于其他的单元只能拥有有限的知识,只是与当前单元紧密联系的单元
- 每个单元只能和他的朋友交谈,不能喝陌生单元交谈
- 只和自己直接的朋友交谈
- 具体来说,对象O有一个方法M,M只能调用下列对象的方法
- O自己
- M中的参数对象
- 任何在M中创建的对象
- O的成员变量
- 例子:在连锁商店销售系统中,我们要获知每个已购买商品的价格,为此可以在sale类的方法中调用——违反了迪米特法则
salesList.getSalesLineItem().getCommodity().getPrice();
继承耦合
- 继承耦合分为
- 修改
- 修改规格——子类任意修改从父类继承回来的方法的接口
- 修改实现——子类任意修改从父类继承回来的方法的实现
- 精化
- 精化规格——子类只根据已经定义好的规则(语义)来修改父类的方法,但至少有一个方法的接口被改动
- 精化实现——子类只根据已经定义好的规则来修改父类的方法,但只改动了方法的实现
- 扩展——子类只是增加新的方法和成员变量,不对从父类继承回来的任何成员进行更改
- 修改
- 修改规格、修改实现、精化规格不可接受,精化实现可以接受,扩展是最好的继承耦合
降低继承耦合的方法
- Liskov替换原则
- 子类型必须能够替换掉基类型而起到同样的作用
- 按照契约式设计,为满足LSP
- 子类方法的前置条件必须与超类方法的前置条件相同或者要求更少
- 子类方法的后置条件必须与超类方法的后置条件相同或者要求更多
- 按照契约式设计,为满足LSP
- 子类型必须能够替换掉基类型而起到同样的作用
- 使用组合替代继承
- 组合关系代码
class Backend{
public int method(){
}
}
class Frontend{
public Backend back = new Backend();
public int method(){
back.method();
}
}
class Client{
public static void main(String[] args){
Frontend front = new Frontend();
int i = front.method();
}
}
内聚
- 衡量标准
- 方法和属性是否一致
- 属性之间是否体现一个职责
- 属性之间可否抽象
内聚低的例子:小计每一购物项金额的方法放在Sales类中
class Scales{
HashMap<Integer, SalesLineItem>map;
getSubtotal(int CommodityID){
1)根据CommodityID找到Commodity的价格
2)根据CommodityID找到SalesLineItem,再找到商品购买的数量
3)计算小计
}
}
内聚高的例子:小计每一购物项金额的方法放在SaleLineItem中,计算总额的类放在Sales类中
class Sales{
HashMap<Integer, SalesLineItem>map;
getTotal(){
total = item.getSubtotal();
}
}
class SalesLineItem{
Commodity commodity;
Int quantity;
getSubtotal():
}
内聚低:学号、姓名、成绩、课程编号、课程名在一个类里面
class SCORE{
int studentID;
String name;
int score;
int courseID;
String courseName;
}
内聚高:学生、课程、成绩在成绩类中
class Student{
int studentID;
String name;
}
class Course{
int courseID;
String courseName;
}
class SCORE{
Student student;
Course course;
int score;
}
内聚低:生产年份、月份、日期,进货年份、月份、日期在一个类里
class Product{
int yearOfProduction;
int mothOfProduction;
int dayOfProduction;
int yearOfImport;
int monthOfImport;
int dayOfImport;
}
内聚高:抽象出日期类包括年月日三个属性,类里面只有日期类的生产日期和进货日期两个变量
class Date{
int year;
int month;
int day;
}
class Product{
Data productionDate;
Date importDate;
}
提高内聚的方法
- 集中信息与行为:GPS定位系统的例子
public class Position
{
public double latitude;
public double longtitude;
public double calculateDistance(Position pos){
//计算当前点到pos点的距离
}
public double calculateDirection(Position pos){
//计算当前点到pos点的方向
}
}
- 单一职责原则SRP
- 信息和行为除了要集中外,还要联合起来表达一个内聚的概念,而不是简单的堆砌
耦合与内聚的度量
耦合的度量
- 方法调用耦合
- CBO (coupling between objects)越小越好
- 访问耦合
- DAC(data abstraction coupling)越小越好
- 继承耦合
- NOC(number of children)
- DIT(depth of the inheritance tree)
内聚的度量
详细设计中面向对象方法下的信息隐藏
封装类的职责
为变更而设计
详细设计的设计模式
设计模式基础
可修改性和基本实现机制
-
通过接口和接口的实现
-
public class Client{ public static void main(String [] args){ //创建 Interface_A a = new Class_A1(); Interface_A a = new Class_A2(); //多态 //调用 a.method_A(); } } public interface Interface_A{ //接口 public void method_A(); } public class Class_A1 implements Interface_A{ public void method_A(){ //实现 System.out.printIn("Class_A1's method_A()!") } } public class Class_A2 implements Interface_A{ public void method_A(){ //扩展新的实现类 } }
-
通过子类继承父类
-
public class Client{ public static void main(String [] args){ Super_A a = new Sub_A(); Super_A a = new Sub_B(); //多态 a.method_A(); } } public class Super_A{ public void method_A(){ System.out.printIn("Super_A's method_A()!") } } public class Sub_A extends Super_A{ public void method_A(){ Syetem.out.printIn("Sub_A's method_A()!") } } public class Sub_B extends Super_A{ public void method_A(){ //新的子类 } }
-
组合关系中的前端类和后端类例子
- 前端和后端接口上不具有耦合性,后端接口改变时不会影响到Client的代码
-
后端类的实现可以动态创建、动态配置、动态销毁,非常灵活
class Backend{ public int method_2(){ } } class Frontend{ //组合关系,Frontend类中包含Backend类的实例化 public Backend back = new Backend(); public int method_2(){ back.method_2(); } } class Client{ //Client类中无需关心Backend类 public static void main(String [] args){ Frontend front = new Frontend(); int i = front.method_2(); } }
策略模式
- 策略模式的优点
- 提高代码的可扩展性,符合面向接口编程原则
- 策略模式提供了可以替换继承关系的办法
- 使用策略模式可以避免使用多重条件转移语句
- 策略模式的缺点
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类
- 策略模式造成很多的策略类,每一个具体策略类都会产生一个新类
//Client:TestDriver类
public class TestDriver{
public static void main(String [] args){
Employee tom = new Employee("tome",0);
Employee jack = new Employee("jack",1);
Employee kevin = new Employee("kevin",2);
HourlyClassfication hc = new HourlyClassfication(10,40);
CommissionedClassfication cc = new CommissionClassfication(0.01,1000000);
SalariedClassification sc = new SalariedClassfication(3000);
WeeklySchedule ws = new WeeklySchedule(2019, Calendar.FEBRUARY,22);
BiweeklySchedule bs = new BiweelySchedule(2019, Calendar.FEBRUARY,22);
MonthlySchedule ms = new MonthlySchedule(21);
//配置Employee对象,也可通过带参数的构造方法来配置
tom.setPaymentClassification(hc);
tom.setPaymentSchedule(ws);
//...
//Employee对象的使用
while(i.hasNext()){
Employee e = i.next();
if(e.isPayDay()){
e.getPayment();
}
}
//...
}
}
//Context:Employee类
public class Employee{
String name;
int ID;
PaymentClassification pc;
PaymentSchedule ps;
//在Employee类中加入两个实例变量,声明为接口类型而不是具体类实现类型
//实例对象在运行时持有特定行为的引用
public Employee(String s, int id){
//构造器
name = s;
ID = id;
}
public void getPayment(){
//支付方式
double payment = 0;
payment = pc.calculatePayment();
System.out.printIn(name + "get" + payment + " dollars!");
}
public booleam isPayDay(){
//使用支付策略
boolean isPay = false;
isPay = ps.isPayDay();
if(isPay){
//...
}
return isPay;
}
//setter方法设置策略,而不是在Employee构造器内实例化,可随时调用改变行为
//setter方法持有对行为设定的引用即可实现
public void setPaymenyClassification(PaymentClassification paymentClassification){
pc = paymentClassification;
}
public void setPaymentSchedule(PaymentSchedule paymentSchedule){
ps = paymentSchedule;
}
}
//Strategy:策略接口
public interface PaymentClassification{
public double calculatePayment();
}
//ConcreteStrategy:HourlyClassification类,实现PaymentClassification接口的具体策略类
public class HourlyClassification implements PaymentClassification{
int hourlyRate;
int hours;
public HourlyClassification(int rate,int h){
hourlyRate = rate;
hours = h;
}
public double calculatePayment(){
//策略接口方法的实现
int sum = 0;
sum = hourlyRate * hours;
return sum;
}
}
抽象工厂模式
//可通过多态的形式体现不同的行为实现,但构造方法无法多态
class Client{
public void dosomething1(int type){
if(type == 1){
Class a = new ClassA1();
}else{
Class a = new ClassA2();
}
a.method1();
}
public void dosomething2(int type){
if(type == 1){
Class a = new ClassA1();
}else{
Class a = new ClassA2();
}
a.method2();
}
}
class ClassA{}
class ClassA1 extends ClassA{}
class ClassA2 extends ClassA{}
//工厂模式就是为对象的创建提供一个接口,将具体创建的实现封装在接口之下,这样具体创建的改变不会影响Client代码
//工厂模式代码如下
class Client1 extends Client{
public ClassA createClassA(){
if(type == 1){
Class a = new ClassA1();
}else{
Class a = new ClassA2();
}
return a;
}
}
public interface DatabaseFactory{
public CommodityDatabaseService getCommodityDatabase();
public UserDatabaseService getUserDatabase();
public MemberDatabaseService getMemberbase();
public SalesDatabaseService getSalesbase();
}
public class DatabaseFactoryMysqlImpl implements DatabaseFactory{
UserDatabaseService userDatabase = new UserDatabaseServiceMySqlImpl();
//...
SalesDatabaseService salesDatabase = new SalesDatabaseServiceMySqlImpl();
//构造函数
public DatabaseFactoryMySqlImpl() throws RemoteException{}
public UserDatabaseService getUserDatabase(){
return UserDatabase;
}
//...
public SalesDayabaseService getSalesDatabase(){
return SalesDatabase;
}
}
public class UserDatabaseServiceMysqlImpl implements UserDataService{
//用组合代替继承
}
单件模式
//数据库抽象工厂的具体工厂应该只有一个实现,根据它我们可以得到不同数据库表的不同实现
public class DatabaseFactoryTxtFileImpl implements DatabaseFactory{
//单件
//UML类图中private是减号,public是加号
private static DatabaseFactoryTxtFileImpl databaseFactoryTxtFile = null;
//databaseFactoryTxtFile是静态类型的单件类型的引用变量
private DatabaseFactoryTxtFileImpl(){
}
//类的构造方法要私有!!
public static DatabaseFactory getInstance(){
//通过静态方法获得对象的引用
if(datdabaseFactoryTxtFile == null)
//如果是null就是首次创建,通过new创建单件对象
databaseFactoryTxtFile = new DatabaseFactoryTxtFileImpl();
return databaseFactoryTxtFile;
//返回单件类型的引用
}
}
- 单件模式使用的设计原则:职责抽象,即隐藏单件创建的实现
- 为了实现只创建一个对象,首先要让类的构造方法私有,然后只能通过静态的getInstance方法获得单件类型的对象的引用
- 这就要求类的成员变量中拥有一个静态类型的单件类型的引用变量uniqueInstance
- getInstance方法返回引用变量uniqueInstance
- 如果uniqueInstance是null说明是首次创建,通过new创建单件对象,并且将该对象的引用变量赋值给uniqueInstance;否则说明不是首次创建,正常返回
迭代器模式
软件测试
测试相关概念
- 集成测试
- 也叫组装测试或者联合测试。在单元测试的基础上,将所有模块按照设计要求(如根据结构图)组装称为子系统或系统,进行集成测试
- 回归测试
- 指修改了旧代码后,重新进行测试以确认没有引入新的错误或导致其他代码产生错误
- Alpha测试
- 是由一个用户在开关环境下进行的测试,也可以是公司内部的用户在模拟实际操作环境下进行的受控测试,Alpha测试通常不能由测试员完成
- Beta测试
- 是软件的多个用户的实际使用环境下的测试。开发者通常不在测试现场,Beta测试不能由程序员或测试员完成
验证与确认
V&V
验证
- 检查开发者是否正确的使用技术建立系统,确保系统能够在预期的环境中按照技术要求建立系统,确保系统能够在预期的环境中按照技术要求正确地运行
确认
- 检查开发者是否建立了正确的系统,确保最终产品符合规格。
- 例如:对需求文档内容是否反映用户真实意图,设计是否能跟踪到需求,测试是否覆盖需求,代码是否按照需求与设计的要求编写
测试技术
随机测试
基于规格的技术——黑盒测试
- 早期的黑盒测试主要使用等价类划分、边界值分析等简单方法,后来为形式化模型、UML等各种规格手段都建立了相应的测试方法
等价类划分
- 等价类划分就是把所有可能的输入数据,即程序的输入域划分成若干子集,然后从每一个子集中选取少数具有代表性的数据作为测试用例
- 可分为两种情况
- 有效等价类
- 无效等价类
边界值分析
- 边界值分析方法是对等价类划分方法的补充
决策表
- 决策表是为复杂逻辑判断设计测试用例的技术
- 决策表是由条件声明、行动声明、规则选项和行动选项四个象限组成的表格
状态转换
基于代码的技术——白盒测试
语句覆盖
- 确保被测试对象的每一行程序代码都至少执行一次
- 相比于条件覆盖和路径覆盖,语句覆盖是一种比较弱的代码覆盖技术,不能覆盖所有的执行路径
路径覆盖
- 确保程序中每个判断的每个结果都至少满足一次
- 条件覆盖保证判断中的每个条件都被覆盖了
- 条件覆盖的覆盖程度比语句覆盖强
分支覆盖
- 路径覆盖测试用例的标准就是确保程序中每条独立的执行路径都至少执行一次
测试活动
- 典型活动包括:测试计划、测试设计、测试执行和测试评价
测试度量
软件有效性验证
- 通常被称为检验和有效性验证(V&V)
- 测试过程中的阶段包括
- 组件测试(单元测试):组件可以是简单的实体
- 系统测试:集成组件形成完整的系统,关注无法预测的组件交互和组件界面问题引发的错误
- 接收测试:在系统接受并运行之前进行的最后阶段测试,使用客户提供的真实数据测试系统
- V开发模型:把测试和开发活动联系起来
- 接收测试又是被称为alpha测试——针对定制系统
- beta测试——将系统交付给愿意使用该系统的潜在用户,能够暴露开发者无法预见的错误
- V开发模型:把测试和开发活动联系起来
人机交互设计
人机交互的目标
易用性的维度
- 易学性
- 效率
- 易记性
- 出错率
- 主观满意度
人机交互设计的人类因素
- 精神模型
- 差异性
一些人机交互设计原则
- 简洁设计
- 常见要求是不要使用太大的菜单、不要在一个窗口中表现过多的信息类别、不要在一个表单中使用太多的颜色和字体作为线索
- 一致性设计
- 低出错率设计
- 出错信息应该遵循以下四个原则
- 应当使用清晰的语言来描述
- 使用的语言应该精炼准确,不空泛模糊
- 应该对用户解决问题提供建设性的帮助
- 出错信息应该友好,不要威胁或责备用户
- 出错信息应该遵循以下四个原则
- 易记性设计
- 减少短期记忆负担
- 使用逐层递进的方式展示信息
- 快捷方式
- 设置有意义的默认值
人机交互设计过程
- 需求开发
- 需求收集、场景分析
- 软件设计
- 导航设计、界面设计、界面原型化、界面评估与修正
- 构造、测试与维护
- 构造、测试与维护
代码设计
设计可靠的代码
契约式设计
- 契约式设计又称断言式设计,基本思想是:如果一个函数或方法,在前置条件满足的条件下开始执行,完成后能够满足后置条件那么这个函数或方法就是正确、可靠的
异常方式
- 在契约式设计的异常方式就是在代码开始执行时,检查前置条件是否满足,不满足就抛出异常。在代码执行完后,再检查后置条件是否满足,不满足也抛出异常
- Sales类的getChange方法
public class Sales extends DomainObject{
...
public double getChange(double payment)throws PreException, PostException{
//前置条件检查
if(payment<=0)||(payment<total){
throw new PreException(“Sales.getChange: Payment” + String.valueOf(payment) + “; Total” + String.valueOf(total));
}
...
//返回result之前进行后置条件检查
if(result != (payment-total)){
throw new PostException(“Sales.getChange: Payment” + String.valueOf(payment) + “; Total” + String.valueOf(total));
}
return result;
}
}
断言方式
- Sales.getChange()方法
public class Sales extends DomainObject{
...
public double getChange(double payment)throws AssertionError{
//前置条件检查
assert((payment>0) && (payment>=total)):
(“Sales.getChange:Payment” + String.valueOf(payment) + “; Total” + String.valueOf(total));
...
//返回result之前进行后置条件检查
assert(result == (payment-total)):
(“Sales,getChange:Payment” + String.valueOf(payment) + “; Total” + String.valueOf(total));
return result;
}
}
比较
- 虽然断言方式实现起来简单,但不推荐在复杂系统中使用断言方式,因为断言方式只能抛出AssertionError异常,不利于故障诊断
- 最好在Public方法中使用异常方式,在Protected、Private方法中使用断言
防御式编程
- 基本思想:在一个方法和其他方法、操作系统、硬件等外界条件交互时,不能确保外界都是正确的,所以要在外界发生错误时,保护内部方法不受损害
- 与契约式设计的共同点是:他们都要检查输入参数的有效
- 差异点是:防御式编程将所有与外界的交互(不仅仅是前置条件所包含的)都纳入防御范围,例如用户输入的有效性、待读写文件的有效性、调用的其他方法返回值的有效性
- 防御式编程不检查输出和后置条件,因为他们的使用者会自行检查