Programing for/with reuse:
源码级别复用:白盒复用:源代码可见,可修改和扩展
黑盒复用:源代码不可见,不能修改
模块级别复用:类与接口。继承,委托。
库级别复用:API、包。
系统级别复用:框架:一组具体类、抽象类、及其之间的连接关系
Liskov替代原理LSP:
子类型多态:客户端可用统一的方式处理不同类型的对象
LSP:更强的不变量;更弱的前置条件;更强的后置条件
强行为子类型化:前置条件不能强化;后置条件不能弱化;不变量要保持;子类型方法参数:逆变;子类型方法的返回值:协变;异常类型:协变。
协变、反协变:
协变:
父类型到子类型:越来越具体specific
返回值类型:不变或变得更具体
异常的类型:也是如此。
反协变:
父类型到子类型:越来越具体specific
参数类型:要相反的变化,要不变或越来越抽象
数组的子类型化:
协变的:如T[]中的任意元素可以实T的子类
泛型的子类型化:
不可协变:类型擦除
Vector<? extends 类型1> x = new Vector<类型2>();
类型1指定一个数据类型,那么类型2就只能是类型1或者是类型1的子类:
Vector<? super 类型1> x = new Vector<类型2>();
类型1指定一个数据类型,那么类型2就只能是类型1或者是类型1的父类
Delegation委托:
委派/委托:一个对象请求另一个对象的功能,委派是复用的一种常见形式
Comparator和Comparable:
Dependency委派:Delegation关系通过方法的参数传递建立起来
Association委派:Delegation关系通过固有的field建立起来
Composition: 更强的association,但难以变化:Composition是Association的一种特殊类型,其中Delegation关系通过类内部field初始化建立起来,无法修改
Aggregation: 更弱的association,可动态变化:Aggregation也是Association的一种特殊类型,其中Delegation关系通过客户端调用构造函数或专门方法建立起来
CRP原则:
利用接口和delegation编程(CRP)
接口的组合:
interface Ducklike extends Flyable, Quackable {}接口的组合,定义了行为的组合
public class Duck implements Ducklike从组合接口中派生具体类
白盒框架的原理与实现:
框架:一组具体类、抽象类、及其之间的连接关系
白盒框架,通过代码层面的继承进行框架扩展,通常用继承,子类,重载
黑盒框架,通过实现特定接口/delegation进行框架扩展,通常用委托/组合
可维护性的常见度量指标:
圈复杂度:度量代码的结构复杂度。
代码行数:指示代码中的大致行数。
可维护性指数:计算介于0和100之间的索引值,表示维护代码的相对容易性。高价值意味着更好的可维护性。
继承的层次数:表示扩展到类层次结构的根的类定义的数量。等级越深,就越难理解特定方法和字段在何处被定义或重新定义。
类之间的耦合度:通过参数,局部变量,返回类型,方法调用,泛型或模板实例化,基类,接口实现,在外部类型上定义的字段和属性修饰来测量耦合到唯一类。
单元测试的覆盖度:指示代码库的哪些部分被自动化单元测试覆盖。
聚合度与耦合度:
模块之间的联系是耦合,模块之间联系越紧密,耦合性越强。追求低耦合。
模块内部元素的联系是内聚,元素结合越紧密,聚合度越高。追求高内聚。
SOLID:
SRP单一责任原则
责任:变化的原因。不应有多于1个的原因使得一个类发生变化。
OCP开放-封闭原则
对扩展性的开放
对修改的封闭:模块自身的代码是不应被修改的
典型的违反OCP的例子是大量的使用 if-else / switch-case 语句
LSP替换原则
子类型必须能够替换其基类型
派生类必须能够通过其基类的接口使用,客户端无需了解二者之间的差异
DIP依赖转置原则
抽象的模块不应依赖于具体的模块
具体应依赖于抽象
ISP 接口聚合原则
客户端不应依赖于它们不需要的方法
“胖”接口具有很多缺点,它不够聚合
语法、正则表达式:
连接x ::= y z
重复x ::= y*
选择x ::= y | z
x ::= y? an x is a y or is the empty string
x ::= y+ an x is one or more y (equivalent to x ::= y y* )
x ::= [a-c] is equivalent to x ::= 'a' | 'b' | 'c'
x ::= [^a-c] is equivalent to x ::= 'd' | 'e' | 'f' | ... (all other characters)
. 匹配任何字符除了换行符
\d [0-9]
\s 匹配所有空格字符
\w 匹配所有字母和数字
\.,\( , \), \* 具体符号,转义
[]外的^表示从开始行开始匹配
使用正则语言表达式时,出现\要换成\\
设计模式adapter、decorator、strategy、template method、iterator/iterable、factory method、visitor:
factory method:
当client不知道要创建哪个具体类的实例,或者不想在client代码中指明要具创建的实例时,用工厂方法。
定义一个用于创建对象的接口,让其子类来决定实例化哪一个类,从而使一个类的实例化延迟到其子类。
Adapter:
将某个类/接口转换为client期望的其他形式。
Decorator:
为对象增加不同侧面的特性,对每一个特性构造子类,通过委派机制增加到对象上。
Strategy:
有多种不同的算法来实现同一个任务,但需要client根据需要动态切换算法,为不同的实现算法构造抽象接口,利用delegation,运行时动态传入client倾向的算法类实例.
template method:
做事情的步骤一样,但具体方法不同,共性的步骤在抽象类内公共实现,差异化的步骤在各个子类中实现.
使用继承和重写实现模板模式.
iterator/iterable:
Iterable接口:实现该接口的集合对象是可迭代遍历的.
Iterator接口:迭代器
Iterator pattern:让自己的集合类实现Iterable接口,并实现自己的
独特Iterator迭代器(hasNext, next, remove),允许客户端利用这
个迭代器进行显式或隐式的迭代遍历
Visitor:
为ADT预留一个将来可扩展功能的“接入点”,外部实现的功能代码可以在不改变ADT本身的情况下通过delegation接入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的角度,灵活变化对其内部功能的不同配置。
健壮性和正确性:
健壮性:系统在不正常输入或不正常外部环境下仍能够表现正常的程度
总是假定用户恶意、假定自己的代码可能失败
把用户想象成白痴,可能输入任何东西
正确性:程序按照spec加以执行的能力,是最重要的质量指标!
正确性倾向于直接报错(error),健壮性则倾向于容错(fault-tolerance)
Throwable:
异常类都继承自Throwable类
Throwable两个子类Error和Exception
Error:程序员通常无能为力,一旦发生,想办法让程序优雅的结束。种类:用户输入错误、设备错误、物理限制
Exception:你自己程序导致的问题,可以捕获、可以处理。
分类:RuntimeException和其他
RuntimeException:运行时异常,是程序源代码中引入的故障所造成的
非运行时异常,是程序员无法完全控制的外在问题所导致的。
Checked异常、Unchecked异常:
Unchecked 异常:Error + RuntimeException,不需要在编译的时候用try…catch等机制处理,可以不处理,编译没问题,但执行时出现就导致程序失败,代表程序中的潜在bug。
Checked异常:必须捕获并指定错误处理器handler,否则编译无法通过。从异常发生的现场获取详细的信息,利用异常返回的信息来明确操作失败的原因,并加以合理的恢复处理
Checked异常的处理机制:
程序员必须在方法的spec中明确写清本方法会抛出的所有checked exception,以便于调用该方法的client加以处理。
在spec中声明异常:@throw 关键字 。。。
当异常抛出时,方法中正常执行的代码被终止
如果异常发生前曾申请过某些资源,那么异常发生后这些资源要被恰当的清理
不管程序是否碰到异常,finally都会被执行(通常用于清理资源)
自定义异常类:
包含更多“案发现场信息”的异常类定义和辅助函数
抛出异常的时候,将现场信息记入异常
在异常处理时,利用这些信息给用户更有价值的帮助
断言的作用、应用场合:
断言:在开发阶段的代码中嵌入,检验某些“假设”是否成立。若成立,表明程序运行正常,否则表明存在错误。
断言即是对代码中程序员所做假设的文档化,也不会影响运行时性能(因为默认断言无效,否则断言很影响性能)。
可以应用在:内部不变量、表示不变量、控制流不变量、方法的前置条件、方法的后置条件。
断言主要用于开发阶段,避免引入和帮助发现bug,实际运行阶段,不再使用断言。
Java缺省关闭断言,如果要断言有效,要记得打开(-ea)。
Assertion vs. Exception:
断言用于Correctness,使用断言处理绝不应该发生”的情况,如果来自于自己所写的其他代码,可以使用断言来帮助发现错误
错误/异常处理用于Robustness,使用异常来处理你“预料到可以发生”的不正常情况,如果参数来自于外部(不受自己控制),使用异常处理。
断言和异常处理都可以处理同样的错误。
防御式编程的基本思路:
对来自外部的数据源要仔细检查,例如:文件、网络数据、用户输入等
对每个函数的输入参数合法性要做仔细检查,并决定如何处理非法输入
类的public方法接收到的外部数据都应被认为是脏的,需要处理干净再传递到private方法——隔离舱,“隔离舱”外部的函数应使用异常处理,“隔离舱”内的函数应使用断言。
黑盒测试用例的设计:
等价类划分:
基于等价类划分的测试:将被测函数的输入域划分为等价类,从等价类中导出测试用例。
针对每个输入数据需要满足的约束条件,划分等价类。
每个等价类代表着对输入约束加以满足/违反的有效/无效数据的集合。
边界值分析:
边界值分析方法是对等价类划分方法的补充。
在等价类划分时,将边界作为等价类之一加入考虑
基本类型的边界,如 INT_MAX、INT_MIN
集合中的第一个和最后一个元素
以注释的形式撰写测试策略:
测试策略:根据什么来选择测试用例非常重要,需要在程序中显式记录下来
目的:在代码评审过程中,其他人可以理解你的测试,并评判你的测试是否足够充分
JUnit测试用例写法:
测试用例:输入 + 执行条件 + 期望结果
测试的动机:让代码出错,出错越快越好
编写测试的过程:先写规约,再写符合规约的测试用例,
写代码、执行测试、有问题再改、再执行测试用例,直到通过它
测试驱动开发(TDD):将需求转化为具体的测试用例,然后软件经过改进,通过新的测试
测试覆盖度:
代码覆盖度:已有的测试用例有多大程度覆盖了被测程序
代码覆盖度越低,测试越不充分,但要做到很高的代码覆盖度,需要更多的测试用例,测试代价高
语句覆盖:每⼀条语句至少执行一次
分支覆盖:判定中每个条件的所有可能结果至少出现一次,并且每个判定本身的所有可能结果也至少出现一次
路径覆盖:每条可能执行到的路径至少执行一次
测试效果:路径覆盖 > 分支覆盖 > 语句覆盖
测试难度:路径覆盖 > 分支覆盖 > 语句覆盖