软件构造知识点总结
本文仅供参考,不能保证正确性,且部分内容因为在实验中已经多次练习过,便不再赘述
-
软件构造的多维视图
- Code-level view: source code——代码的逻辑组织
- Component-level view: architecture——代码的物理组织
- Moment view: 特定时刻的软件形态
- Period view: 软件形态随时间的变化
-
软件构造的质量目标
- 外部质量因素:影响用户,取决于内部质量,matters
- 正确性:最重要的质量指标
- 健壮性:对异常情况的处理能力
- 可扩展性
- 可复用性
- 兼容性
- 高效,Efficiency
- 可移植性
- 易用性
- 及时性
- …
- 内部质量因素:影响软件本身和开发者
- 折中,tradeoff
- 五个关键的质量目标
- Easy to understand
- Ready for change
- Cheap for develop: reusability
- Safe from bugs
- Efficient to run
- 外部质量因素:影响用户,取决于内部质量,matters
-
SCM&VCS
-
SCM:Software Configuration Management,追踪和控制软件的变化,核心是版本控制和基线确立
- SCI软件配置项:软件中发送变化的基本单元(如对于Git来说是文件,对于传统的VCS是变化的代码行)
- baseline基线:稳定版本
- CMDB配置管理数据库:存储软件的各配置项随时间发生变化的信息+基线
-
VCS:Version Control System,分为三类
- Local VCS:本地版本控制系统,仓库位于开发者的本地机器,无法共享和协作
- Centralized VCS(如CVS,SVN):集中式版本控制系统,仓库位于独立的服务器,支持协作
- Distributed VCS(如Git):分布式版本控制系统,仓库位于独立的服务器和开发者的本地机器
-
Git
-
.git目录相当于本地的CMDB
-
staging area暂存区:隔离工作目录和Git仓库
-
文件的三种状态:
- Modified已修改
- Staged已暂存
- Committed已提交
-
Object Graph对象图:版本间的演化关系图,是一张有向无环图DAG,存储在.git目录下,下面是一个Object Graph的示意图,取自讲义2-1
每一个commit在图中表示为一个节点,也被称为一个版本,commit指向它的父亲
-
-
-
可变性和不可变性
-
spec规约
- 强度:前置更弱、后置更强的规约强度更高
- 分类
- 确定的规约:给定一个满足前置条件的输入,输出唯一且确定
- 欠定的规约(Under-deterministic):同一个输入可以有多个输出(多次执行输出相同)
- 非确定的规约(NonDeterministic):同一个输入,多次执行时输出可能不同,可能实现中有随机或者和时间有关的因素
-
ADT抽象数据类型
- ADT是由操作定义的,与内部实现无关
- 可变与不可变
- 可变数据类型:提供了可改变其内部数据的值的操作
- 不可变数据类型:提供的操作不能改变内部值,而是构造新的对象
- ADT操作的分类
- 构造器Creators
- 生产器Producers
- 观察器Observers
- 变值器Mutators
- 设计一个好的ADT
- 设计简单、一致的操作
- 足以支持client对数据的所有操作需要,且尽可能实现简单
- 要么抽象、要么具体,不要混合
- 抽象函数、表示独立性、不变量、表示泄露
- 防御式拷贝
-
instanceof判断某个对象是不是特定类型或其子类型
instanceof is dynamic type checking, not the static type
重写的equals()函数必须是等价关系:1.自反 2.对称 3.传递
hashCode()函数要求:一次运行中多次调用同一对象的hashCode()方法,返回值必须相同;不过在多次运行中不要求其返回值相同
等价的对象必须有相同的 hashCode
-
Equality of Mutable Types:
- observational equality观察等价性: 在不改变状态的情况下, 两个 mutable 对象是否看起来一致
- behavioral equality行为等价性:调用对象的任何方法都展示出一致的结果,对可变类型来说,往往倾向于实现严格的观察等价性
当可变类型被mutator改变时,hashCode值会发生变化(具体见3-5 Equality in ADT and OOP)
The Java library is unfortunately inconsistent about its interpretation of equals() for mutable classes.
比如在JDK中,Collections使用observational equality,而其他的可变类(like StringBuilder)使用behavioral equality. -
Liskov Substitution Principle(LSP)
如果对于类型 T 的对象x, q(x) 成立, 那么对于类型 T 的子类型 S 的对象y, q(y) 也成立。
要求
1. 前置条件不能强化
2. 后置条件不能弱化(前两条其实为强度不变弱的spec)
3. 要不变量保持父类的不变量
4. 子类型方法参数:逆变
5. 子类型方法返回值:协变
6. 抛出的异常类型:协变
协变:
父类型到子类型:越来越具体 specific
返回值类型:不变或变得更具体
异常的类型:不变或变得更具体
逆变:
参数类型:不变或变得更抽象泛型是类型不变的:ArrayList是List子类,但List不是 List子类,类型参数在编译后被丢弃,在运行时不可用,该过程被称为类型擦除
委托的种类:
- Dependency:use a,临时的委托. 如duck.fly(new FlyWithWings()); 在Duck类中, fly方法:void fly(Flyable f) {…}
- Association:has a,永久的委托,以成员变量的形式存在
- Composition:owns a,更强的Association,但是难以变化,如成员变量不可设定、不可改变
- Aggregation:has a,更弱的Association,可以动态变化
可以认为 Composition/Aggregation 是 Association 的两种具体形态
面向复用的设计模式
Structural patterns
-
Adapter适配器模式
将某个类/接口转换为client期望的其他形式
-
Decorator装饰器模式
为每一个特性构造子类,通过委托增加到对象上
Component是被装饰的的对象,Decorator是抽象类,是所有装饰类的父类 -
Facade外观模式
Behavioral patterns
-
Strategy策略模式:给client提供多种策略
-
Template method模板模式
使用的是继承和重写,常常用于白盒框架,比如下面的安装两种车的例子:
在BuildCar()实现的通用的代码逻辑 -
Iterator迭代器模式
为客户端提供遍历元素的方法
Iterable<>接口:实现该接口的类需要实现Iterator iterator()方法;
Iterator<>接口:实现该接口的类需要实现boolean hasNext()方法和E next()方法.
面向可维护性编程
度量可维护性的标准:圈复杂度、代码行数、继承树的深度、类之间的耦合度、单元测试的覆盖度等
追求:高内聚、低耦合
-
评价可维护性的标准:
-
可分解性Decomposability
将问题分解为各个可独立解决的问题的能力
目标:使模块之间的依赖关系显式化和最小化
-
可组合性Composability
将模块组合起来形成新的系统的能力
目标:使模块可以在不同的环境下复用
-
可理解性Understandability
每个模块被设计者理解的能力
-
可持续性Continuity
规格说明中小的变化将只影响一小部分,而不会影响这个体系结构
-
出现异常之后的保护Protection
运行时的不正常将局限于小范围模块内
-
-
五项规则:
- 直接映射:模块的结构与现实世界中问题领域的结构保持一致——持续性、可分解性
- 尽可能少的接口:模块应尽可能少的与其他模块通讯——持续性、保护性、可理解性、可组合性
- 尽可能小的接口:如果两个模块通讯,那么它们应交换尽可能少的信息——可持续性、保护性
- 显示接口:当A与B通讯时,应明显的发生在A与B的接口之间——可分解性、可组合性、可持续性、 可理解性
- 信息隐藏:经常可能发生变化的设计决策应尽可能隐藏在抽象接口后面——可持续性
-
设计原则:
- SRP单一责任原则:ADT中不应该有多于一个原因使其发生变化
- OCP开放-封闭原则:对扩展的开放和对修改的封闭
利用继承或委托实现扩展 - LSP里氏替换原则
- DIP依赖转置原则:高层模块不应该依赖于低层模块,二者都应该依赖于抽象,抽象不应该依赖于实现细节,实现细节依赖于抽象
- ISP接口聚合原则:客户端不应依赖于它不需要的方法——尽量使用小接口而不是“胖”接口
面向可维护性的设计模式
-
Creational patterns
工厂方法模式 Factory Method pattern
抽象工厂模式 Abstract Factory,抽象工厂模式和工厂方法模式的不同在于,抽象工厂创建的不是一个完整产品,而是“产品族”(遵循固定搭配规则的多类产品的实例),得到的结果是:多个不同产品的object ,各产品创建过程对 client 可见,但“搭配”不能改变。
本质上, Abstract Factory 是把多类产品的 factory method 组合在一起,如果不用抽象工厂而使用多个工厂方法可能导致client用错工厂——牛仔裤+西装 -
Structural patterns
Proxy代理模式,某些对象不希望被client直接访问,故设置Proxy,在二者间建立防火墙
三种类型:- Remote Proxy远程代理,为对象创建缓存
- Virtual Proxy虚代理
- Access control访问控制
作用:隔离对复杂对象的访问,降低其代价,同时可以控制访问
-
Behavioral patterns
-
Observer观察者模式 “偶像粉丝模式”
粉丝(Observer)会接受偶像(Subject)的状态变化,或者说偶像会广播自己的变化
偶像类中存储了粉丝列表(List),当自身的状态发生变化时通知粉丝,采用的是委托的机制 -
Visitor访问者模式
Strategy模式和Visitor模式的异同:
两者都是通过委托建立两个对象的动态联系;但是Visitor强调的是外部定义某种对ADT的操作,该操作与ADT关系不大,而Strategy则强调的是ADT内部某些要实现的功能的相应算法的灵活替换,这些算法也是ADT的重要组成部分,总的来说visitor是站在外部client的角度,灵活增加对ADT的各种不同操作(哪怕ADT没实现该操作),strategy则是站在内部ADT的角度,灵活变化对其内部功能的不同配置。
-
基于状态的构造技术
State Pattern状态模式
Memento备忘录模式,记录对象的历史状态,以便于回滚
Originator:需要备忘的类
Caretaker:添加Originator的备忘记录和恢复
Memento:备忘录,记录Originator对象的历史状态
Originator只需要存储当前状态,在每次备份时,都生成一个外部的Memento对象,Caretaker负责掌控全部的状态备份,客户端通过它来操纵ADT的状态备份与恢复
Caretaker中保存了一个List用于回滚
基于语法的构造技术
正则表达式
终止节点(叶节点、终止符)
产生式 = 终结符、产生式和操作符
*、+、?、连接xy、或|、[]、[^] (注:在[]中的"^" 表示取反,但不在方括号中^匹配的是字符串的开头,$匹配结尾
特殊字符:
‘.’:代表任何单字符,除了换行符’\n’
‘\d’:一个数字,等同于[0-9]
‘\s’:匹配任何空白符,包括空格、制表符、换页符等,等价于[ \f\n\r\t\v],注意最开头有空格,\f为换页符,\r是回车符,\n换行符,\v垂直制表符
‘\w’:匹配大小写字母和数字以及下划线,等同于[a-zA-Z_0-9]
‘\D’:非数字,相当于[^0-9]
‘\S’:非空白符,[^\s]
‘\W’:非字母、非数字、非下划线,[^\w]
Greedy贪婪的匹配:被强制要求第一次尝试匹配时读入整个输入串,匹配失败时从后往前逐个字符地回退并尝试匹配,直到匹配成功或无字符可退
Reluctant勉强的:第一次匹配时只匹配首字符,失败后逐个往后增加,直到匹配成功或无字符可加
Possessive独占的直接匹配整个字符串
健壮性、正确性
正确性是最重要的指标
对外的接口倾向于健壮性;对内的实现倾向于正确性
可靠性Reliability=健壮性Robustness+正确性Correctness
断言和异常
Java中的错误、异常类
Throwable为所有异常、错误类的基类
Error+Exception
Unchecked Exceptions:所有的Error以及RuntimeException,可以不捕获,不写异常处理,编译器不会check,unchecked
Checked Exception:需要自己的程序进行捕获并进行异常处理,编译器会check,checked
Try-with-Resource语句
try(Ressource res = ...) {
...
}
==
try {
...
}
finally {
...
res.close();
}
也就是说当try(Resource res = …) {}的try退出时,res.close会被自动执行
举个栗子:
try(Scanner in = new Scanner(new File("…"))) {
…
}
try退出时会自动执行in.close();甚至还可以打开多个Resource,退出try时也会关闭多个,每个资源之间用";"隔开
try(Scanner in = …; PrintWriter out = …) {
…
}
assert断言在开发阶段帮助调试程序,运行时关闭断言避免降低性能
需要注意的是assert必须避免副作用;如assert list.remove(x)语句,加入assert被disable了,list的元素就不会被删除,这种情况用boolean found = list.remove(x); assert found;
程序之外的事,不受你控制,不要乱用断言,断言检查的只是程序内部状态是否符合规约,外部的东西使用异常进行处理,断言确保正确性,异常确保健壮性;使用异常处理"预料到可以发生"的情况,使用断言处理"绝不应该发生"的情况
pre-condition用异常处理,post-condition用assert处理;nonpublic的方法的前置条件可以用assert,public和nonpublic的方法的后置条件都可以用assert处理
snapshot
基本类型的值直接显示,对象类型的值用圆圈圈起来;不可变类型的对象用双圈椭圆;final的使用双线箭头
线程
Thread.sleep(),使当前线程休眠,值得注意的是进入休眠的线程不会丢失现有的锁,从休眠中苏醒后可以继续执行
t.interrupt(),向其他线程发出中断信号,值得注意的是中断是一种请求,线程不一定要立即停止,只有在线程正在Thread.sleep(),Thread.join()或Object.wait()时才会立即响应,否则只会设置线程的中断状态,将中断状态设置为true,表示有线程希望自己中断
t.isInterrupted()只是查询状态,不会做其他事情
Thread.interrupted()查询状态,并将其重置为false,该函数返回的是设置前的状态
值得注意的是interrupt()和isInterrupted()都是实例方法,而interrupted()是类方法(静态)
t.join()让当前线程暂停直到线程t执行结束
线程安全Threadsafe
线程安全可以从三个方面理解:
- Behaves correctly:不违反spec、保持RI
- Regardless of how threads are executed:与操作系统如何调度无关
- Without additional coordination:不需要在spec中强制要求client满足某种“线程安全”的义务
达到线程安全的四种手段
-
限制数据共享:线程间不共享mutable的数据
-
使用不可变的数据类型和不可变的引用
如果一个ADT满足加强的Immutability的定义,则可以确定该ADT线程安全。加强的Immutability定义:
- 没有mutator
- 所有成员变量都是private final的
- 没有表示泄露
- 可变对象没有任何形式的mutation
-
使用线程安全的ADT
如synchronized类型,但是不要保存别名;而且即使使用synchronized类型,迭代器仍然不安全,使用迭代器时需要加锁
synchronized(c) { for(Type e : c) ... }
虽然synchronized类型保证某一操作线程安全,但是多个操作间也存在竞争
-
通过锁的机制共享线程安全的可变数据
使用synchronized代码块或者synchronized方法,如果使用synchronized方法,当线程调用该方法时,会自动获取该方法所在对象的内部锁,并在方法返回时释放它,即使返回是由未捕获的异常引起的,也会释放锁;如果该方法是static的,由于静态方法与类关联,而不是对象,此时线程获取与该类关联的Class对象的内部锁。但是使用锁机制会降低程序的并发性,影响性能