第五章
5.1
框架
白盒框架(whitebox framework):内部可见
- 扩展性通过继承和动态绑定来实现。
- 已存在的功能通过继承和重写钩子方法(hook method)来扩展。
- 重写钩子方法时通常使用设计模式,如模板方法模式。
黑盒框架(blackbox framework):内部不可见
- 扩展性通过定义容器接口来实现。
- 已存在的功能通过定义符合特定接口的容器来重用。
- 框架通过委托来集成上述容器。
里氏代换原则(LSP)
里氏代换原则:对于类型为T的对象x,令q(x)表示x具有的某一特性,则当S是T的子类型时,类型为S的对象y也应具有特性q(y)。通俗地说,每一个父类对象替换为子类对象后必能实现完全相同的功能。
Java编译器对LSP的保证
- 子类可以添加,但不可删除父类的方法。
- 具体类必须实现所有未定义的方法。
- 重写方法的返回类型必须为原方法返回类型或原方法返回类型的子类。
- 重写方法必须接受相同的参数类型。
- 重写方法不能抛出更多的异常,或原方法所抛异常的父异常。
LSP的其他要求(编译器无法检查)
- 子类比父类的不变量应当相等或更强。
- 子类的每个方法的后置条件应当比父类的对应方法的后置条件相等或更强。
- 子类的每个方法的前置条件应当比父类的对应方法的前置条件相等或更弱。
以自定义方式排序
实现方式
- 继承Comparator以实现比较器。
- 实现Comparable以实现比较器,不需要构建新的Comparator类,比较代码放在ADT内部。
- 使用lambda表达式(a)->boolean以实现比较器。
设计模式
-
适配器模式(adapter)
系统所需的接口与已有类的接口不一致,但功能相同时,提供接口之间的转换,使得系统能够使用已有类。
-
装饰器模式(decorator)
已知一接口AbstractComponent及其实现类Component,欲为Component添加功能,可创建同样实现接口B的抽象类AbstractDecorator(抽象装饰器),在抽象装饰器中保存一个实现AbstractComponent的实例对象以让抽象装饰器可以实现AbstractComponent要求的所有操作。之后新建继承自抽象装饰器的Decorator(称为装饰器),此时即可在装饰器中添加新方法。
-
外观模式(facade)
客户端使用子系统中的多个组件易导致混乱时,为组件的所有功能设置一个界面类(图中为Facade类),客户端仅通过界面类提供的方法操作各个组件协同工作。
-
策略模式(strategy)
一个功能(方法)可能以不同的策略实现,希望在运行时灵活切换策略,可将方法实现的策略实现为策略类(图中为若干ConcreteStrategy类),不同策略类实现相同的接口(图中为Strategy接口),但拥有不同的实现。
-
模板模式(template)
一个过程的每个步骤功能是确定的,但某些功能有多种实现方式,以不同的方式实现可获得不同的具体过程。此时将该过程实现为抽象类的一个方法(图中为work()方法),其调用一些抽象方法(图中为若干step()方法),而抽象方法交由具体的子类实现,这样每个具体子类的work()方法就是以某种方式实现的一个具体的过程。
-
迭代器模式(iterator/iterable)
一个用于表示若干对象之集合的类(图中为Aggregate类)对外提供遍历功能时,可建立一个对应的迭代器类(图中为Iterator类)。迭代器提供依序访问该集合中所有对象的功能。
-
工厂模式(factory)
希望通过提供参数灵活控制对象的创建过程(例如提供某参数时建立Product1对象,否则建立Product2对象),可创建一个工厂类(图中为Factory类)以负责创建对象。工厂类提供工厂方法,接受一定的参数,根据参数创建相应的对象并返回。
-
抽象工厂模式(abstract factory)
若干种不同组件(图中为ProductA类和ProductB类)需要搭配在一起使用,因而需要一起生产。每种组件又有若干等级(图中等级表现为数字),不同种的组件并不能随意搭配,一种某等级的组件只能搭配另一种某特定等级的组件(例如图中A1仅能与B2搭配,A2仅能与B1搭配)。为了实现该种逻辑,将每一种可能的搭配实现为一个工厂(图中为FactoryA1B2类与FactoryA2B1类)。由于每一种组件在不同工厂中生产的结果只是等级不同,而种类相同,因此所有这样的工厂可全部继承自一个抽象工厂(图中为AbstractFactory类)。抽象工厂可提供建造具体工厂的方法,而具体工厂负责生产合法搭配过的产品。
-
观察者模式(observer)
当某对象(称为被观察者,图中为Worker)的状态改变时,其它一些对象(称为观察者,图中为ConcreteObserver)的状态必须立即随着改变。可为观察者对象实现观察者(图中为Observer)接口,当被观察者变化时,被观察者通过观察者接口通知它的所有观察者进行变化。
-
访问者模式(visitor)
受限于Java的实现机制,当需要根据传入对象所属的类来确定究竟执行哪种操作时,方法重载不能达到目的,只能使用instanceof进行运行时类型判断。为了通过面向对象的方式解决问题,考虑为所有传入对象实现Visitable接口,即为每个对象实现一个accept(Visitor)方法,通过令传入对象反向调用访问者的方式,可以利用Java多态解决这一问题。
缺点为破坏了类的开闭原则,外部的逻辑侵入了类内部。
-
状态模式(state)
某类对象(图中为Context类对象)在有限种状态(图中为若干StateX类)间切换,在不同状态时调用相同方法可能导致不同的行为,并切换到不同的状态。即对象的行为受当前状态影响,同时也会改变当前状态。
题型:写出设计模式思想或画图。
第六章
可维护性
聚合度与耦合度
聚合度(cohesion):模块内部各组件联系程度的度量。取决于模块中各组件是否为同一目标工作。
耦合度(coupling):各个模块之间联系程度的度量。包括模块间接口的数目及每个接口的复杂程度。
好的软件设计要求高聚合度、低耦合度。
SOLID原则
- 单一责任原则:每个类应当且仅应当完成一件事。
- 开闭原则:软件实体(类、模块、函数等)应当对扩展开放,而对修改关闭。
- 里氏代换原则:见第五章里氏代换原则。
- 接口分离原则:不应强迫类实现其无法实现的方法;接口不应具有过多的方法。
- 依赖倒置原则:高层模块不应依赖底层模块;抽象不应依赖于具体,具体应当依赖于抽象。
正则表达式
常见正则表达式元字符
元字符 | 含义 |
---|---|
. | 任意单个字符 |
\d | 任意数字字符,等价于[0-9] |
\s | 任意空白符,包括空格符、制表符和换行符 |
\w | 任意单词中符号,包括字母和数字 |
* | 匹配前面的子表达式0至若干次 |
? | 匹配前面的子表达式0或1次 |
题型:给出具体例子,要求写出与之匹配的正则表达式。
例题及解答
1. 写出尽可能短的正则表达式,使得它能够从字符串中移除仅含单个单词的,由小写字母组成的HTML标签:
String input = "The <b>Good</b>, the <i>Bad</i>, and the <strong>Ugly</strong>"; String regex = "_______"; //填空 String output = input.replaceAll(regex, "");
2. 若期望的输出为"The Good, the Bad, and the Ugly",能填入的最短正则表达式是什么?
1. 填入:</?[a-z]+>
2. 填入:<.+?>
第七章
健壮性和正确性
健壮性/鲁棒性(robustness)
定义:系统或组件在非法输入或高压环境中依然保持正确功能的程度。
实现:尽量尝试能让软件继续执行下去,即使这意味着结果将变得不准确。
正确性(correctness)
定义:软件根据其规约行事的能力。
实现:永远不返回不精确的结果,没有结果好于结果不精确。
异常
Error与Exception
Java中所有异常对象都实现了Throwable接口,分为Error和Exception两大类。
Error:不是由于程序错误而是由于系统内部错误(例如JVM故障)或资源耗尽引起的异常。均为unchecked的。
Exception:由程序引发的异常。既包含checked异常也包含unchecked异常。其中unchecked异常全部为RuntimeException及其子类异常。
名词解释
Unchecked异常:由程序错误引起,不需要显式地throw和catch。例如NullPointerException。
Checked异常:不是由程序错误而是由于外部环境因素引起,可以预见,必须被每个调用者捕获和处理。例如IOException。checked异常在抛出前必须被声明抛出,在抛出后必须被catch。
Checked异常的处理机制
- 抛出:必须在方法声明中使用throws声明该异常将被抛出,然后才可在代码中使用throw抛出。
- 捕获:在调用者处通过try…catch进行捕获。
- 处理:在catch语句块中进行异常处理或再次抛出异常。
- 清理现场/释放资源:在finally块中进行现场清理和资源释放,无论try块的执行结果如何(即使是抛出未被catch的异常),finally块都会被执行。
自定义异常类
途径:定义继承自Exception类或其子类的异常类即可。
异常类的父子关系注意事项参考第五章里氏代换原则。
题型
- 给定异常代码,判断代码正误
- 怎样捕获异常
断言(assertion)
描述
一段允许程序在运行过程中检查自身的代码。检查内容可以包括前置条件、后置条件、不变式等。当断言不被满足时,JVM将抛出AssertionError异常。
语法
- assert condition; 检查表达式condition的值是否为true。
- assert condition : expression; 检查表达式condition的值是否为true,若不是则抛出的异常描述信息为expression。
注意事项
- 断言用于检查正常情况下永远不会出现的条件。异常用于检查非正常情况下偶尔可能出现的错误。
- 断言是书写前置条件和后置条件的良好方式。
- 断言在调试时打开(确保正确性),在交由客户运行时关闭(确保鲁棒性)。
调试
调试过程
- 复现(reproduce):找到能够方便地、可靠地使问题重复出现的方法。
- 诊断/定位(diagnose/locating):通过假设-验证的方式找出bug的出现原因。
- 修复(fix):修改源码以修复问题,同时维持软件整体的质量不变或提升软件质量。
- 反思(reflect):从该bug中寻找原因,学习经验。
调试方法
- 格式和逻辑检查
- 源代码比对
- 内存转储(memory heap dump)
- 打印调试
- 堆栈跟踪(stack trace)
- 编译器警告
- 调试器
- 执行分析器
- 测试专用框架
测试(重点考察黑盒测试)
黑盒测试用例设计
等价类划分
将输入划分为多个等价类,每个等价类内取一个用例进行测试。
划分方法
- 输入在一定范围内时,划分1个合法等价类和2个非法等价类。
- 输入需要一个特定值时,划分1个合法等价类和2个非法等价类。
- 输入需要某集合中的一个元素时,划分1个合法等价类和1个非法等价类。
- 输入是布尔值时,划分1个合法等价类和1个非法等价类。
边界值分析
在输入的边界值处进行测试。
选取方法
- 输入在以值a和b确定的范围内时,测试用例需要包含a、b以及恰好大于/小于a、b的情形。
- 输入需要一系列值时,测试用例需要包含最大值及最小值,同时也应设计超过最大值和低于最小值的用例。
- 上述策略也需应用至输出上。
- 若内部数据结构有容量限制,也应设计测试用例测试这一容量限制。
测试策略书写
写出划分出的等价类以及选取的边界值。
题型
- 撰写测试策略。
- 读测试策略后选择适当的输入。
测试覆盖度
描述:程序源代码中被测试的运行路径数目相对于运行路径总数目的比例。
题型:给定输入,写出执行了哪些语句。