软件构造
1.软件构造过程中的多维视图
2.软件生命周期与配置管理
开发模式、配置管理、版本控制
2.1开发模式
两个过程模式:线性过程、迭代过程
五个模型:Waterfall (Linear, non-iterative) 瀑布过程、– Incremental (non-iterative) 增量过程、V-Model (for verification and validation) V字模型、Prototyping (iterative) 原型过程、Spiral (iterative) 螺旋模型。
2.1.1 瀑布过程
线性推进、阶段划分清楚、整体推进、无迭代、管理简单、无法适应需求增加/变化。
2.1.2 增量过程
线性推进:增量式(多个瀑布的串行)无迭代( 比较容易适应需求的增加)
2.1.3 原型过程
在原型上持续不断的迭代,发现用户变化的需求。(时间代价高,但开发质量也高。)
2.1.4 螺旋过程
非常复杂的过程:多轮迭代基本遵循瀑布模式、每轮迭代有明确的目标,遵循“原型”过程,进行严格的风险分析,方可进入下一轮迭代。
2.2 配置管理、版本控制
Software Configuration Management (SCM)
Version Control System (VCS)
2.2.1 配置管理
软件配置管理:追踪和控制软件的变化
CMDB:配置管理数据库(git 仓库)
2.2.2 版本控制
回滚到上一个版本、比较两个版本的差异、备份软件版本历史、获取备份、合并。
2.3 利用Git进行版本控制
.git 文件夹就是本地的CMDB。
Git的基本操作:
-
将变更存入本地的仓库:
git commit
-
将变更存入远程仓库
git push
-
分支操作
切换当前分支:git checkout [branch name]
创建并且换分支:git checkout -b [new branch name]
合并分支:git merge [branch name] (将括号内分支合并至当前分支)
同步(从远程仓库获取最新版本):git fetch
2.4 软件开发的过程
广义的软件构造:构建(build) 编码(coding) 重构(refactoring) 调式(debug) 测试(test) 性能分析(dynamic code analysis) 代码评审(Code review Static code analysis)
狭义的软件构造:Validate(验证)Compile(编译)Link(链接)Test(测试)Package(打包)Install(安装)Deploy(应用)
build工具:Make, Ant, Maven, Gradle, Eclipse
2.4.1 编码
从用途上划分:
编程语言:C,C++,Java,Python
建模语言:UML
配置语言:XML
构建语言:XML
从形态上划分:
基于语言学的的构造语言、基于数学的形式化构造语言、基于图形的可视化构造语言。
2.4.2 代码评审
结对编程 、 走查 、 正式评审会议 、 自动化评审。
动态分析:要执行程序并观察现象、收集数据、分析不足。
2.4.3 调试和测试
测试:发现程序是否有错误。
调试:定位错误、发现错误根源。
2.4.4 重构
在不改变功能的前提下优化代码
3.数据类型和类型检验
软件构造的理论基础——ADT(抽象数据类型)
软件构造的技术基础——OOP(面对对象编程)
3.1 编程语言中的数据类型
-
基本数据类型
int long boolean double char short byte float
都是不可变数据类型、只有值没有ID(无法与其他值区分) 在栈中分配内存
-
对象数据类型
Classes interfaces arrays
有时是可变有时不可变、既有ID也有值、 在堆中分配内存
-
对象类型形成层次结构
extends(继承关系) -
将基本数据类型包装成对象类型
Integer、Boolean…(仍是不可变的,一般可以进行自动转换)
3.2 静态类型和动态类型
静态类型语言(Java)在编译时进行类型检查,动态类型语言(Python)在运行时进行类型检查
3.3 类型检查
int a = 2; | a = 2 |
---|---|
double a = 2; | a = 2.0 (Implicit) |
int a = 18.7; | ERROR |
String a = 1; | ERROR |
int a = (int) 18.7; | a = 18 |
double a = 2/3; | a = 0.0 |
double a = (double)2/3; | a = 0.6666… |
检查内容:
Syntax errors | 语法错误 |
---|---|
Wrong names | 类名/函数名错误 |
Wrong number of arguments | 参数数目错误 |
Wrong argument types | 参数类型错误 |
Wrong return types | 返回值类型错误 |
Illegal argument values | 非法的参数值 |
Unrepresentable return values | 非法的返回值 |
Out-of-range indexes | 越界 |
Calling a method on a null object reference | 空指针 |
静态检查:关于“类型”的检查,不考虑值
动态检查:关于“值”的检查
3.4 可变数据类型和不可变数据类型
改变一个变量:将该变量指向另一个值的存储空间。
改变一个变量的值:将该变量当前指向的值的存储空间中写入一个新的值。
final:
final类无法派生子类
final变量无法改变值/引用
final方法无法被子类重写
不变对象:一旦被创建,始终指向同一个值/引用
可变对象:拥有方法可以修改自己的值/引用
例如,String是不可变对象。改变了String的值后,JVM会申请新的内存,并将这个变量指向这个区域
安全的使用可变类型:局部变量,不会涉及共享;只有一个引用
如果有多个引用(别名),使用可变类型就非常不安全
3.5 Snapshot diagram as a code-level, run-time, and moment view
Moment | Period | |
---|---|---|
Build-time | design,build,refactoring | Version Control |
Run-time | Code snapshot,heap dump | Thread and Process |
3.5.1 Snapshot diagrams
用于描述程序运行时的内部状态、便于程序员之间的交流、便于刻画各类变量随时间变化、便于解释设计思路
3.5.2 变量类型的图示
单箭头:表示一个基本数据类型的值
单线圆圈:表示一个对象(可以细化对象内部的变量)
双线椭圆:表示不可变的对象
不可变的引用:用双线箭头
引用是不可变的,但指向的值却可以是可变的(final StringBuilder s)
可变的引用,也可指向不可变的值(String s)
3.6 复杂数据类型
Arrays and Collections
List Set Map
可以使用迭代器进行迭代,但是在使用迭代器进行迭代的时候要注意是否会出现暗中破坏的情况(remove)
3.7 有用的不可变数据类型
可以利用Collections.unmodifiableList将一个可变的List、Set、Map包装成不可变的类型。即不能再更改其中的值。(只能看)
也可以使用List.of() Set.of() Map.of()进行不可变的包装。
3.8 空引用
空引用只能应用在对象上,基本数据类型上会报错。
传递给变量后不能使用这个类型变量的方法,会产生空指针错误。空引用和空字符串是不一样的。
3.9 编程语言中的函数和方法
方法中输入参数和返回参数的匹配在静态类型检查阶段完成
3.10 编程中的规约(描述)(Java Doc)
(1)描述了方法的决策设计和变量的类型,注释是给别人读的。
(2)规定了程序的目的,没有规约无法写程序,即使写完也不能确定程序的对错。达到了客户端和程序的一致。精确的规约有助于区分责任。
(3)通过规约确定不同的方法是否是一致的。
(4)通过规约确定前置条件(对客户端的约束,在使用方法时必须满足的条件)、后置条件(对开发者的约束,方法结束时必须满足的条件)如果前置条件满足了后置条件必须满足。前置条件不满足,则方法可以做任何事情。如果后置条件没有声明,方法内部不应该更改输入的参数。
3.11 设计规约
-
为规约分类
规约的强度:更强大规约=更放松的前置条件+更严格的后置条件。越强的规约代表着implementor的自由度和责任越重,而client的责任越轻。
Deterministic(确定的规约):给定一个满足前置条件的输入,其输出是唯一的、明确的
Under-deterministic(欠定的规约):同一个输入可以有多个输出
Non-deterministic(非确定的规约):同一个输入,多次执行时得到的输出可能不同Declarative(声明性规约):没有内部实现的描述,只有“初-终”状态
Operational(操作式规约):伪代码
声明式规约更有价值 -
设计好的规约
Spec描述的功能应单一、简单、易理解
不能让客户端产生理解的歧义
规约中若没有确定需要满足的前置条件,那么需要在代码中进行复杂的check,若写了,则客户端需要满足这些前置条件。惯用做法是:不限定太强的precondition,而是在post condition中抛出异常:输入不合法。 -
归纳
是否使用前置条件取决于(1)check的代价;(2)方法的使用范围
– 如果只在类的内部使用该方法(private),那么可以不使用前置条件,在使用该方法的各个位置进行check,责任交给内部client;
– 如果在其他地方使用该方法(public),那么必须要使用前置条件,若client端不满足则方法抛出异常。
3.12 抽象数据类型(ADT)
抽象数据类型与表示独立性:设计良好的抽象数据结构,通过封装来避免客户端获取数据的内部表示(即“表示泄露”)
- 几个定义
abstraction functions (AF):抽象函数
representation invariant (RI):表示不变量
客户端获取数据的内部表示:表示泄露
3.12.1 抽象和自定义数据类型
Abstract data types的含义:抽象的、模块化的、封装的、信息隐藏、关注分割
- 抽象数据类型与传统类型的差异
传统的数据类型关注具体表示,抽象类型:强调“作用于数据上的操作”,程序员和用户无需关心数据如何具体存储的,只需设计/使用操作即可。
3.12.2 类型分类和操作
可变数据类型:提供了可以改变内部值的操作
不可变数据类型:其操作不是改变内部值而是创造了新的对象
- 抽象数据类型的操作类型分类
creators:构造器(Integer.valueOf())
observers:观察器 获取信息(get…(), size(), contains())
producers:生产器 根据已有的创造没有的(String中的:substring())
mutators:变值器(改变成员变量值)例(add(),remove())
3.12.3 设计一个抽象数据类型
- 设计简洁、一致的操作
- 要足以支持client对数据所做的所有操作需要,且用操作满足client需要的难度要低
- 要么抽象、要么具体,不要混合 — 要么针对抽象设计,要么针对具体应用的设计
3.12.4 Representation Independence(RI)表示独立性
定义:client使用ADT时无需考虑其内部如何实现,ADT内部表示的变化不应影响外部spec和客户端。
获得对象中的数据类型时需要返回该成员变量的复制。
3.12.5 测试抽象数据类型
- 测试creators, producers, and mutators:调用observers来观察这些operations的结果是否满足spec;
- 测试observers:调用creators, producers, and mutators等方法产生或改变对象,来看结果是否正确。
3.12.6 不变性(Invariant)
不变量:在任何时候总是true
表示不变性具体含义是某个“表示”是否是合法的。
由ADT来负责其不变量,与client端的任何行为无关
例:
有一个抽象数据类型A,A中有一个List B,List中储存着抽象数据类型C。若某一个外部的操作导致了某一个C发生了改变,此时A并没有发生表示泄露。但是如果A提供了直接返回该List的引用的方法,则可能会发生表示泄露。所以如果需要返回该List变量需要返回new ArrayList<>(B)
3.12.7 表示不变性和抽象函数
抽象函数是将表示空间R(代码部分)映射到抽象空间A(想象的部分,猴子、桥、棋子等)的方法。
开发者更多关注表示空间,用户更多关注抽象空间。必是满射但是不一定是单射和双射。
表示不变性和抽象函数的关系:
选择某种特定的表示方式R,进而指定某个子集是“合法”的(RI),并为该子集中的每个值做出“解释”(AF)——即如何映射到抽象空间中的值。
3.12.8 有益的可变性
对immutable的ADT来说,它在A空间的abstract value应是不变的。但其内部表示的R空间中的取值则可以是变化的。
3.13 面向对象的编程(OOP)
用接口来实现OOP
基本概念:封装与信息隐藏、继承与重写、多态、子类型、重载、静态与动态分派
3.13.1 面对对象的一些标准
泛型(Genericity)、继承(Inheritance)、多态(Polymorphism)、动态分派/绑定(Dynamic dispatch/binding)
3.13.2 基本概念
对象(Object) 类(Class)属性(attribute)方法(method)
- Object
一束状态和行为。状态用field描述,行为用method描述 - Class
每一个对象都有一个类,类定义了:对象在哪里使用和这些对象是如何完成事情的。
一个类的方法被称为API(Application Programming Interface)
3.13.3 接口和枚举
Interface(接口)确定ADT的规约
Class(类): 实现ADT
接口之间可以继承
一个类可以实现多个接口
3.13.4 封装和信息隐藏
信息隐藏:使用接口类型声明变量、客户端仅使用接口中定义的方法、 客户端代码无法直接访问属性
- private:只有声明的类可以使用
- protected:同一个包下的可以使用
- public:同一个工程下的都可以使用
3.13.5 继承和重写(Overriding)
- Overriding 重写
严格继承:子类只能添加新方法,不能重写超类中的方法
super:复用了父类中的方法功能(必须写在第一行) - Abstract Class 抽象类
如果某些操作是所有子类型都共有,但彼此有差别,可以在父类型中设计抽象方法,在各子类型中重写
所有子类型完全相同的操作,放在父类型中实现,子类型中无需重写。
有些子类型有而其他子类型无的操作,不要在父类型中定义和实现,而应在特定子类型中实现。
3.13.6 多态、子类和重载(Polymorphism, subtyping and overloading)
- 三种类型的多态
特殊多态(传入的不同)、参数化多态()、子类型多态,包含多态 - 特殊多态和重载
重载:多个方法具有同样的名字,但有不同的参数列表或返回值类型
重写和重载
重写:子类继承父类。具有相同的方法名和参数类型
重载:不一定是继承关系,仅是具有相同的方法名和不同的参数类型。 - 参数多态和泛型编程
范型变量 泛型类 泛型接口 泛型方法
子类型的规约不能弱化超类型的规约。 - 子类多态
instanceof --比较类型 Integer a; a instanceof Integer == true
3.13.7 动态分配
绑定:将调用的名字与实际的方法名字联系起来(可能很多个);分派:具体执行哪个方法(early binding static dispatch)
动态分派:编译阶段可能绑定到多态操作,运行阶段决定具体执行哪个(override和overload均是如此)
推迟绑定:编译阶段不知道类型,一定是动态分派(override是推迟绑定,overload是early binding)
3.13.8 一些重要的方法
- toString() 转换成字符串
- hashCode() 返回哈希值
- equals() 自反 传递 对称:首先需要满足instanceof然后比较关键的变量
3.14 ADT和OOP中的等价性
- 等价关系
- 站在观察者角度,利用AF,定义不可变对象之间的等价关系
- 引用等价性和对象等价性
- 可变数据类型的观察等价性和行为等价性
3.14.1 等价关系
参照集合论,自反对称传递的关系是等价关系
3.14.2 不可变数据类型的等价关系
AF映射到同样的结果,则等价。
观察等价:站在外部观察者的角度观察
3.14.3 ==对比equals()
==:引用的等价性(类似地址相等)
equals:对象的等价性
3.14.4 重写equals()
缺省情况下equals使用的是==来判断的
3.14.5 对象的规约
equals的规约,必须满足自反对称传递的性质,并且和null的比较值必须是false
除非对象被修改了,否则调用多次equals应同样的结果。“相等”的对象,其hashCode()的结果必须一致
3.14.6 可变数据类型的等价比较
观察等价性:在不改变状态的情况下,两个mutable对象是否看起来一致
行为等价性:调用对象的任何方法都展示出一致的结果
对可变类型来说,往往倾向于实现严格的观察等价性
所以实现观察行为等价性只需要重写equals即可。否则需要比较是否指向了同一个内存空间
clone():
x.clone() != x
x.clone().getClass() == x.getClass()
x.clone().equals(x)