章节 | 内容 |
---|---|
6 | 可维护性 |
7 |
6.1可维护性
Ready for Change,Ready for Extension
可扩展性,灵活性,可适应性
度量指标:
Cyclomatic Complexity 圈复杂度:代码的结构复杂度
程序路径条数,测试需要覆盖
Lines of Code 代码行数
Maintainability Index (MI) 可维护性指数
Depth of Inheritance 继承的层次数
Class Coupling 类之间的耦合度
Unit test coverage单元测试的覆盖度
6.2聚合度Cohesion与耦合度Coupling的trade-off
耦合是模块之间依赖关系的度量。
如果一个模块中的更改可能需要另一个模块中的更改,则两个模块之间存在依赖关系。
模块间耦合程度由:
-模块间接口数量(数量)
-每个接口的复杂度(由通信类型决定)(质量)
内聚性是度量一个模块的功能或职责之间的紧密程度。
如果一个模块的所有元素都朝着同一个目标工作,那么这个模块就具有很高的内聚性。
6.3SOLID
SOLID: 5 classes design principles
(SRP) The Single Responsibility Principle 单一责任原则
责任:变化的原因
不应有多于1个的原因使得一个类发生变化
一个类,一个责任
(OCP) The Open-Closed Principle 开放-封闭原则
- 对扩展性的开放:
模块的行为应是可扩展的,从而该模块可表现出新的行为以满足需求的变化 - 对修改的封闭
但模块自身的代码是不应被修改的
扩展模块行为的一般途径是修改模块的内部实现
如果一个模块不能被修改,那么它通常被认为是具有固定的行为
(LSP) The Liskov Substitution Principle Liskov替换原则
子类型必须能够替换其基类型
派生类必须能够通过其基类的接口使用,客户端无需了解二者之间的差异
(DIP) The Dependency Inversion Principle 依赖转置原则
抽象的模块不应依赖于具体的模块
具体应依赖于抽象
delegation的时候,要通过interface建立联系,而非具体子类
(ISP) The Interface Segregation Principle 接口隔离原则
胖接口可分解为多个小的接口
不同的接口向不同的客户端提供服务
客户端只访问自己所需要的端口
6.4设计模式
Creational patterns创建者模式
- 工厂模式(Factory Pattern)
创建对象时不会对客户端暴露创建逻辑,通过使用一个共同的接口来指向新创建的对象。 - 抽象工厂模式(Abstract Factory Pattern)是围绕一个超级工厂创建其他工厂。
该超级工厂又称为其他工厂的工厂。 - 建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。
Structural patterns结构性模式
4. 桥接(Bridge)
把抽象化与实现化解耦,使得二者可以独立变化。
Bridge vs. Strategy
前者:强调结构关系的动态绑定,对象与对象之间
一个类A的对象中有其他类B的对象作为其组成部分,在运行时通过delegation加以组合,并永久保存这种delegation关系。
后者:动态绑定,强调算法的动态调用,类与类之间
“算法”通常实现为“类的某个方法”的形式,strategy的目的并非在“调用算法的类”与“被调用算法所在的类”之间建立起永久联系,而只是帮助前者临时使用后者中的“算法”,前者无需永久
保存后者的实例。
5. 代理模式(Proxy Pattern)创建具有现有对象的对象,以便向外界提供功能接口。
避免二次加载RealImage。
6. 组合模式(Composite Pattern)把一组相似的对象当作一个单一的对象。
Composite vs Decorator
Composite: 目的是在同类型的对象之间建立起树型层次结构,一个上层对象可包含多个下层对象,例子:ceo,经理,员工
Decorator: 强调的是同类型对象之间的“特性增加”问题,它们之间是平等的,区别在于“拥有特性”的多少,每次decoration只能作用于一个object。
Behavioral patterns行为模式
7. 观察者模式(Observer Pattern)一个对象被修改时,则会自动通知它的依赖对象。
例子:微博大V与粉丝。
8. 访问者模式(Visitor Pattern)使用一个访问者类,它改变了元素类的执行算法,使得元素的执行算法可以随着访问者改变而改变。
Visitor vs Iterator
Iterator. 迭代器:以遍历的方式访问集合数据而无需暴露其内部表示,将“遍历”这项功能delegate到外部的iterator对象。
Visitor: 在特定ADT上执行某种特定操作,但该操作不在ADT内部实现,而是delegate到独立的visitor对象,客户端可灵活扩展/改变visitor的操作算法,而不影响ADT
Strategy vs visitor
二者都是行为模式,都是通过delegation建立两个对象的动态联系
– 但是Visitor强调是的外部定义某种对ADT的操作,该操作于ADT自身关系不大(只是访问ADT),故ADT内部只需要开放accept(visitor)即可,client通过它设定visitor操作并在外部调用。
– 而Strategy则强调是对ADT内部某些要实现的功能的相应算法的灵活替换。这些算法是ADT功能的重要组成部分,只不过是delegate到外部strategy类而已。
区别:visitor是站在外部client的角度,灵活增加对ADT的各种不同操作(哪怕ADT没实现该操作),strategy则是站在内部ADT的角度,灵活变化对其内部功能的不同配置。
9. 中介者模式(Mediator Pattern)提供了一个中介类,该类通常处理不同类之间的通信,用来降低多个对象和类之间的通信复杂性。
聊天室作为多个用户相互发信息的中介。
Observer vs Mediator
Observer pattern:一组object对另一个object B的状态变化感兴趣(1对多),所以对其进行观察。B维持着一个对自己感兴趣的object list,一旦自己发生变化,就通知这些object。双方地位并不对等,一方只是“通知”,另一方接到通知后执行特定行为。
Mediator pattern: 一组同类型的object,彼此之间相互发/收消息(多对多),不存在谁观察谁的问题,所有object都对等。每个object都维持一个mediator,将通讯的任务delegate给它,自己只负责send和receive即可,无需了解其他object,实现了“解耦”。
-Observer:自己来广播,其他对象接收;
-Mediator:第三方中介负责广播(类似于“邮件列表”)。
10. 命令模式(Command Pattern)请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。
Façade vs. Command
Façade: structural pattern
Command: behavioral pattern
均强调对某个复杂系统内部提供的功能的“封装”,对外提供简单的调用接口,简化client的使用,“隐藏”细节。
Command:强调将指令封装为了“对象”,提供了统一的对外接口
(execute) 。
Façade:没有显式的“对象”,仍然通过类的方法加以调用。
责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。
在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。
例如:log等级
Visitor vs. Chain of Responsibility
不是像传统类设计中将操作与数据捆绑在一起形成ADT,这两个设计模式都是将“数据”与“作用于数据上的客户端定制操作”分离开来
原因:操作可以灵活增加、运行时使用的操作可以动态配置、多个操作的执行次序可以动态变化
区别1:visitor只定义了一个操作,chain of responsibility定义了一组操作及其之间的次序
区别2:visitor中,client创建visitor之后传入ADT,ADT再将执行权delegate到visitor;chain of responsibility中,控制权完全在各个handler之间流转,ADT(request对象)完全感受不到。
行为型模式
1.状态模式(State Pattern)中,类的行为是基于它的状态改变的
创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象。
context 对象的状态delegation到状态类。
2.备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。
备忘录模式使用三个类 Memento、Originator 和 CareTaker。
Memento 包含了要被恢复的对象的状态。
Originator 创建并在 Memento 对象中存储状态。
Caretaker 对象负责从 Memento 中恢复对象的状态。
设计模式总结
Text: “manufacturer independent”, “device independent”, “must
support a family of products”
=> Abstract Factory Pattern
Text: “must interface with an existing object”
=> Adapter Pattern
Text: “must interface to several systems, some of them to be
developed in the future”, “an early prototype must be
demonstrated”
=>Bridge Pattern
Text: “must interface to existing set of objects”
=> Façade Pattern
Text: “complex structure”, “must have variable depth and width”
=> Composite Pattern
Text: “must be location transparent”
=> Proxy Pattern
Text: “must be extensible”, “must be scalable”
=> Observer Pattern
Text: “must provide a policy independent from the mechanism”
=> Strategy Pattern
正则表达式:
Pattern是对regex正则表达式进行编译之后得到的结果
Matcher:利用Pattern对输入字符串进行解析
7.1Robustness and Correctness健壮性与正确性
健壮性:系统在不正常输入或不正常外部环境下仍能够表现正常的程度
Robust programming 面向健壮性的编程
处理未期望的行为和错误终止
即使终止执行,也要准确/无歧义的向用户展示全面的错误信息
错误信息有助于进行debug
正确性:程序按照spec加以执行的能力,是最重要的质量指标!
区别
正确性:永不给用户错误的结果
让开发者变得更容易:用户输入错误,直接结束。(不满足precondition的调用)
健壮性:尽可能保持软件运行而不是总是退出
让用户变得更容易:出错也可以容忍,程序内部已有容错机制
对外的接口,倾向于健壮;对内的实现,倾向于正确
因果关系:error > defect/fault/bug > failure
程序员犯错导致软件存在缺陷,导致软件运行时失效
改进健壮性和正确性的步骤:
第0步:使用断言、防御性编程、代码评审、形式验证等方法编写具有健壮性和正确性目标的代码
步骤1:观察故障症状(内存转储、堆栈跟踪、执行日志、测试)
步骤2:识别潜在的错误(错误定位、调试)
步骤3:修复错误(代码修订)
7.2错误与异常处理
错误:程序员通常无能为力,一旦发生,想办法让程序优雅的结束
异常:你自己程序导致的问题,可以捕获、可以处理
异常分类:
运行时异常:由程序员在代码里处理不当造成
其他异常:由外部原因造成,例如I/O
checked异常
异常发生后,要么处理,要么抛出。
编译器可帮助检查你的程序是否已抛出或处理了可能的异常
必须捕获并指定错误处理器handler,否则编译无法通过(静态检查)
unchecked异常
编译器不能检查Errors and Runtime Exceptions(动态检查)
可以不处理,编译没问题,但执行时出现就导致程序失败,代表程序中的潜在bug
java异常处理
– 如果客户端可以通过其他的方法恢复异常,那么采用checked exception;
– 如果客户端对出现的这种异常无能为力,那么采用unchecked exception;
– 异常出现的时候,要做一些试图恢复它的动作而不要仅仅的打印它的信息。
方法要在定义和spec中明确声明所抛出的全部checkedexception
没有抛出checked异常,编译出错
Unchecked异常和Error可以不用处理
LSP中子类型多态:
客户端可用统一的方式处理不同类型的对象,子类型可替代父类型
如果子类型中override了父类型中的函数,那么子类型中方法抛出的异常不能比父类型抛出的异常类型更宽泛
子类型方法可以抛出更具体的异常,也可以不抛出任何异常
如果父类型的方法未抛出异常,那么子类型的方法也不能抛出异常。
异常抛出:
– 找到一个能表达错误的Exception类/或者构造一个新的Exception类
– 构造Exception类的实例,将错误信息写入构造函数
– 抛出它
自定义异常:
不管程序是否碰到异常,finally都会被执行
无需在finally中关闭文件,java自动关闭。
7.3Assertions and DefensiveProgramming
错误处理和异常处理 > 鲁棒性
断言与防御式编程 > 正确性
断言:在开发阶段的代码中嵌入,检验某些“假设”是否成立。
若成立,表明程序运行正常,否则表明存在错误。
– assert condition;
– assert condition : message;
断言主要用于开发阶段,避免引入和帮助发现bug
实际运行阶段,不再使用断言,避免降低性能
使用断言的主要目的是为了在开发阶段调试程序、尽快避免错误
-ea打开 -da关闭断言,默认是关闭的
断言 > Correctness
错误/异常处理 > Robustness
开发阶段用断言尽可能消除bugs
在发行版本里用异常处理机制处理漏掉的错误
7.4 Debugging代码调试
bug来源:Grace Murray Hopper
Debug是测试的后续步骤:测试发现问题,debug消除问题
Debug的目的是寻求错误的根源并消除它
调试过程:
重现:
诊断:找出导致bug的根源所在
修复:
反思:
7.5测试优先的编程
测试:发现程序中的错误,提高程序正确性的信心
白盒测试:对程序内部代码结构的测试
黑盒测试:对程序外部表现出来的行为的测试
测试优先的编程
黑盒测试用例设计
(1)基于等价类划分的测试:将被测函数的输入域划分为等价类,从等价类中导出测试用例。
(2)边界值分析方法是对等价类划分方法的补充
白盒测试要考虑内部实现细节
代码覆盖度:已有的测试用例有多大程度覆盖了被测程序
回归测试:一旦程序被修改,重新执行之前的所有测试
注释形式的测试