大纲
软件维护和演变
可维护性度量
模块化设计和模块化原则
OO设计原则:SOLID
OO设计原则:GRASP
总结
软件维护和演变
什么是软件维护?
软件工程中的软件维护是交付后修改软件产品以纠正故障,提高性能或其他属性。软件维护:修复错误,改善性能
在“ISO / IEC 14764:2006软件工程 - 软件生命周期过程 - 维护”
运维工程师
维护是软件生产中最困难的方面之一,因为维护包含了所有其他阶段的各个方面
用户报告故障并由维护工程师处理。
维护工程师必须具备出色的调试技能
- 故障可能存在于产品中的任何地方,故障的原因可能在于现在不存在的规格或设计文档(错误/问题本地化)。
- 需要高超的诊断技能,测试技能和文档技能(测试,修复和记录更改)。
软件维护的类型
纠正性维修25%纠错性
- 对交付后执行的软件产品进行无功修改,以纠正发现的问题;
适应性维护21%适应性
- 修改交付后执行的软件产品,以保持软件产品在变化或变化的环境中可用;
完善性维护50%完善性
- 交付后增强软件产品以提高性能或可维护性;
预防性维护4%预防性
- 交付后对软件产品进行修改,以便在软件产品发生有效故障前检测并纠正软件产品中的潜在故障。
雷曼关于软件演化的规律(Lehman’s Laws on Software Evolution)
反馈系统
持续变化
持续增长
质量下降
增加复杂性
自我调节
- 保持组织稳定
- 保持熟悉
软件维护和进化的目标
软件维护和演化的目标:为了提高软件的适应性和适应性,并保持其活力,即“长期软件(低熵软件)”
提高软件的适应性,延续软件生命
Linux内核发展的一个例子:可维护性指数
维护不仅仅是op工程师的任务......
维护不仅仅是维护和操作工程师的任务,也是软件设计人员和开发人员的潜在任务。 软件维护不仅仅是运维工程师的工作,而是从设计和开发阶段就开始了
对他们来说,在设计和施工阶段必须考虑软件的未来潜在变化/扩展;在设计与开发阶段就要考虑将来的可维护性
因此,灵活和可扩展的设计/结构被全面考虑,换句话说,“易于改变/扩展”。 设计方案的“容易改变”
这就是所谓的软件构建的“可维护性”,“可扩展性”和“灵活性”。
可维护性建设的例子
模块化设计与实现模块化
- 低耦合和高内聚力
OO设计原则OO设计原则
- SOLID,GRASP
OO设计模式OO设计模式
- 工厂方法模式,Builder模式
- 桥模式,代理模式
- 纪念模式,状态模式
基于状态的构造技术(自动机编程)
表驱动的构造技术
基于语法的构造技术(Grammar-based construction)
可维护性度量
许多可维护性的名字
可维护性
- “软件系统或组件可轻松修改以纠正故障,改善性能或其他属性,或适应变化的环境”。
可扩展性
- 软件设计/实现将未来的增长考虑在内,并被视为扩展系统能力的系统性测量以及实现扩展所需的工作量。
灵活性
- 软件根据用户需求,外部技术和社会环境等容易改变的能力。
可适应性
- 一种交互式系统(自适应系统)的能力,可以根据所获得的有关其用户及其环境的信息,使其行为适应个人用户。
可管理性
- 如何有效和轻松地监控和维护软件系统,以保持系统的正常运行,安全并平稳运行。
可支持性
- 基于包含质量文档,诊断信息以及知识丰富且可用的技术人员的资源,可以有效地使软件在部署后保持运行。
有关可维护性的问题
Code review的时候经常问的关于可维护性的问题:
- 结构和设计简单:改变事物有多容易?
- 事情紧密或松散(即关注点分离)?
- 包/模块中的所有元素是否具有凝聚力,其职责是否清晰且密切相关?
- 它是否具有过深的继承层次结构,还是赞成继承的组合?
- 方法定义中存在多少个独立的执行路径(即,cycolmatic复杂度)?
- 存在多少代码重复?
一些常用的可维护性度量标准
圈复杂度 - 测量代码的结构复杂度。
- 它是通过计算程序流程中不同代码路径的数量而创建的。
- 具有复杂控制流程的程序将需要更多的测试来实现良好的代码覆盖率,并且将不易维护。
- CC = E-N + 2,CC = P + 1,CC =区域的数量
Lines of Code(代码行数) - 表示代码中的近似行数。
- 非常高的数值可能表明某种类型或方法试图做太多工作,应该分手。
- 这也可能表明类型或方法可能难以维护。
对于给定的问题,令:
η_1=不同运算符的数目
η_2=不同的操作数的数量
Ν_1=运算符总数
Ν_2=操作数总数
从这些数字中可以计算出几项度量:
程序词汇表:η=η_1+η_2
程序长度:N=Ν_1+Ν_2
计算的程序长度:N ̂=η_1 log_2〖η_1 〗+η_2 log_2〖η_2 〗
体积:V=N×log_2η
难度:D=η_1/2×Ν_2/η_2
代价:E=D×V
难度测量与程序编写或理解的难度有关
代价度量使用以下关系转化为实际编码时间,
编程所需的时间:T=E/18 秒
Halstead提供的错误(B)是对执行错误数量的估计。
提供的错误数量:B = E^(2/3)/3000或更接近的B = V/3000也被接受。
Halstead Volume:基于源代码中(不同)运算符和操作数的数量的合成度量。
一些常用的可维护性度量标准
可维护性指数(MI)- 计算介于0和100之间的索引值,表示维护代码的相对容易性。 高价值意味着更好的可维护性。 它的计算基于:
- Halstead体积(HV)
- 圈复杂性(CC)
- 每个模块的平均代码行数(LOC)
- 每个模块注释行的百分比(COM)。
一些常用的可维护性度量标准
继承的层次数
- 表示扩展到类层次结构的根的类定义的数量。 等级越深,就越难理解特定方法和字段在何处被定义或重新定义。
类之间的耦合度
- 通过参数,局部变量,返回类型,方法调用,泛型或模板实例化,基类,接口实现,在外部类型上定义的字段和属性修饰来测量耦合到唯一类。
- 良好的软件设计决定了类型和方法应该具有高内聚性和低耦合性。
- 高耦合表示一种设计难以重用和维护,因为它与其他类型之间存在许多相互依赖关系。
单元测试覆盖率
- 指示代码库的哪些部分被自动化单元测试覆盖。 (将在第7章中研究)
模块化设计和模块化原则
模块化编程
模块化编程是一种强调将程序功能分离为独立,可互换模块的设计技术,每种模块都包含执行所需功能一个方面所需的一切。
将整个程序的代码高度分解为结构化编程和OOP。
模块化编程设计的目标是将系统划分为模块,并通过以下方式在组件之间分配责任:
- 模块内高凝聚力(高内聚)
- 模块之间的耦合松耦合(低耦合)
模块化降低了程序员在任何时候都必须处理的总体复杂性,假设:
- 将功能分配给将相似功能组合在一起的模块(分离关注点)
- 模块之间有小而简单的定义明确的接口(信息隐藏)
内聚和耦合的原则可能是评估设计可维护性的最重要的设计原则。
(1)评估模块性的五个标准
Decomposability(可分解性)
- 较大的组件是否分解为较小的组件?
Composability(可组合性)
- 较大的组件是否由较小的组件组成?
Understandability(可理解性)
- 组件是否可单独理解
Continuity(可持续性)
- 规范的小改动是否会影响本地化和有限数量的组件?
Protection(出现异常之后的保护)
- 运行时异常的影响是否局限于少数相关组件?
(2)模块化设计的五条原则
Direct Mapping (直接映射)
Few Interfaces (尽可能少的接口)
Small Interfaces (尽可能小的接口)
Explicit Interfaces (显式接口)
Information Hiding (信息隐藏)
(3)耦合和内聚
耦合
耦合是模块之间依赖关系的度量。 如果两个模块之间的变化可能需要另一个模块的变更,则两个模块之间存在依赖关系。
模块之间的耦合度取决于:
- 模块之间的接口数量(数量)和
- 每个接口的复杂性(由通信类型决定)(质量)
HTML,CSS和JavaScript之间的耦合
一个精心设计的网络应用程序模块化:
- 指定数据和语义的HTML文件
- 规定HTML数据的外观和格式的CSS规则
- 定义页面行为/交互性的JavaScript
内聚
内聚是衡量一个模块的功能或责任有多强烈程度的一个指标。
如果一个模块的所有元素都朝着相同的目标努力,那么它就具有很高的内聚。
最好的设计在模块内具有高内聚力(也称为强内聚力)和模块之间的低耦合(也称为弱耦合)。
OO设计原则:SOLID
SOLID:5类设计原则
(SRP) The Single Responsibility Principle 单一责任原则
(OCP) The Open-Closed Principle 开放-封闭原则
(LSP) The Liskov Substitution Principle Liskov替换原则
(DIP) The Dependency Inversion Principle 依赖转置原则
(ISP) The Interface Segregation Principle 接口聚合原则
(1)单一责任原则(SRP)
“类改变不应该有一个以上的原因”,即一个类应该集中精力做一件事,只能一件事。
责任:“变更的理由”(责任:变化的原因)
SRP:
- 类改变不应该有一个以上的原因。 (不应有多于1个的原因使得一个类发生变化)
- 一个类,一个责任。(一个类,一个责任)
如果一个类包含了多个责任,那么将引起不良后果:
- 引入额外的包,占据资源
- 导致频繁的重新配置,部署等
SRP是原则中最简单的一种,也是最难做到的一种。(最简单的原则,却是最难做好的原则)
(2)开放/封闭原则(OCP)
类应扩展(对扩展性的开放)
- 这意味着模块的行为可以扩展。 我们可以根据应用程序的需求变化,或者为了满足新应用程序的需求,使模块以新的和不同的方式运行。 (模块的行为应是可扩展的,从而该模块可表现出新的行为以满足需求的变化)
但关闭修改。(对修改的封闭)
- 这种模块的源代码是不可侵犯的。 没有人可以对其进行源代码更改。(但模块自身的代码是不应被修改的)
- 扩展模块行为的正常方法是对该模块进行更改。(扩展模块行为的一般途径是修改模块的内部实现)
- 无法更改的模块通常被认为具有固定的行为。(如果一个模块不能被修改,那么它通常被认为是具有固定的行为)
Key:abstraction(关键的解决方案:抽象技术)
“软件实体(类,模块,函数等)应该被打开以进行扩展,但是为了修改而关闭”,即,使用继承和组合来改变类的行为
开放封闭原则 - 几个问题...。
不可能在不修改GraphEditor的情况下添加新的Shape
重要的是要了解GraphEditor添加一个新的形状
GraphEditor和Shape之间的紧密耦合
不参与GraphEditor就很难测试特定的Shape
If-Else-/Case应该被避免
OCP表示单一选择
只要软件系统必须支持一组替代方案,系统中的一个且只有一个模块应该知道他们的详尽列表。
编辑器:一组命令(插入,删除等)
图形系统:图形类型集(矩形,圆形等)
编译器:语言结构集(指令,循环,表达式等)
(3)Liskov替换原则(LSP)
“使用指针或对基类的引用的函数必须能够使用派生类的对象而不知道它”,即,子类在使用它们的基类时应该表现得很好
LSP:子类型必须可替代其基本类型。(子类型必须能够替换其基类型)
派生类必须可以通过基类接口使用,而不需要客户端了解其差异。 (派生类必须能够通过其基类的接口使用,客户端无需了解二者之间的差异)
在第5-2节中已经讨论过可复用性。
(4)接口隔离原理(ISP)
“客户不应该被迫依赖他们不使用的接口”,即保持接口小。
不要强制类来实现它们不能实现的方法(Swing / Java)
不要用很多方法污染界面
避免“胖”的接口
客户不应该被迫依赖他们不使用的方法。(客户端不应依赖于它们不需要的方法)
接口属于客户端,而不属于层次结构。
这个原则处理“胖”接口的缺点。(“胖”接口具有很多缺点)
具有“胖”接口的类是接口不具有内聚性的类。(不够聚合)
- 该类的接口可以分解为多组成员函数。(胖接口可分解为多个小的接口)
- 每个小组服务一组不同的客户端(不同的接口向不同的客户端提供服务)。
- 因此有些客户使用一组成员函数,而其他客户使用其他组。(客户端只访问自己所需要的端口)
(5)依赖倒置原理(DIP)
高级模块不应该依赖于低级模块。 两者都应该取决于抽象。
- 抽象不应该依赖于细节(抽象的模块不应依赖于具体的模块)
- 细节应该取决于抽象(具体应依赖于抽象)
应该使用大量的接口和抽象!
为什么DIP?
优点:
- 将类契约正式化。
- 您根据前置和后置条件定义例程的服务。 这非常清楚会发生什么。
尝试设计测试
- 创建一个测试友好的设计
- 测试友好的模块可能会展现其他重要的设计特征。
例如:你会避免循环依赖。 如果您必须从UI单独测试,业务逻辑将更好地与UI代码隔离
OO设计原则:GRASP
什么是GRASP模式
一般责任分配软件模式(原则),缩写为GRASP,包含为OOP中的类和对象分配责任的准则。
GRASP模式是帮助理解基本对象设计的学习辅助,并以有条理,合理,可解释的方式应用设计推理。
这种理解和使用设计原则的方法是基于对班级分配责任的模式。
GRASP是关于如何为“类”和“对象”指派“职责”的一系列原则
什么是责任
对象的责任:与对象的义务有关
了解:
- 了解私有封装数据
- 了解相关对象
- 了解它可以派生或计算的事物
这样做:
- 自己做一些事情,比如创建一个对象或者做一个计算
- 在其他对象中发起行动
- 控制和协调其他对象的活动。
总结
软件维护和演变
可维护性度量
模块化设计和模块化原则
OO设计原则:SOLID
OO设计原则:GRASP
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。