软件的维护和发展
软件维护:修复错误、改善性能
运维是软件开发中最困难的工作之一,涉及到其他所有环节
- 处理来自用户报告的故障/问题
- 修复代码后:
- 测试所做的修改
- 回归测试
- 记录变化
- 除了修复问题,修改中不能引入新的故障。
最大的问题:修改后没有足够的文档记录和测试用例
软件维护的类型
- 纠错性(25%)
对交付后执行的软件产品进行反应性修改,以纠正发现的问题;
- 适应性(21%)
修改交付后执行的软件产品,以保持软件产品在变化或变化的环境中可用;
- 完善性(50%)
软件产品交付后的改进,以提高性能或可维护性;
- 预防性(4%)
软件产品交付后的修改,在软件产品中的潜在故障成为有效故障。
软件演化:是软件维护的重要部分;对软件进行持续的更新
软件的大部分成本来自于维护阶段
软件维护不仅仅是运维工程师的工作,而是从设计和开发阶段就开始了
在设计与开发阶段就要考虑将来的可维护性
面向可维护性的构造技术:
- 模块化
- 低耦合和高内聚
- OO设计原则
- SOLID、GRASP
- OO设计模式
- 工厂方法模式、生成器模式
- 桥模式、代理模式
- 记忆件模式、状态模式
- 基于状态的构造
- 表驱动的构造技术
- 基于语法的构造技术
可维护性度量指标
- 可维护性:易于修改软件系统或组件以纠正故障、提高性能或其他属性,或适应不断变化的环境。
- 可扩展性:软件设计/实现考虑到未来的扩展。可扩展性被视为扩展系统的能力和实现扩展所需的努力水平的系统度量。
- 灵活性:软件能够根据用户需求、外部技术和社会环境等轻松更改的能力。
- 可适应性:交互式系统(自适应系统)的能力,它可以根据所获得的关于用户及其环境的信息来适应个人用户的行为。
- 可管理性:如何监控和维护软件系统的效率和容易,以保持系统的执行、安全和平稳运行。
- 支持性:基于包括高质量文档、诊断信息和知识渊博和可用的技术人员在内的资源,在部署后软件如何有效地保持运行。
关于可维护性的问题:
- 设计结构是否足够简单?
- 模块之间是否松散耦合?模块内部是否高度聚合?
- 是否使用了非常深的继承树,是否使用了委托替代继承?
- 代码的圈复杂度是否太高?
- 是否存在重复代码?
模块化设计和模块化原则
评估模块化五个标准
- 可分解性
较大的组件会分解成较小的组件吗?
将问题分解为各个可独立解决的子问题
目标:使模块之间的依赖关系显式化和最小化
- 可组合性
较大的组件是否由较小的组件组成?
可容易的将模块组合起来形成新的系统
目标:使模块可在不同的环境下复用
- 可理解性
组件是否单独可理解?
每个子模块都可被系统设计者容易的理解
- 可持续性
对规范的小更改是否会影响本地化且数量有限的组件?
规格说明小的变化将只影响一小部分模块,而不会影响整个体系结构
模块提供的所有服务应该通过统一标识提供
- 出现异常之后的保护
运行时异常的影响是否仅局限于少量的相关组件?
运行时的不正常将局限于小范围模块内
模块化设计五个规则
- 直接映射:模块的结构与现实世界中问题领域的结构保持一致
- 尽可能少的接口:模块应尽可能少的与其他模块通讯
- 尽可能小的接口:如果两个模块通讯,那么它们应交换尽可能少的信息
- 显式接口:当A与B通讯时,应明显的发生在A与B的接口之间
- 信息隐藏:经常可能发生变化的设计决策应尽可能隐藏在抽象接口后面
耦合度与内聚度
耦合度是衡量模块之间依赖性的度量方法。如果一个模块的更改需要另一个模块,则两个模块之间存在依赖关系。
模块间的耦合程度决定于:
- 模块之间的接口数量(数量)
- 每个接口的复杂度(由通信类型决定)(质量)
内聚度是衡量一个模块的功能或责任的相关程度。
如果模块的所有元素都朝着相同的目标工作,那么它具有很高的凝聚力。
OO设计原则:SOLID
SOLID:5类设计原则
- (SRP) The Single Responsibility Principle 单一责任原则
- (OCP) The Open-Closed Principle 开放-封闭原则
- (LSP) The Liskov Substitution Principle 里氏替换原则
- (DIP) The Dependency Inversion Principle 依赖转置原则
- (ISP) The Interface Segregation Principle 接口聚合原则
单一责任原则
ADT中不应该有多于1个原因让其发生变化,否则就拆分开。
如果一个类包含了多个责任,那么将引起不良后果:
– 引入额外的包,占据资源
– 导致频繁的重新配置、部署等
开放-封闭原则
对扩展性的开放:模块的行为应是可扩展的,从而该模块可表现出新的行为以满足需求的变化
对修改的封闭:但模块自身的代码是不应被修改的
里氏替换原则
子类型必须能够替换其基类型
派生类必须能够通过其基类的接口使用,客户端无需了解二者之间的差异
接口聚合原则
不能强迫客户端依赖于它们不需要的接口:只提供必需的接口
依赖转置原则
高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
抽象不应该依赖于实现细节,实现细节应该依赖于抽象
语法驱动的构造
有一类应用,从外部读取文本数据,在应用中做进一步处理。
输入文件有特定格式,程序需读取文件并从中抽取正确的内容
从网络上传输过来的消息,遵循特定的协议
用户在命令行输入的指令,遵循特定的格式
内存中存储的字符串,也有格式需要
使用grammar判断字符串是否合法,并解析成程序里使用的数据结构
语法组成:
终止符:语法中的文字字符串
为了描述一串符号,无论它们是字节、字符,还是从一个固定的集合中提取的其他类型的符号,我们使用一种称为语法的紧凑表示。语法定义了一组字符串。语法中的文字字符串被称为终止符。
它们被称为终止符,因为它们是表示字符串结构的解析树的叶子。
它们没有任何子节点,也不能再往下扩展。
通常表示为字符串。
语法器中的非终止接点和产生式节点
语法由一组产生式节点描述,其中每个产生式节点定义了一个非终止节点。
非终止节点就像一个变量,它代表一组字符串,并根据其他变量(非终止节点)、运算符和常量(终端)生成作为该变量的定义。(遵循特定规则,利用操作符、终止节点和其他非终止节点,构造新的字符串)
非终止节点是表示字符串的树的内部节点。
语法中产生式有以下形式:
非终止节点 ::= 终止符、非终止节点和操作符的表达式
语法中的其中一个非终止节点被认为是根。
语法识别的一组字符串是与根匹配的字符串。
这种非终止节点通常被称为根节点或开始节点。
语法运算符:
连接: “ ”(空格)
重复: “*”
选择: “|”
? 可选择的:0次或1次
+ 一次或多次
[] 包含其中任何字符
[^]不包含其中字符
语法递归
可根据自身递归定义
解析树
将语法与字符串进行匹配可以生成解析树,该树显示字符串的某些部分如何与语法的某些部分对应。
解析树的叶子用终止符标记,表示已解析的字符串的各个部分。它们没有任何孩子,也不能进一步扩展。
如果我们将叶子连接在一起,我们就会得到原始的字符串。
Markdown 和 HTML
标记语言:表示文本中的排版样式。
一种可以使用普通文本编辑器编写的标记语言,通过简单的标记语法,它可以使普通文本内容具有一定的格式
正则语法和正则表达式
正则语法:简化之后可以表达为一个产生式而不包含任何非终止节点
正则表达式:终端和运算符的简化表达式可以写成一种更紧凑的形式,称为正则表达式。
正则表达式去掉了终端周围的引号,以及终端和运算符之间的空格,因此它只由终端字符、用于分组的圆括号和运算符字符组成。
正则表达式也被简称为regex。
正则表达式的可读性远不如原始语法,因为它缺少记录每个子表达式含义的非终端名称。但是正则表达式的实现速度很快,而且在许多编程语言中都有支持正则表达式的库。
正则表达式中的一些特殊运算符
. 任何单个字符
\d 任何数字,与[0-9]相同
\s 任何空格字符,包括空格、选项卡、换行符
\w 任何单词字符,包括下划线,与[a-zA-Z0-9]相同
\., \(, \), \*, \+, ... 转义运算符或特殊字符,使其与字面上匹配