哈工大 软件构造课程 复习考点总结(第六、七章)
可维护性的常见度量指标
- Cyclomatic complexity 圈复杂度
- Lines of Code LoC 代码行数
- Maintainability Index (MI) 可维护性指数
- Depth of Inheritance 继承的层次数
- Class Coupling 类之间的耦合度
- Unit test coverage 测试代码覆盖率
Coupling 耦合度 and Cohesion 聚合度
- Coupling 耦合度:
模块之间的依赖性。
- Conhesion 聚合度
功能专一性。高聚合度:模块的所有元素作用于同一目标。
SOLID 设计原则
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 接口聚合原则
- SRP 单一责任原则
一个类仅有一个责任;
至多有1个原因使得一个类发生变化。
- OCP 开放-封闭原则
- 对扩展性开放
模块的行为可扩展,从而该模块可表现出新的行为以满足需求的变化。
- 对修改封闭
模块自身原本的代码不应被修改。
扩展模块行为的一般途径是修改模块的内部实现。
如果一个模块不能被修改,那么通常认为其具有固定的行为。
实现方案:抽象:继承、组合
注意:
避免使用 if-else / switch-case 语句。
- LSP liskov替换原则
- DIP 依赖转置原则
低层模块依赖于高层模块,具体依赖于抽象(抽象类、接口)。
- ISP 接口聚合原则
客户端不应被迫依赖于它们不需要的方法。
客户端不需要实现(接口中)不需要的方法。
实现方案:将"胖"接口分解为若干个小接口,不同的接口向不同客户端提供服务。
语法、正则表达式
Productions 产生式节点,也称nonterminal非终止节点。(中间变量)
Terminal 终止节点。(终止字符)
Root 根节点。(起始字符)
正则语法:简化之后可以表达为一个产生式而不包含任何非终止节点。
Java中:
匹配:
Pattern . matcher ( regex , string)
或 Pattern pattern = Pattern.compile( regex ) ;
Matcher m = pattern.matcher (string) ;
m.matcher().
捕获:
Pattern parse = Pattern . compile (regex) ;
Matcher match = parse.matcher(string) ;
match.find() ;
match.group(index)
设计模式
Creational patterns
Factory method pattern 工厂方法模式
当client不知道要创建哪个具体类的实例,或不想在client代码中指明要具体创建的实例时,采用工厂方法。
定义一个用于创建对象的接口,让其子类来决定实例化哪一个类,从而使一个类的实例化延迟到其子类。
有新的子类时,只需添加新的工厂方法。(OCP)
Abstract factory pattern 抽象工厂模式
提供接口来创建一组相互关联/相互依赖的对象,但不需要指明其具体类。
抽象工厂接口的每个具体实现类实现一种具体的产品生产搭配。
抽象工厂创建的不是一个完整产品,而是"产品族"(遵循固定搭配规则的多类产品的实例),
得到的结果是:多个不同产品的对象,各产品创建过程对client可见(client或辅助类中调用工厂实现类的相关方法组装具体部件,但每块部件到底选择哪种类型实现,取决于工厂类的搭配规则。),但"搭配"不能改变。
本质上,Abstract Factory 是把多类产品的factory method组合到一起,抽象出配置每块部件的方法。
Builder pattern 构造器模式
创建复杂对象,包含多个组成部分。
创建一个完整的产品,有多个部分组成,client不了解各部分如何创建、各部分如何组合,最终得到一个完整产品对象。
Structural patterns
Bridge 桥接模式
通过delegation + inheritance 建立两个具体类之间的关系。(DIP 依赖转置,抽象依赖于抽象)
实例化abstraction时,new RefinedAbstraction(左侧继承树的子类)并将接口实现类传入构造函数(右侧继承树的子类)。
Proxy Pattern 代理模式
某个对象比较具有高隐私性,不希望client直接访问,设置proxy,在二者间建立防火墙。
隔离对复杂对象的访问,降低难度/代价,定位在"访问/使用"行为。
Composite 组合模式
递归组合,组合对象生成树结构,来体现层级关系(如职位等级图)。
在同类型的对象之间建立起树型层次结构,一个上层对象可包含多个下层对象。
Behavioral patterns
Observer
"粉丝"(observer)对"偶像"(subject)感兴趣,希望随时得知偶像的一举一动;
- 粉丝到偶像那里注册(obsever关联subject , 并调用subject的相关方法将自身加入其"粉丝"容器),
- 偶像一但有新闻发生,就推送给已注册(容器中的对象)的"粉丝"(回调observer的特定功能)。
在java中:
Observer 接口,实现该接口,构造"粉丝"。
Observable抽象类,派生子类,构造"偶像。"
Visitor pattern
对特定类型的object的特定操作(visit),在运行时将二者动态绑定到一起,该操作可以灵活更改,无需更改被visit的类。
本质上:将数据和作用于数据上的某种/某些特定操作分离开来。
类似strategy,不过visitor对ADT本身进行操作。
State pattern 状态模式
具体状态实现state接口,根据操作state不同实现之间转换。
Memento pattern 备忘录模式
记录originator对象的历史状态,并可恢复记录或删除。
Robustness 健壮性 和 Correctness 正确性
健壮性 | 正确性 |
系统在不正常输入或不正常外部环境下仍能够表现正常的程度。 | 程序按照spec加以执行的能力,是最重要的质量指标。 |
出错后:·1、退出(并提示信息) 2、容错(转为正常) | Fail fast |
让用户变得更容易:出错也可以容忍,程序内部已有容错机制。 | 让开发者变得更容易:用户输入错误(不满足precondition的调用),直接结束。 |
对外部接口,开放(倾向于健壮) | 对内部逻辑:保守(倾向于正确) |
可靠性reliability = robustness + correctness
Error --> defeat/fault / bug -->failure
Mistake 程序员犯的错误 缺陷 失效,运行时程序的外部表现
对程序运行结果(Throwable):
- Error 程序员无能为力(设备、I/O、网络、JVM等)
- Exception 程序员自身引入:捕获、处理
- Return (正常)
- Throws (非正常)
Thorwable
- Error 内部错误:java运行时系统内部错误或资源不足
程序员无能为力,一旦发生,想办法让程序优雅的结束。
分类:用户输入错误、设备错误、物理限制等
- Exception 异常:程序捕捉到的error(不是Error)
程序导致的问题,可以捕获、可以处理。
Exception
异常:程序执行中的非正常事件,程序无法再按预想的流程执行。
程序若找不到异常处理程序,则退出,在控制台打印出statck trace。
Runtime Exception 运行时异常,有程序代码、或I/O等外部原因造成,不需要捕获/处理。
Checked and Unchecked exceptions
从异常处理机制的角度进行分类:异常被谁check? 编译器/程序员
Unchecked异常:Error + RuntimeException
程序代码产生,编译正常,可以不处理,抛出则程序失败。(类似 dynamic type checking)
Checked 异常:
必须捕获并制定错误处理机制,否则编译无法通过。(类似 static type checking)
如何选择? 对于一个异常:
如果客户端可以通过其他的方法恢复异常,则采用checked exception;
如果客户端对出现的异常无能为力,则采用unchecked exception。
异常处理
根据LSP:
父类抛出异常与子类协变,或子类不抛出异常;
若父类不抛出异常,则子类不抛出并自行处理异常。
Try-catch-finally :
无论程序是否碰到异常(无论Exception还是Error(包括assertion)),finally均会被执行。
(try 中 return 后,finally中若有return则,则依然执行,若无则在try执行return前执行)
(若 try或catch中强制退出(exit),则finally无法执行)
Try-with-resource:
try-with-resources这种声明方式指定了一个或多个资源,而且这些资源需要在程序结束的时候进行关闭。这种方式可以确保每个指定的资源都可以在声明结束的时候进行关闭(就是在try(){}结束的时候)。但是前提是这些资源必须实现接口java.lang.AutoCloseable(其中包括实现了java.io.Closeable接口的所有对象),原因是java.io.Closeable接口继承了java.lang.AutoCloseable接口。
如上图,BufferedReader 在try结束后自动关闭资源(br.close()).
Assertion 断言机制
Assert (布尔值) : (输出字符串) ;
Assert false 则打印输出字符串,并throw AssertionError
断言主要用于开发阶段,为了调试程序、尽快避免错误。
什么时候用断言:
- Inernal invariants 内部不变量
- Rep invariants 表示不变量
如,验证private Boolean checkRep();
- Control-Flow Invariante 控制流不变量
判定不能到达的位置,如switch-case中的default语句
- 方法的前置条件
对public方法:使用异常处理(因为是不受控制的外部条件)
对private方法:使用断言(对内严格)
- 方法的后置条件
断言 维护正确性,使用断言处理"绝不应该发生"的情况;
错误/异常处理 维护健壮性,处理"预料到可以发生"的不正常情况。
Debug 调试
识别错误根源,消除错误
预防:防御式编程、assertion、exception
Debug: test -> debug ->
重现 -> 诊断(假设检验,分治法) -> 修复 -> 反思(是否存在相似的bug等)
Git bisect (诊断方法) 在输入的正确commit和错误commit之间二分查找,定位出错的commit。
常用手段:
Print(到处print)、stack tracing(输出调用栈),memory dump(内存导出分析),logging(记录日志),
断点调试。
Testing 测试
探测是否存在错误。
测试的等级:单元测试、集成测试、系统测试、验收测试,回归测试。
白盒测试:对程序内部代码结构的测试。
黑盒测试:对程序外部表现出来的行为的测试。
测试优先编程:先写spec,再写符合spec的测试用例,写代码、执行测试、发现问题并修改、再测试直到通过。
Code coverage 代码覆盖度: 测试效果/测试难度:路径覆盖(所有可能的分支、路径)>分支覆盖>语句覆盖
黑盒测试
检查程序是否符合规约。
用尽可能少的测试用例,尽快运行,并尽可能大的发现程序的错误。
- 等价类划分
将被测函数的输入域划分为等价类,从等价类中导出测试用例。
针对每个输入数据需要满足的约束条件,划分等价类。
每个等价类代表着对输入约束加以满足/违反的有效/无效数据的集合。
- 边界值分析 BVA(Boundary Value Analysis)
在等价类划分时,将边界作为等价类之一加入考虑。不仅考虑边界,还要考虑边界两侧(稍微偏离边界)。