一.软构1-3讲
1.软件构造的多维度视图和质量目标
多维度视图:
外部质量指标有:正确性:测试和调试、防御式编程、形式化方法(check\ensure\guarantee)
健壮性:针对异常情况的处理(对正确性的补充) 可扩展性:对软件的规约进行修改,应对变化(简约主义设计、分离主义设计)
可复用性:一次开发,多次使用
兼容性:不同软件系统之间互相可容易集成(保持设计的同构性\标准化)
性能:更少的资源占用
可移植性:软件可方便的在不同的技术环境之间移植
易用性、更多功能(不好)、及时性……
正确性:软件的行为要严格的符合规约中定义的行为
健壮性:出现规约定义之外的情形的时候,软件要做出恰当的反应内部质量指标有代码可读性可理解性清晰性等等。
二者区别在于外部质量因素影响用户,内部质量因素影响软件本身和它的开发者。
2.软件测试与测试优先的编程
黑盒测试用例的设计:
黑盒测试:对程序外部表现出来的行为的测试,检查功能,不关心内部实现细节
test case = {test inputs + execution conditions+ expected results}
测试用例:输入+执行条件+期望结果
等价类划分、边界值分析:
每个等价类代表着对输入约束加以满足/违反的有效/无效数据的集合
相似的输入,将会展示相似的行为。故可从每个等价类中选一个代表作为测试用例即可
边界值分析方法是对等价类划分方法的补充
在等价类划分时,将边界作为等价类之一加入考虑
3.软件构造过程与配置管理
本地版本控制系统:仓库存储于开发者本地机器无法共享和协作
集中式版本控制系统:仓库存储于独立的服务器,支持多开发者之间的协作
分布式版本控制系统:仓库存储于独立的服务器+每个开发者的本地机器
Git 的结构、工作原理、基本指令:
二.软构4-8讲
4.数据类型与类型检验
基本数据类型和对象数据类型:
Mutable/Immutable:
值的改变、引用的改变:
防御式拷贝:
改变一个变量:将该变量指向另一个值的存储空间
改变一个变量的值:将该变量当前指向的值的存储空间中写入一个新的值
不变数据类型:一旦被创建,其值不能改变
如果编译器无法确定 final 变量不会改变,就提示错误,这也是静态类型检查的一部分
例如,String 是一个不可变数据类型,StringBuilder 是一个可变数据类型
使用不可变类型,对其频繁修改会产生大量的临时拷贝(需要垃圾回收),可变类型最少化拷贝以提高效率
如果有多个引用(别名),使用可变类型就非常不安全
画Snapshot diagram图:
不可变对象:用双线椭圆
不可变的引用 final:用双线箭头(引用是不可变的,但指向的值却可以是可变的)
5.设计规约
规约包括:
前置条件,对客户端的约束,在使用方法时必须满足的条件
后置条件,对开发者的约束,方法结束时必须满足的条件
(如果前置条件满足了,后置条件必须满足)
静态类型声明是一种规约,可据此进行静态类型检查 static checking
方法前的注释也是一种规约,但需人工判定其是否满足
行为等价性:实现的两个方法是否可以相互替换(对于用户)
如果两个函数符合相同规约,则它们等价
规约的强度:
不限定太强的 precondition,而是在 postcondition 中抛出异常:输入不合法
如果只在类的内部使用该方法(private),那么可以不使用前置条件,在使用该方法的各个位置进行 check——责任交给内部 client
如果在其他地方使用该方法(public),那么必须要使用前置条件,若client 端不满足则方法抛出异常
6.抽象数据类型 (ADT)
ADT 操作的四种类型:
构造器 Creaters:实现为构造函数或静态函数
生产器 Producers(concat() method of String)
观察器 Observers(size() method of List)
变值器 Mutators(add() method of List)改变对象属性的方法
不可变类型没有mutators
表示独立性:
client 使用 ADT 时无需考虑其内部如何实现,ADT 内部表示的变化不应影响外部 spec 和客户端
表示泄露:
不仅影响不变性,也影响了表示独立性:无法在不影响客户端的情况下改变其内部表示
不变量、表示不变量 RI:
不变量:在任何时候总是 true,immutability 就是一个典型的"不变量"
7.面向对象的编程
接口、抽象类、具体类:
接口(确定 ADT 的规约)和类(实现 ADT):定义和实现 ADT
接口之间可以继承与扩展
一个类可以实现多个接口
一个接口可以有多种实现类
抽象类:包含至少一个没有实现的抽象方法的类
接口是只有抽象方法的抽象类
严格继承:子类只能添加新方法,无法重写超类中的方法
多态:特殊多态(功能重载)、参数化多态、子类型多态\包含多态
重载:多个方法具有同样的名字,但有不同的参数列表或返回值类型,是静态多态,在编译阶段进行静态类型检查(override 在运行阶段进行动态类型检查)
泛型:
泛型接口,非泛型的实现类
泛型接口,泛型的实现类
子类型多态:不同类型的对象可以统一的处理而无需区分
8.ADT和OOP中的“等价性”
等价性 equals()和==:
等价的对象必须有相同的 hashCode
== 对基本数据类型,使用==判定相等(引用等价性)
对对象类型,使用 equals()(对象等价性)
如果用= =,是在判断两个对象身份标识 ID 是否相等(指向内存里的同一段空间)
在自定义 ADT 时,需要重写 Object 的 equals()
可变对象的观察等价性、行为等价性:
观察等价性:在不改变状态的情况下,两个 mutable 对象是否看起来一致
行为等价性:调用对象的任何方法都展示出一致的结果
对可变类型来说,往往倾向于实现严格的观察等价性
但在有些时候,观察等价性可能导致 bug,甚至可能破坏 RI
在 JDK 中,不同的 mutable 类使用不同的等价性标准
对可变类型,实现行为等价性即可
也就是说,只有指向同样内存空间的 objects,才是相等的
所以对可变类型来说,无需重写这两个函数,直接继承 Object 的两个方法即可
三.软构9-12讲
9.面向复用的软件构造技术
面向复用编程:开发出可复用的软件
基于复用编程:利用已有的可复用软件搭建应用系统
白盒复用:源代码可见,可修改和扩展
黑盒复用:源代码不可见,不能修改
白盒框架,通过代码层面的继承进行框架扩展
黑盒框架,通过实现特定接口/delegation 进行框架扩展
LSP: 子类必须能替换它们的父类
子类型多态:客户端可用统一的方式处理不同类型的对象
协变:父类型到子类型,更具体的规约,不变或更具体的返回值类型和异常类型
反协变\逆变:父类型到子类型,更具体的规约,不变或更抽象的参数类型
泛型不是协变的
委派/委托:一个对象请求另一个对象的功能
委派是复用的一种常见形式
“委托”发生在 object 层面,而“继承”发生在 class 层面
接口的组合:
使用接口定义系统必须对外展示的不同侧面的行为
接口之间通过 extends 实现行为的扩展(接口组合) 类 implements 组合接口从而规避了复杂的继承关系
白盒框架,通过代码层面的继承进行框架扩展
黑盒框架,通过实现特定接口/delegation 进行框架扩展
白盒框架的原理与实现:继承
黑盒框架的原理与实现:委派
10.面向可维护性的构造技术
可维护性的常见度量指标:
圈复杂度、代码行数、可维护性指数、继承的层次数、类之间的耦合度、单元测试的覆盖度
聚合度与耦合度:
模块化编程:高内聚、低耦合(子程序之间的相关联性)、分离关注点、信息隐藏
评估:可分解性、可组合性、可理解性、可持续性(发生变化时受影响范围最小)、出现异常后的保护(出现异常后受影响范围最小)
规则:直接映射、尽可能少的接口、尽可能小的接口、显式接口、信息隐藏
SOLID:
SRP 单一责任原则:不应该有多于 1 个原因让你的 ADT 发生变化, 否则就拆分开
OCP:面向变化的开放/封闭原则
对扩展性的开放:模块的行为应是可扩展的,从而该模块可表现出新的行为以满足需求的变化
对修改的封闭性:模块自身的代码是不应被修改的,扩展模块行为的一般途径是修改模块的内部实现
关键的解决方案:抽象技术
LSP:Liskov 替换原则
ISP:接口隔离原则
不能强迫客户端依赖于它们不需要的接口:只提供必需的接口
DIP:依赖转置原则
抽象的模块不应依赖于具体的模块
具体应依赖于抽象
语法、正则表达式(运用形式语言与自动机课程知识很好理解):
输入文件有特定格式,程序需读取文件并从中抽取正确的内容用语法定义一个“字符串”
11.面向可复用性和可维护性的设计模式
1.创建型模式
工厂方法模式(虚拟构造器): 当 client 不知道要创建哪个具体类的实例,或者不想在 client 代码中指明要具体创建的实例时,用工厂方法。定义一个用于创建对象的接口,让其子类来决定实例化哪一个类,从而使一个类的实例化延迟到其子类
2.结构化模式
适配器:将某个类/接口转换为 client 期望的其他形式,通过增加一个接口,将已存在的子类封装起来,client 面向接口编程,从而隐藏了具体子类
装饰器:
用每个子类实现不同的特性
对每一个特性构造子类,通过委派机制增加到对象上
3.行为类模式
策略:
有多种不同的算法来实现同一个任务,但需要 client 根据需要动态切换算法,而不是写死在代码里为不同的实现算法构造抽象接口,利用 delegation,运行时动态传入 client 倾向的算法类实例
模板(Template):
做事情的步骤一样,但具体方法不同
共性的步骤在抽象类内公共实现,差异化的步骤在各个子类中实现
使用继承和重写实现模板模式
迭代:
客户端希望遍历被放入容器/集合类的一组 ADT 对象,无需关心容器的具体类型
也就是说,不管对象被放进哪里,都应该提供同样的遍历方式
让自己的集合类实现 Iterable 接口,并实现自己的独特 Iterator 迭代器(hasNext, next, remove),允许客户端利用这个迭代器进行显式或隐式的迭代遍历
访问:
对特定类型的 object 的特定操作(visit),在运行时将二者动态绑定到一起,该操作可以灵活更改,无需更改被 visit 的类
将数据和作用于数据上的某种/些特定操作分离开来
为 ADT 预留一个将来可扩展功能的“接入点”,外部实现的功能代码可以在不改变 ADT 本身的情况下通过 delegation 接入 ADT
12.面向正确性与健壮性的软件构造
健壮性和正确性:
健壮性:系统在不正常输入或不正常外部环境下仍能够表现正常的程度
处理未期望的行为和错误终止即使终止执行,也要准确/无歧义的向用户展示全面的错误信息
错误信息有助于进行 debug
正确性:程序按照 spec 加以执行的能力,是最重要的质量指标
throwable:
内部错误:程序员通常无能为力,一旦发生,想办法让程序优雅的结束(用户输入错误、设备错误、物理限制)
异常:你自己程序导致的问题,可以捕获、可以处理
Error/Runtime 异常、其他异常:
运行时异常,是程序源代码中引入的故障所造成的,如果在代码中提前进行验证,这些故障就可以避免
Checked 异常、Unchecked 异常:
Unchecked exceptions
不需要在编译的时候用 try…catch 等机制处理
Checked exception
Checked 异常的处理机制:
声明、抛出、捕获、处理、清理现场、释放资源等
如果子类型中 override 了父类型中的函数,那么子类型中方法抛出的异常不能比父类型抛出的异常类型更宽泛
异常发生后,如果找不到处理器,就终止执行程序,在控制台打印出 stack trace
也可以不在本方法内处理而是传递给调用方,由 client 处理(“推卸责任”)
本来 catch 语句下面是用来做 exception handling 的,但也可以在catch 里抛出异常(rethrowing),目的是,更改 exception 的类型,更方便 client 端获取错误信息并处理
Finally clause
当异常抛出时,方法中正常执行的代码被终止
如果异常发生前曾申请过某些资源,那么异常发生后这些资源要被恰当的清理
不管程序是否碰到异常,finally 都会被执行
断言的作用、应用场合:
Fail fast,避免扩散
检查前置条件是防御式编程的一种典型形式
断言:在开发阶段的代码中嵌入,检验某些“假设”是否成立。若成立,表明程序运行正常,否则表明存在错误
对代码所做的假设都保持正确
用于:
内部不变量 aasert x > 0
表示不变量 checkRep()
控制流不变量 switch-case
方法的前置条件、方法的后置条件
断言一旦 false,程序就停止执行
外部错误要使用 Exception 机制去处理,即使 spec 被违反,也不应 通过 assert 直接 fail,而是应抛出具体的 runtime 异常
断言非常影响运行时的性能
使用异常来处理你“预料到可以发生”的不正常情况
使用断言处理“绝不应该发生”的情况