依据三个实验的知识点需求,可以将课程划分为三个阶段。
第一阶段则是Lectur1-Lecture3:软件构造基础介绍。
1、软件构造的多维度视图和质量目标
本节主要介绍分析软件系统的不同维度和软件构造过程中重要的质量目标。
1.1、软件的多维度视图
软件构造可以分为两个阶段:Build-Time(构造阶段)和Run-Time(运行阶段)。
两种状态:Moment(静止)、Period(动态)。
两种级别:Code-Level(代码),Component-Level(组件)。
当处于Build-Time时,Code-Level指的是代码的逻辑组织,如类、方法、接口的设计;Component-Level指的是代码的物理组织,如文件夹、包、Library等。
如果我们以Moment的视角去观察,在Code-Level,代码具有近乎自然语言的编程风格,同时遵循一定的语法又使得编译器能够识别。代码的结构可以抽象成语义树,对语义树进行修改就相当于对源代码进行修改;在Component-Level,主要指源代码中使用了库提供的语句,在链接时可以直接接入已经写好的可重复使用的代码。
如果我们以Period的视角去观察,在Code-Level,主要指代码发生的增删改。在Component-Level,则主要指版本的迭代更新。
当处于Run-Time时,Code-Level指代码层面的逻辑实体如何在内存中呈现,Component-Level指物理实体在物理硬件环境下的呈现。
如果我们以Moment的视角去观察,在Code-Level,程序运行时内存里变量层面的状态可以形成一张代码快照图来反应代码的状态,同时也会有内存信息转储,来方便访问更多的信息;在Component-Level,则为各部分之间的部署和调度。
如果我们以Period的视角去观察,在Code-Level,程序的各个部分之间的交互会被日志记录用以追踪信息;在Component-Level,则会有更多系统层面的信息被记录。
说到底,软件构造过程也是这些视图的转移过程:通过编码,我们从一无所有到获得了源代码;通过设计,我们让源代码分成了一个又一个的组件;通过安装,我们又获得了可以运行的程序;通过版本控制,我们可以让一个又一个的瞬间组成动态变化的过程。
1.2、软件系统的质量属性
软件系统的质量属性主要分为外部质量和内部质量。在软件构造的最后终点,只有外部质量是有意义的,但是外部质量也取决于开发过程中的内部质量。
外部质量有这些:
正确性:最重要的质量指标。假设软件系统被分为很多层,每一层都保证自己的正确性,同时假设其下层是正确的。可以通过测试和调试、防御式编程、形式化方法来保证正确性。
健壮性:主要是指软件能够应对规约以外的特殊情况,不会出现崩溃。这种异常情况是主观的而非客观的,也有可能随着规约范围的变大而成为正常情况。
可扩展性:越大的软件越难扩展,但是为了对应不断变化的情况,软件需要可扩展。可以通过简约主义设计、分离主义设计来保证可扩展性。
可复用性:一次开发,多次使用,在面对相似的问题是不要重复造轮子。
兼容性:指不同软件系统之间相互可以容易的集成。核心在于保持设计的同构性,可以规范化文件格式、数据结构以及用户接口。
性能:一个软件只有在保证正确性的情况下追求高性能才有意义。而且过度的优化性能可能会破快可扩展性和可复用性。
可移植性:软件可以在不同的技术环境之间移植。
易用性:对于用户来说,安装、操作、监控是容易的。
功能性:指软件系统提供的可用范围。但是不要为了一昧的追求新特性而忽视了其它更重要的质量属性。
及时性:即使更新迭代软件的新版本。
可验证性:软件的正确性可以得到验证。
完整性:软件保护自己运行过程中不被非法操作影响。
可修复性、经济性等等。
内部质量有这些:
圈复杂度、耦合度、内聚度、可读性、可理解性等等。
人生在世,很难做到尽善尽美,软件构造也是如此。在所有的质量属性中,我们需要进行取舍,而关键在于我们要将取舍的决策和标准明确的写下来。不过不论如何,正确性都是不可以被舍弃的。
1.3、五个重要的软件构造质量属性
易懂、容易开发、容易改变、没有bug、高性能。
2、软件测试与测试优先的编程
本节主要介绍了一些软件测试方法以及测试优先编程的优势。
2.1、软件测试概述
软件测试是一种提高软件质量的手段,可以确保某一外部质量因素是否达到了用户标准。不过,即使是最好的测试,也无法完全把Bug从软件里清除掉。一个好的测试应该能发现错误、不冗余、不太复杂也并不简单。
测试按照规模可以分为单元测试、集成测试、系统测试、验收测试。
还有静态测试、动态测试、回归测试、黑盒测试、白盒测试等方法。
测试很困难,主要是因为情况太多,几乎不可能枚举每一种情况,依靠偶然也没有意义,因为相比于实物测试,对于软件来说基于样本的统计数据意义不大。
测试用例=输入+执行条件+期望结果
测试优先的编程:先写规约,再写满足规约的代码,同时也更进一步的帮助你理解规约,最后再编写代码。先写测试会节省大量的调试时间。
2.2、测试方式详解
单元测试:针对软件的最小单元模型开展测试,隔离各个模块,容易定位错误和调试。
使用Junit进行单元测试:详细步骤会写在实验的博客里。
黑盒测试:用于检查代码功能,不关心内部实现。主要是检查程序是否符合规约。
白盒测试:需要考虑内部实现,一般较早执行。需要对代码的各个部分进行测试。
自动测试与回归测试:手工测试的成本很高,如果用类似Junit这样的程序自动进行测试则会节省许多时间和成本。回归测试则是指每次改动代码都再测试一遍之前进行过测试的用例,直到样例全部被通过为止。
2.3、测试用例选择策略
测试策略非常重要,需要在程序中显式的记录下来,这样在代码评审的过程中,其他人可以理解你的测试,并评测你的测试是否足够充分。
对于每个数据需要满足的条件,我们可以划分等价类,每个等价类代表着对输入约束加以满足/违反的有效/无效数据的集合。因为相似的输入,将会展示相似的行为。故可以从每个等价类中选一个代表作为测试用例即可。从而可以降低测试用例数量。
而有一类测试用例非常重要,即边界数据。因为大量的错误都会发生在边界数据上而非中央。测试用例具有一定的代码覆盖度,代码覆盖度越低,测试越不充分;但是想要测试充分,又需要付出更多代价。根据代码覆盖度,选用测试用例有两种方式:
笛卡尔积:全覆盖。
多个维度上划分的多个取值,要组合起来,每个组合有一个用例。覆盖面广,但是成本高。
覆盖每个取值:最少1次即可
每个维度的每个取值至少被1个测试用例覆盖一次即可。覆盖面低,会有疏漏,但是成本少。
3、软件构造过程与配置管理
SDLC:软件开发生命周期(见上图)。
3.1、软件开发模型
所有的软件开发模型可以分为两个基本模型:线性模式和迭代模型。意思很好理解,就如字面所示。在这两个模型的分类下还有很多其它模型,选择这些模型来开发软件的依据主要是用户的参与程度(决定了软件需要具备的适应变化的能力)、开发效率/管理复杂度、开发出的软件的质量。有这样一些经典模型:
Waterfall瀑布模型:线性推进、阶段划分清楚、整体推进、无迭代、管理简单、无法适应变需求增加/变化
Incremental增量过程:线性推进、增量式(多个瀑布的串行)、无迭代、比较容易适应需求的增加。
V-Model V字模型:确认与验证、是瀑布模型的扩展。
Prototyping 原型过程(迭代的):开发早期原型,持续不断的迭代直到满足用户需求,时间代价高,但开发质量也高。
Spiral螺旋模型(迭代的):多轮迭代基本遵循瀑布模式、每轮迭代都有明确的目标,遵循“原型”过程,进行严格的风险分析,方可进入下一轮迭代
敏捷开发:通过快速迭代和小规模的持续改进,以快速适应变化。每次迭代处理一个小规模增量,拥有极限的用户参与、极限的小步骤迭代、极限的确认/验证。如果说瀑布模式是鲸吞,那么敏捷开发则是蚕食。
3.2、软件配置管理和版本控制系统
软件配置管理:追踪和控制软件的变化。软件的任何组成部分(源代码、数据、文档、硬件、各种环境)都可能随着软件生命周期的时间而更新。
软件配置项:软件中发生变化的基本单元。
基线:软件持续变化过程中的“稳定时刻”。
CMDB配置管理数据库:存储软件的各配置项随时间发生变化的信息+基线。
版本:为软件的任一特定时刻的形态指派一个唯一的编号,作为身份标识。
版本控制的作用:回滚到上一个版本、比较两个版本的差异、备份软件历史版本、获取备份、合并版本、在多个开发者之间共享和协作、记录每个开发者的动作便于“审计”等。
版本控制术语:仓库(即与SCM中的CMDB)、工作拷贝(在开发者本地机器上的一份项目拷贝)、文件(一个独立的配置项)、版本(在某个特定时间点的所有文件的共同形态)、变化(两个版本之间的差异)、HEAD(程序员正在工作的版本)。
本地版本控制系统:仓库存储于开发者本地机器,无法共享和协作。
集中式版本控制系统:仓库存储于独立的服务器,支持多开发者之间的协作。
分布式版本控制系统:仓库存储于独立的服务器+每个开发者的本地机器。
Git作为版本控制工具的使用方法:将在实验博客中给出。
3.3、广义的软件构造过程
Programming-Build-Code review/Static code analysis-Dynamic code analysis/profiling-Testing-Debugging-Refactoring
3.3.1、Programming
软件构造语言从用途上划分可分为:编程语言、建模语言、配置语言、构建语言。
从形态上划分可以分为:基于语言学的构造语言、基于数学的形式化构造语言、基于图形的可视化构造语言。
3.3.2、Code review/Static code analysis
代码评审的方式:结对编程、走查、正式评审会议、自动化评审。代码评审有助于提升代码质量,也能帮助程序员提高自身水平。
3.3.3、Dynamic code analysis/Profiling
动态分析:要执行程序并观察对象、收集数据、分析不足。对代码的运行时状态和性能进行度量,发现代码中的潜在问题。
3.3.4、Debugging and Testing
关于测试,可以参考本篇博客的第2节。
调试:定位错误、发现错误根源。
3.3.5、Refactoring
重构:在不改变功能的前提下优化代码。
3.4、狭义的软件构造过程
即从源代码到发布软件的过程。这个在CSAPP里有非常详细的讲解。
简单来说:一个idea->代码->编译成汇编语言->汇编成可重定位文件->链接成可执行文件->与其它文件打包->发布。