软件构造复习笔记(4)

注:文章中带有 * 的标题表示往年考试中出现过相应考点

第十章 面向可维护性的构造技术

1 软件可维护性和演化

软件维护:修复错误、改善性能

主要步骤:
1.测试所做的修改
2.回归测试
3.记录变化

除了修复问题,修改中不能引入新的故障
最大的问题:修改后没有足够的文档记录和测试

软件可维护性的类型:
在这里插入图片描述
软件演化:对软件进行持续的更新,软件的大部分成本来自于维护阶段

软件维护不仅仅是运维工程师的工作,而是从设计和开发阶段就开始了
在设计与开发阶段就要考虑将来的可维护性

面向可维护性的构造技术:
1.模块化设计
2.OO设计原则
3.OO设计模式
4.基于状态的构造技术
5.表驱动的构造技术
6.基于语法的构造技术

2 可维护性的指标

可维护性:软件系统或组件易于修改以纠正故障、提高性能或其他属性,或适应已改变的环境”。

可拓展性:软件设计/实施考虑到未来的增长,可拓展性被视为扩展系统能力和实施扩展所需努力水平的系统衡量。

灵活性:软件根据用户需求、外部技术和社会环境等而轻松改变的能力。

可适应性:交互系统(自适应系统)的能力,可以根据获取的用户及其环境信息来适应个别用户的行为。

可管理性:监控和维护软件系统的效率和容易,以保持系统运行、安全和平稳运行。

支持性:基于包括质量文档、诊断信息和知识渊博的技术人员的资源,在部署后软件如何有效地运行。

在这里插入图片描述
在这里插入图片描述
Halstead Volume:基于源代码中(不同的)运算符和操作数数量的复合度量。

3 模块化设计和模块化原则

目的:将系统划分为模块,并用一种模块内高内聚,模块间低耦合的方式分配各模块的职责

模块化降低了程序员在任何时候都必须处理的总复杂性,做到分离关注点和信息隐藏

评估模块性的五个标准:
1.可分解性(Decomposability):较大的组件是否已分解为较小的组件
2.可组合性(Composability):较大的组件是否可以由较小的组件构成
3.可理解性(Understandability):组件是否可以单独理解
4.可持续性(Continuity):对规约的小改变是否只影响本地优先数量的组件
5.出现异常之后的保护(Protection):运行时异常的影响是否局限于少量的相关组件

五个模块化设计的规则:
1.直接映射(Direct Mapping)
2.尽可能少的接口(Few Interfaces)
3.尽可能小的接口(Small Interfaces)
4.显式接口(Explicit Interfaces)
5.信息隐藏(Information Hiding)

耦合性:耦合是衡量模块之间依赖关系的度量方法
模块间耦合性程度由模块之间的接口数量和每个接口的复杂性决定(复杂性由通信的类型决定)

内聚性:是衡量模块功能或责任的紧密相关的标准,如果一个模块的所有元素都在朝着相同的目标工作,那么该模块就具有很高的内聚性

好的设计需要高内聚,低耦合,但有时需要折中,耦合程度高时,内聚内聚程度倾向于变低,反之亦然

4 OO设计原则:SOLID*

SOLID:五大类的设计原则
The Single Responsibility Principle 单一责任原则(SRP)
The Open-Closed Principle 开放-封闭原则(OCP)
The Liskov Substitution Principle Liskov替换原则(LSP)
The Dependency Inversion Principle 依赖转置原则(DIP)
The Interface Segregation Principle 接口聚合原则(ISP)

单一责任原则(SRP)

SRP:不应该有多于1个原因让你的ADT发生变化,否则就拆分开;一个类,一个责任
责任:变化的原因

如果一个类包含了多个责任,那么将引起不良后果:
1.引入额外的包,占据资源
2.导致频繁的重新配置、部署等
在这里插入图片描述

开放-封闭原则(OCP)

开放:对拓展性的开发,模块的行为应是可扩展的,从而该模块可表现出新的行为以满足需求的变化

封闭:对修改的封闭,模块自身的代码是不应被修改的;扩展模块行为的一般途径是修改模块的内部实现;如果一个模块不能被修改,那么它通常被认为是具有固定的行为

关键的解决方案:抽象技术

“软件实体(类、模块、功能等)。应该开放扩展,但关闭修改”,即使用继承和组合/委托更改类的行为
在这里插入图片描述

Liskov替换原则(LSP)

LSP:子类型必须能够替换其基类型
派生类必须能够通过其基类的接口使用,客户端无需了解两者之间的差异

接口隔离原则(ISP)

ISP:不能强迫客户端依赖于他们不需要的接口,只提供必需的接口

不要用许多方法污染接口,避免臃肿的接口,客户端不应该被强迫依赖他们不需要的方法
接口属于客户端,而不是体系结构

接口臃肿的类是接口不够聚合的类,胖接口可以分解为多个小接口;不同的接口向不同的客户端提供服务;客户端只访问自己所需要的接口

图示:在这里插入图片描述

依赖转置原则(DIP)

DIP:抽象的模块不应该依赖于具体的模块,具体的模块应该依赖于抽象的
大部分的接口和抽象都应该被使用

delegation时,要通过接口建立联系,而非具体子类
在这里插入图片描述
注:本节SOLID五个原则都应该了解

5 语法驱动的构造*

语法的构成要素

有一类应用,从外部读取文本数据,在应用中做进一步处理。
特定的,字节或字符序列应该有如下特性:
1.输入文件有特定格式,程序需读取文件并从中抽取正确的内容
2.从网络上传输过来的信息,遵循特定的协议
3.用户在命令行输入的指令,遵循特定的格式
4.内存中存储的字符串,也有格式需要

通常使用语法分析来判断字符串是否合法,并解析成程序里使用的数据结构,且这个数据通常是一个递归的数据结构

一个语法定义了一个字符串集合
语法中的文字字符串被称作终止节点,他们是语法解析树的叶节点,无法再向下扩展,通常被表示为字符串

一个语法由一个产生式节点的集合描述,其中每一个产生式都定义了一个非终止节点,并遵循特定规则,利用操作符、终止节点和其他非终止节点,构造新的字符串,非终止节点是树中表示字符串的内部节点

一个语法中的产生式有如下形式:
nonterminal ::= expression of terminals, nonterminals, and operators

根节点:是一个语法中的非终止节点,语法识别出的字符串集合中都是与根节点匹配的字符串

语法中的运算*

三种基本语法运算:
连接(Concatenation) - x::= y z - x匹配y后接z的字符串
重复(Repetition) - x::=y* - x匹配零个或多个y
选择(Union) - x::= y | z - x要么匹配y要么匹配z

运算优先级:
1.通常情况下,前缀运算符*, ?, +有最高的运算优先度
2.连接其次
3.选择优先度最低
但圆括号可以被用作修改优先级,括号内的优先运算

其他语法运算符:
可选(Optional) - x ::= y+ - x要么是y要么为空
一次或多次出现 - x ::= y+ - x是一个或多个y的连接
一个字符类 - x ::= […] - 表示包含方括号中列出的任何字符的长度为1的字符串,例如
x ::= [a-c] 等价于 x ::= ‘a’ | ‘b’ | ‘c’
一个倒置的字符类 - x ::= [^…] - 表示包含括号中未列出的任何字符的长度为1的字符串,例如
x ::= [^a-c] 等价于 x ::= ‘d’ | ‘e’ | ‘f’ | …

语法中的递归

在这里插入图片描述

分析树

分析树:根据语法匹配将一个字符串生成一个能显示字符串中的各部分如何和语法中各部分联系起来的树
树的叶节点标记着终止符号,代表已经被分析完的字符串部分,如果从左到右将叶节点连接,将获得原字符串

正则语法和正则表达式*

正则语法:简化之后可以表达为一个产生式而不包含任何非终止节点

正则表达式:终端和运算符的约简表达式可以写成一种更紧凑的形式,称为正则表达式。
正则表达式会去掉了终端周围的引号以及终端和运算符之间的空格,因此它只由终端字符、用于分组的括号和运算符字符组成。

一些正则表达式的运算:
. - 任意字符
\d - 任意数字
\s - 任意空白符
\w 任意有意义的字符,相当于[a-zA-Z_0-9]
. ( ) * + … - 表示反斜杠后的字符

注:本节只需要学会正则表达式即可

第十一章 面向可复用性和可维护性的设计模式

1 创建模式*

工厂方法,也被称作虚拟构造器,当client不知道要创建哪个具体类的实例,或者不想在client代码中指明要具体创建的实例时,用工厂方法。
可以定义一个用于创建对象的接口,让其子类来决定实例化哪个类,从而使一个类的实例化延迟到其子类

在这里插入图片描述
带有工厂方法的类也可以实现其他功能
有新的具体产品类加入时,可以在工厂类里修改过增加新的工厂函数(OCP),不会影响客户端代码
客户端使用工厂方法来创建实例,得到的实例的类型是抽象接口而非具体类
根据类型决定创建哪个具体产品

优点:
消除了将特定于应用程序的类绑定到代码的需要。
代码仅处理产品接口,因此它可以处理任何用户定义的具体产品

潜在缺点:
客户可能必须创建构造类的子类,以便他们可以创建特定的产品。

注:本节了解一下就行

2 结构模式*

适配器模式(Adapter Pattern)

将某个类/接口转换为客户端期望的其他形式,适配器使由于不相容的接口导致本不能一起使用的类可以一起使用。一般通过增加一个接口,将已存在的子类封装起来,客户端面向接口编程,从而隐藏了具体子类。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

装饰器模式(Decorator)

装饰器用于为对象添加不同侧面的特性,对于每一个特性构造子类,通过委派机制增加到对象上
装饰器使用了子类型和委派

在这里插入图片描述
Collections.unmodifiedSet等通过装饰器实现,修改原数据依然会导致所得到的添加特性后的对象改变
在这里插入图片描述
在这里插入图片描述
客户端需要一个具有多种特性的object,可以通过一层一层的装饰来实现
在这里插入图片描述
注:本节需要简单理解一下

3 行为模式*

策略模式(Strategy)

问题:有多种不同的算法来实现同一个任务,但需要客户端根据需要动态切换算法,而不是写死在代码中

实现方法:为不同的实现算法构造抽象接口,利用delegation,运行时动态传入客户端倾向的算法实例

优点:
易于扩展到新的算法实现
将算法与客户端上下文进行分离
在这里插入图片描述

模板模式(Template Method)

问题:做事情的步骤一样,但具体方法不同

实现方法:共性的步骤在抽象类内公共实现,差异化的步骤在各个子类中实现

模板模式使用继承和重写方法组合的策略来实现

在这里插入图片描述

迭代器模式(Iterator)

问题:客户端希望遍历被放入容器/集合类的一组ADT对象,无需关心容器的具体类型

解决方案:一种专为迭代的策略模式

作用:
1.隐藏底层容器的内部实现
2.支持具有统一接口的多个遍历策略
3.易于更改容器类型
4.促进项目各部分之间的沟通

在这里插入图片描述
Iterable接口:实现该接口的集合对象是可迭代遍历的
Iterator接口:迭代器

迭代器模式:让自己的集合实现Iterable接口,并实现自己的独特Iterator迭代器,允许客户端利用这个迭代器进行显示或隐式的迭代遍历:
在这里插入图片描述

访问器模式(Visitor)

Visitor pattern:对特定类型的object的特定操作(visit),在运行时将二者动态绑定到一起,该操作可以灵活更改,无需更改被visit的类
本质上:将数据和作用于数据上的某种/些特定操作分离开

效果:为ADT预留一个将来可拓展功能的“接入点”,外部实现的功能代码可以在不改变ADT本身的情况下通过delegation接入ADT

在这里插入图片描述
在这里插入图片描述
只要更换visitor的具体实现,即可切换算法

在这里插入图片描述
在这里插入图片描述
注:本节内容都比较重要,需要会根据各种设计模式写代码

4 设计模式的共性与差异*

共性样式1:只使用继承,不使用委派
核心思路OCP/DIP,依赖反转,客户端只依赖“抽象”,不能依赖于”具体“
发生变化时最好是扩展,而非修改

代表:Adaptor、Template
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
共性样式2:在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
注:本节需要理解

第十二章 面向正确性与健壮性的软件构造

1 什么是健壮性和正确性?

健壮性(Robustness):系统在不正常输入或不正常外部环境下仍能够表现正常的程度

面向健壮性的编程:
1.处理未期望的行为和错误终止
2.即使终止执行,也要准确/无歧义的向用户展示全面的错误信息
3.错误信息有助于debug

健壮性原则:
1.总是假定用户恶意,假定自己的代码可能失败
2.把用户想象成白痴,可能输入任何东西
3.对别人宽容点,对自己狠一点
对自己的代码要保守,对用户的行为要开放

健壮性编程的原则:
1.封闭实现细节,限定用户的恶意行为
2.考虑极端情况,没有不可能

正确性(Correctness):程序按照spec加以执行的能力,是最重要的质量指标

正确性 - 永不给用户错误的结构
健壮性 - 尽可能保持软件运行而不是总是退出
正确性倾向于直接报错(error),健壮性则倾向于容错(fault-tolerance)

在这里插入图片描述
健壮性:让用户变得更容易:出错也可以容忍,程序内部已有容错机制
正确性:让开发者变得更容易:用户输入错误(不满足precondition的调用),直接结束。

对外的接口,倾向于健壮;对内的实现,倾向于正确

安全关键型应用程序相比于健壮性倾向于支持正确性
消费者的应用程序相比于正确性倾向于支持健壮性

可靠性(Reliability):系统在规定条件下执行其所需功能的能力 - 故障之间的平均时间长度。
Reliability = Robustness + Correctness

提高健壮性和正确性的步骤:
0.使用断言、防御性编程、代码审查、正式验证等来编写具有健壮性和正确性的代码
1.观察故障症状(内存转储、堆栈跟踪、执行日志、测试)
2.识别潜在的故障(错误定位、调试)
3.修复错误(代码修订)

在这里插入图片描述

2 如何量度健壮性和正确性

外部观察角度:
Mean time between failures 平均失效间隔时间(MTBF):系统运行过程中系统固有故障之间的预期运行时间。
MTBF描述了可修复系统的两次故障之间的预期时间,而 mean time to failure 平均故障时间(MTTF)表示不可修复系统的预期故障时间。在这里插入图片描述
内部观察角度:
残余缺陷率:每千行代码中遗留的bug数量

3 Java中的Error和Exception*

在这里插入图片描述
内部错误:程序员通常无能为力,一旦发生,想办法让程序优雅的结果
异常:程序的问题,可以捕获、可以处理

Error种类:用户输入错误、设备错误、物理限制
大多数情况下,Error不需要被实例化
在这里插入图片描述
注:本节了解一下就行

4 异常处理*

使用try - catch结构
在这里插入图片描述
异常:程序执行中的非正常时间,程序无法再按预想的流程执行

Exception会将错误信息传递给上层调用者,并报告”案发现场“的信息
Exception可以是return之外的第二种退出途径 - 当找不到异常处理程序时,整个系统会完全退出

在这里插入图片描述

异常可以被分为RuntimeException和其他类型
运行时异常:由程序员在代码中处理不当造成,是程序源代码中引入的故障造成的,如果在代码中提前进行验证,则这些故障就可以避免

其他异常:由外部原因造成,是程序员无法完全控制的外在问题所导致的,即使在代码中提前加以验证(文件是否存在等),也无法完全避免失效发生

在这里插入图片描述
当异常产生时,要么异常处理程序处理了异常,要么会告诉编译器无法处理该异常,并在命令行中输出错误信息。编译器可以帮助检查程序是否已经抛出或处理了可能的异常。

Error和RuntimeException并不被编译器进行检查,

RuntimeException不需要在编译时用try catch等机制处理,但执行时可能会导致程序失败,代表程序中的潜在bug - 类似动态检查

而其他Exception则必须捕获并指定错误处理程序,否则编译无法通过 - 类似静态类型检查

Java中异常处理的关键字:try、catch、finally、throws、throw
throws:表明方法会抛出XX异常
throw:抛出XX异常
try、catch、finally:捕获并处理XX异常

Unchecked异常也可以使用throws声明或try/catch进行捕获,但大多数时候是不需要的,也不应该这么做 - 掩耳盗铃,对发现的编程错误充耳不闻

如果客户端可以通过其他的方法恢复异常,那么采用checked exception;
如果客户端对出现的这种异常无能为力,那么采用unchecked exception;
异常出现的时候,要做一些试图恢复它的动作而不要仅仅的打印它的信息

在这里插入图片描述
“异常”也是方法和client端之间spec的一部分,在post-condition中刻画

对于checked exception,需要在方法声明后加throws XX异常
程序员必须在方法的spec中明确写清本方法会抛出的所有checked exception,以便于调用该方法的client加以处理
而unchecked exception则不需要

若一个程序内部某方法可能抛出某checked exception,则要么它内部有该异常的处理程序,要么它也声明会抛出异常,将异常移交给调用它的方法处理。若没有处理程序来处理checked exception,程序将终止运行

程序员不应抛出Error,Error通常只指虚拟机或动态库的错误

子类型多态相关:
1.如果子类型中override了父类型中的函数,那么子类型中方法抛出的异常不能比父类型抛出的异常类型更宽泛
2.子类型方法可以抛出更具体的异常,也可以不抛出任何异常
3.如果父类型的方法未抛出异常,那么子类型的方法也不能抛出异常。

抛出异常方法:
在这里插入图片描述
流程:
1.找到到一个能表达错误的Exception类/或者构造一个新的Exception类
2.构造Exception类的实例,将错误信息写入
3.抛出它

一旦抛出异常,方法不会再将控制权返回给调用它的client,因此也无需考虑返回错误代码

构造异常类:
对于checked exception,只需要继承Exception类
对于unchecked exception,只需要继承RuntimeException类

异常处理程序:
若某段代码中可能抛出异常,可以将这段代码包含在try代码块内,并使用catch捕获其抛出的各种异常。若在各catch代码段后有finally代码段,程序会在执行完catch代码段后执行finally代码段,另外,没有抛出异常时finally代码段也会被执行。

finally代码段通常用于清理异常发生前曾申请过的资源

重新抛出异常:catch代码段中也可以抛出异常
目的:更改exception的类型,更方便客户端获取错误信息并处理
在这里插入图片描述
调用栈追踪:
在这里插入图片描述

在这里插入图片描述

注:本节需要学会ava中的两类异常,并掌握他们区别,并掌握异常抛出、处理等相关代码的编写,还需要了解异常处理观察者控制的转移路径,以及异常导致程序退出时显示的的方法调用栈含义

5 断言

断言:当不满足前提条件时,此代码通过抛出断言错误异常来终止程序。调用者的错误的影响无法传播。检查前置条件是防御式编程的一种典型形式

为什么要断言:
在开发阶段的代码中嵌入,检验某些“假设”是否成立。若成立,表明程序运行正常,否则表明存在错误。

断言即是对代码中程序员所做假设的文档化,也不会影响运行时性能
(在实际使用时,assertion都会被disabled)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
断言为了程序正确性,使用断言处理“绝不应该发生”的情况,如果来自于自己
所写的其他代码,可以使用断言来帮助发现错误
异常为了程序健壮性,使用异常来处理你“预料到可以发生”的不正常情况,如果参数来
自于外部(不受自己控制),使用异常处理

6 防御式编程

防御性编程是一种防御性设计的形式,旨在确保一块软件在不可预见的情况下的持续功能。

在这里插入图片描述

7 SpotBugs工具

SpotBugs是一个使用静态分析来查找Java代码中的bug的程序

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值