第一章 欢迎进入软件构建的世界
1.1 什么是软件构建
构建活动主要是编码与调试,但也涉及详细设计、规划构建、单元测试、集成、集成测试等。
1.2 软件构建为何如此重要
因为构建活动是软件开发的主要组成部分(也是核心部分)、提高构建能力可以大大提高码农的生产率。码农的产物——源代码,需要具有尽可能高的质量。
Key Points
软件构建是软件开发的核心活动;构建活动是每个项目中唯一一项必不可少的工作。
软件构建的主要活动包括:详细设计、编码、调试、集成、开发者测试( developer testing)(包括单元测试和集成测试)。
构建也常被称作“编码”和“编程”。
构建活动的质量对软件的质量有着实质性的影响。
最后,你对“如何进行构建”的理解程度,决定了你这名程序员的优秀程度
第二章 用抽象来更充分地理解软件开发
如果想直接学习实践方面的知识,可以略过本章(本章将介绍一种软件开发的抽象思想)
2.1 抽象的重要性
抽象,可以理解为类比,用来帮助我们对不熟悉的事物产生更深刻的理解,建模就是一种抽象。例如撞球模型是气体的分子运动的抽象等。
最早的对软件的抽象要追溯到图灵机时代,那时候的数据处理可以抽象为流经计算机的连续卡片流。
2.2 抽象的作用
对软件抽象的作用是指引你寻找解决方案的方向,而不是提供给你解决问题的方法(算法)。
2.3常见的软件抽象
-
写作
这是对小型项目的抽象,即不需要做什么计划,想到什么写什么就可以了。但是写作这一抽象并不十分完整,比如代码复用等设计思想无法体现在“写作”这一抽象中。
-
耕作
这一抽象侧重于凸显设计系统的增量技术,即每次小步前进、每一步都需要设计系统、书写代码、单元测试等(对应耕作中的挖坑、播种、埋土、浇水等步骤)。耕作这一抽象的缺点在于弱化了软件开发是如何得到结果的,显然,在耕作完成后需要等待一段漫长的时间才能收获果实,而软件开发则不是依赖于等待来完成项目的。
-
房屋建造
这一抽象包括了寻找建造土地、打地基、购买材料、绘制蓝图、打造房屋框架、砌好边墙、通水电、局部拆除重建、装修等等概念。这些概念都能在软件开发中找到对应的过程,这是一个比较优秀的抽象。
Key Points
抽象是启示而不是算法。因此它们比较随意和自由 。 抽象把软件开发过程与其他你熟悉的活动联系在一起,帮助你更好地理解。有些抽象比其他一些抽象更贴切。
通过把软件的构建过程比作是房屋的建设过程,我们可以发现,仔细的准备是必要的,而大型项目和小型项目之间也是有差异的。
通过把软件开发中的技巧和经验比作是智慧工具箱中的工具,我们又发现,每位程序员都有许多工具,但并不存在任何一个能适用于所有工作的工具,因地制宜地选择正确工具是成为能有效编程的程序员的关键。
不同的抽象彼此并不排斥,应当使用对你最有益处的某种抽象组合。
第三章 三思而后行:前期准备
3.1 前期准备的重要性
前期准备是为了避免出现“制造错误的产品”或者“使用错误的方法制造产品”等情况。准备工作的中心目标就是降低风险————规避掉因为糟糕的需求分析和项目计划而带来的项目风险。
研究人员发现,在构建活动开始之前清除一个错误,那么返工的成本仅仅是“在开发过程的最后阶段(在系统测试期间或者发布之后)做同样事情”的十分之一到百分之一。
一般而言,这里的原则是:发现错误的时间要尽可能接近引入该错误的时间。在软件开发过程的上游引入的缺陷通常比那些在下游引入的缺陷具有更广泛的影响力,这也使得早期的缺陷造成的后果更加严重。
下面两条是自我实现的预言:
我们最好立刻开始编码,因为将会有很多的调试工作需要做。
我们已经非常详细地研究了需求和设计,我想不出在编码和调试期间还会遇到什么大问题。
后者是前期准备的目标。
3.2 辨明你所从事的软件的类型
不同种类的软件项目,需要在“准备工作”和“构建活动”之间做出不同平衡。比如开发一个小游戏和银行要开发的新的支付平台,这两者的平衡比例显然是不同的。
高速迭代的开发方法可以减少前期准备不足造成的负面影响,但是不能完全避免。
一条很有用的经验规则是,要么预先对大约80%的需求做出详细说明,并给“稍后再进行详细说明的额外需求”分配一定的时间。然后在项目进行过程中,实施系统化的变更控制措施,只接受那些最有价值的新需求。要么预先只对最重要的20%的需求做出详细说明,并且计划以小幅增量开发软件的剩余部分,随着项目的进行,对额外的需求和设计做出详细说明。
当需求相对稳定、开发团队对应用领域很熟悉、后期改变需求、设计和编码的代价很昂贵时,比较适用前者;相应的、当需求仍不确定或者难以被理解、设计很复杂,有挑战性、项目包含许多不确定性因素时,比较适用于后者。
3.3 问题定义的先决条件
问题定义只需要提出问题是什么,而不涉及任何的解决方案。在完成问题定义后,需求分析阶段就要对定义的问题进行深入调查。
定义问题应该从客户的角度来书写,有时候程序员思维会让人陷入窘境——许多问题或许会有除了编写程序之外的更简单的解决方案。
3.4 需求的先决条件
明确的需求有助于用户进行自行评审,避免我们在开发过程中猜测用户想要的是什么,也有助于减少争论和分歧。
但是,“客户一但接受了一份需求文档,就不再做更改”听起来更像是奢望。理由是在项目的不断推进中,开发人员与客户对项目的理解也在逐渐深入,这或许会暴露出许多在一开始没能考虑到的问题,这也就是需求变更的主要来源。
平均水平的项目在开发过程中,需求会有25%的变化,这部分变化的需求会导致80%左右的返工工作量。
关于如何在构建期间处理需求变更:
评估变更后的需求质量,在把它完善之前停止继续编码。评估清单如下:
针对功能需求
- 是否详细定义了系统的全部输入,包括其来源、精度、取值范围、出现频率等?
- 是否详细定义了系统的全部输出,包括目的地、精度、取值范围、出现着率、格式等?
- 是否详细定义了所有输出格式( Web页面、报表,等等)?是否详细定义了所有硬件及软件的外部接口?
- 是否详细定义了全部外部通信接口,包括握手协议、纠错协议、通信协议等?
- 是否列出了用户想要做的全部事情?
- 是否详细定义了每个任务所用的数据,以及每个任务得到的数据?
针对非功能需求
- 是否为全部必要的操作,从用户的视角,详细描述了期望响应时间?是否详细描述了其他与计时有关的考虑,例如处理时间、数据传输率、系统吞吐量?
- 是否详细定义了安全级别?
- 是否详细定义了可靠性,包括软件失灵的后果、发生故障时需要保护的至关重要的信息、错误检测与恢复的策略等?
- 是否详细定义了机器内存和剩余磁盘空间的最小值?
- 是否详细定义了系统的可维护性,包括适应特定功能的变更、操作环境的变更、与其他软件的接口的变更能力?
- 是否包含对“成功”的定义?“失败”的定义呢?
需求的质量
- 需求是用用户的语言书写的吗?用户也这么认为吗?每条需求都不与其他需求冲突吗?
- 是否详细定义了相互竞争的特性之间的权衡——例如,健壮性与正确性之间的权衡?
- 是否避免在需求中规定设计(方案)?
- 需求是否在详细程度上保持相当一致的水平?有些需求应该更详细地描述吗?有些需求应该更粗略地描述吗?
- 需求是否足够清晰,即使转交给一个独立的小组去构建,他们也能理解吗?开发者也这么想吗?
- 每个条款都与待解决的问题及其解决方案相关吗?能从每个条款上溯到它在问题域中对应的根源吗?
- 是否每条需求都是可测试的?是否可能进行独立的测试,以检验满不满足各项需求?
- 是否详细描述了所有可能的对需求的改动,包括各项改动的可能性?
需求的完备性
- 对于在开始开发之前无法获得的信息,是否详细描述了信息不完全的区域?
- 需求的完备度是否能达到这种程度:如果产品满足所有需求,那么它就是可接受的?
- 你对全部需求都感到很舒服吗?你是否已经去掉了那些不可能实现的需求——那些只是为了安抚客户和老板的东西?
确保所有人都知道需求变更的代价,避免客户过于随意地提出新功能或者要求修改某个需求,用“进度”和“成本”作为镇定剂吧。
使用更适应变更的开发方法(低耦合、高内聚等),使用演进交付来使每一小部分的进度都能得到用户的反馈,便于及时做出调整。
放弃这个项目:如果需求特别糟糕而不可改变——程序和程序员有一个能跑就行。
3.5 架构的先决条件
架构指的是适用于整个系统范围的设计约束。系统架构的主要组成部分如下
程序组织
以概括的形式对系统做综述,定义程序的主要构造块。根据程序规模的不同,一个构造块可以是一个类,也可以是由许多类组成的子系统。
每个构造块实现一种较高层的功能。
如果有不止一个构造块用于实现同一项功能,它们应该要相互配合而不会冲突。
每一个构造块对其他构造块的影响或者依赖要越小越好。
要定义好构造块之间的通信规则和调用关系。
主要的类
对那些构成系统80%行为的20%的类进行详细说明。如果系统足够大,这些主要的类可以被组织成子系统。
数据设计
架构应该详细定义数据的存储形式和类型。包括某个数据应该用Long或者是short,某一类数据用列表或堆栈或哈希表存储,什么时候用顺序访问什么时候用随机访问存储。什么时候用单个数据库或者多个数据库,同时定义如何与其他访问同一数据的程序之间的交互方式。
业务规则
架构需要详细描述依赖的业务规则,以及这些规则对系统设计的影响,例如要求客户的信息更新时间必须短于一分钟,那么架构就需要设计保持客户信息及时更新同步的方法并考虑其影响。
用户界面设计
应该在需求阶段对用户界面进行详细说明,尽量将用户界面模块化,以便可以轻松地对其进行替换(例如在测试时使用命令行的交互形式而不是图形化界面交互)
资源管理
架构应该描述一份管理稀缺资源的计划,例如数据库连接、线程、内存等。
安全性
架构应该描述实现设计层面和代码层面的安全性的方法。例如指定编码规范,处理缓冲区的方法、输入验证、加密机制等。
性能
性能目标可以包括资源的使用,也应该详细定义资源之间的优先顺序。
可伸缩性
可伸缩性是指系统增长以满足未来需求的能力。架构师应该要描述系统如何应对用户数量、服务器数量、数据库记录等的增长。
互用性
描述如何使该系统与其他系统共享数据或资源。
国际化与本地化
例如字符类型等,以及例如阿拉伯地区右对齐的排版格式。
输入输出
应该详细定义读取策略以及在何处检测I/O错误。
错误处理
需要考虑的问题有
- 错误处理机制是纠正还是仅检测?
- 错误检测是主动还是被动的?
- 程序如何传播错误?(立刻抛弃引发该错误的数据还是处理该错误?立刻处理或是要等到某个时候再处理?)
- 错误的处理约定?
- 异常的处理约定?包括抛出规则与日志规则
- 每个类如何验证其数据有效性?是每个类都验证自己的数据有效性还是有一组类专门验证?
- 使用开发环境(基建)自带的错误兜底机制还是建立自己的一套机制?
容错性
错误的容忍机制,例如重试、功能退化等,这一部分内容很多,值得单独研究。
架构的可行性
在论证了能否达到性能目标、能否在有限的资源下运行、实现环境是否能支撑等因素后,架构应该论证系统的技术可行性。在全面开展构建之前解决掉尽可能多的风险。
过度工程 即,可能会存在过度的为了保证健壮性而进行的编码。例如重复校验数据准确性等。架构中需要对此进行平衡。
关于买还是造 有时候最有效的构建软件的解决方案是购买或者使用开源软件。架构中要考虑到某些组件是否可以直接使用已经存在的软件。
关于复用 架构应该说明如何进行可复用性设计,以及复用的软件如何加工以符合其他架构目标
变更策略 在构建过程中,可能需要对产品进行变更。架构应该列出最有可能发生变更的功能,提前设计好简单的变更方案。
3.6 花费在前期准备上的时间长度
一般来说,前期计划占据10%-20%的工作量以及20%-30%的时间。
Key Points
构建活动的准备工作的根本目标在于降低风险。要确认你的准备活动是在降低风险,而非增加风险。
如果你想开发高质量的软件,软件开发过程必须由始至终关注质量。在项目初期关注质量,对产品质量的正面影响比在项目末期关注质量的影响要大。程序员的一部分工作是教育老板和合作者,告诉他们软件开发过程,包括在开始编程之前进行充分准备的重要性。
你所从事的软件项目的类型对构建活动的前期准备有重大影响——许多项目应该是高度迭代式的,某些应该是序列式的。
如果没有明确的问题定义,那么你可能会在构建期间解决错误的问题。
如果没有做完良好的需求分析工作,你可能没能察觉待解决的问题的重要细节。如果需求变更发生在构建之后的阶段,其代价是“在项目早期更改需求”的20至100倍。因此在开始编程之前,你要确认“需求”已经到位了。
如果没有做完良好的架构设计,你可能会在构建期间用错误的方法解决正确的问题。架构变更的代价随着“为错误的架构编写的代码数量”增加而增加,因此,也要确认“架构”已经到位了。
理解项目的前期准备所采用的方法,并相应地选择构建方法。
第四章 关键的构建决策
4.1 选择编程语言
选择熟悉的语言能够提高编程效率。当然,让一个Java程序员去写Python代码也是可以做到的,但是很容易出现的现象是,他实际产出的是"伪装成Python的Java",这通常不利于发挥出语言本身的优势。
下面是一些常见语言的简介:
C是一种通用的中级语言,它最初与UNIX操作系统紧密关联。C具有某些高级语言的特征,例如结构化的数据、结构化的控制流程、机器无关性以及一套丰富的运算符。它也被称为“可移植的汇编语言”,因为其中大量使用指针和地址,具有某些低级的构件(如位操作),而且是弱类型的。
C++是一种面向对象(object-oriented)的语言,基丁C语言,它是20 世纪80年代在 Bell Laboratories开发的。除了与C兼容之外,C++还提供了类、多态、异常处理、模板,而且提供比C语言更健壮的类型检查功能。它还提供了一套内容广泛而强大的标准库。
C#(C sharp)是一种通用的面向对象语言和编程环境,由 Microsoft开发,它提供了大量的工具,帮助在 Microsoft平台上进行开发。
Fortran是第一个高级计算机语言,引入了“变量”和“高级循环”的概念。“Fortran”代表“FORmula TRANslation”(公式翻译),最早开发于20世纪50年代,而且有若干重要的修订版,包括 1977年的 Fortran 77,加入了块结构的if-then-else 语句和字符及字符串处理功能。Fortran 90加入了用户定义的数据类型、指针、类,以及一套丰富的数组运算。Fortran主要用在科学和工程应用中。
Java是一种面向对象的语言。Java设计为能在任何平台上运行,办法是将Java源代码转变为字节码(byte code),然后让后者在各个平台上的虚拟机环境中运行。Java广泛用于Web应用的编程。
JavaScript是一种解释执行的脚本语言,最初与Java略有关系。它主要用于做客户端编程,例如为Web 页面增加简单的功能及在线应用程序。
Perl是一种处理字符串的语言,基于C和若干UNIX 工具程序。Perl常用于系统管理任务,诸如创建生成脚本(build scripts),也用于生成及处理报表。它也可用来创建Web应用程序,例如 Slashdot。"Perl”是“Practical Extraction and ReportLanguage (实用摘要及报告语言)”的首字母缩写。
PHP是一种开源的脚本语言。PHP能在所有主要的操作系统上运行,用来执行服务器端的交互功能。它也能嵌入Web页面中,用来访问及呈现数据库信息。“PHP”原来代表“Personal Home Page(个人主页)”,现在代表“PHP: Hypertext Processor (PHP;超文本处理器)”。
Python是一种解释性的、交互式的面向对象语言,能在多种环境中运行。它最常见的用处是编写脚本和小型Web应用程序,也支持创建更大型的程序。
SQL语言是查询、更新、管理关系数据库的事实标准。"SQL”代表“StructuredQuery Language(结构化查询语言)”。与本节列出的其他语言不同,SQL是“声明式”语言,意思是说,它不是定义一系列操作,而是定义某些操作的结果。
Visual Basic是一种高级的面向对象的可视化Basic(Beginner's All-purpose Symbolic Instruction Code)语言,由Microsoft开发,最初设计是为了创建Microsoft Windows应用程序,它经过扩展,可以定制桌面应用程序(如 Microsoft Office)、创建Web程序,以及其他应用。在21世纪初期,使用Visual Basic的专业开发人员比用其他任何语言的都多。
4.2 编程约定
高质量软件在构建开始前,应该对变量名称、类的名称、子程序名称、格式约定、注释约定等做规范性约定。这些约定要精确到,一直到编写完软件后,也几乎完全不用修改(增加或删除或改变)这些约定。
4.3 你在技术浪潮中的位置
如果你正在使用一个新技术,即处于技术浪潮的前端,那么做好要花很多时间,去填没有文档、玄学bug、未加说明的语言特性等坑的准备。如果你处于浪潮的后期,显然你就可以用大部分时间来稳定持续地编写新功能。
当你在深入一种语言去编程后,或许会发现该语言的许多缺陷。尝试发明你自己的编码约定、标准、类库等作为改进措施吧。
4.4 选择主要的构建实践方法
下表是构建实践中需要注意的细节:
编码
-
你有没有确定,多少设计工作将要预先进行,多少设计工作在键盘上进行(在编写代码的同时)?
-
你有没有规定诸如名称、注释、代码格式等“编码约定”?
-
你有没有规定特定的由软件架构确定的编码实践,比如如何处理错误条件、如何处理安全性事项、对于类接口有哪些约定、可重用的代码遵循哪些标准、在编码时考虑多少性能因素等?
-
你有没有找到自己在技术浪潮中的位置,并相应调整自己的措施?如果必要,你是否知道如何“深入一种语言去编程”,而不受限于语言(仅仅“在一种语言上编程”)?
团队工作
-
你有没有定义一套集成工序——-即,你有没有定义一套特定的步骤,规定程序员在把代码check in到主源码中之前,必须履行这些步骤?
-
程序员是结对编程、还是独自编程,或者这二者的某种组合?
质量保证
-
程序员在编写代码之前,是否先为之编写测试用例?
-
程序员会为自己的代码写单元测试吗(无论先写还是后写)?
-
程序员在check in代码之前,会用调试器单步跟踪整个代码流程吗?
-
程序员在 check in 代码之前,是否进行集成测试?
-
程序员会review别人的代码吗?
工具
-
你是否选用了某种版本控制工具?
-
你是否选定了一种语言,以及语言的版本或编译器版本?
-
你是否选择了某个编程框架,或者明确地决定不使用编程框架?
-
你是否决定允许使用非标准的语言特性?
-
你是否选定并拥有了其他将要用到的工具——编辑器、重构工具、调试器、测试框架、语法检查器等?
Key Points
每种编程语言都有其优点和弱点。要知道你使用的语言的明确优点和弱点。
在开始编程之前,做好一些约定( convention)。开始编码以后再改变代码使之符合约定是近乎不可能的。
“构建的实践方法”的种类比任何单个项目能用到的要多。有意识地选择最适合你的项目的实践方法。
问问你自己,你采用的编程实践是对你所用的编程语言的正确响应,还是受它的控制?请记得“深入一种语言去编程”,不要仅“在一种语言上编程”。
你在技术浪潮中的位置决定了哪种方法是有效的─甚至是可能用到的。确定你在技术浪潮中的位置,并相应调整计划和预期目标。