目录
软件配置管理(SCM)是“系统化地定义软件项目工作和处理变化,以使项目保持其完整性”的实践活动。SCM关注于程序的需求、源码、文档和测试数据等所有项目相关的产物。配置管理策略将决定如何管理项目中发生的一切变化。因此,它记录了你的系统以及应用程序的演进过程。另外,它也是对团队成员协作方式的管理。
SCM的主要任务有两个:第一,变更控制;第二,版本控制。以上两点只是对SCM的基本概括,想要做好SCM不仅需要完善的流程也需要强大的软件工具支持。
唯一不变的就是变化本身,接受变更是不争的实事。软件产品易于掌握的特性和不可见性,往往导致它的构建人员面临着永恒的变更。因此必须制定相应的变更管理计划,防止变更失控。
执导原则:
- 遵循某种系统化的变更控制手续。
- 将变更请求作为整体考虑。
- 评估变更的成本,并告知相关干系人。
- 提防大量的变更请求,它表示先前的工作做得不够好。
- 成立变更控制委员会或类似组织。
一般程序:
- 识别变更。
- 评价变更对项目整体的影响。
- 寻找处理变更的备选方案。
- 征求项目干系人的意见。
- 批准或否决变更。
- 追踪变更的实施情况。
必须清醒的认识到——没有不变的需求。变更控制只是被动地应对到来的变更,此外,我们还可以主动迎击变更。做法就是将不稳定的区域隔离出来,从而把变化带来的影响限制在一个层、类或者组件的内部。下面给出相应措施:
1 找出看起来容易变化的范围。需求中应该包含这份潜在变化的清单,下面是一些容易变化的区域:
-
- 业务规则
- 对硬件的依赖
- 输入和输出
- 非标准语言特性
- 困难的设计区域和构建区域
- 状态变量(使用枚举;使用访问程序取代多状态变量的直接检查)
- 数据量的限制(eg.数组使用命名常量定义容量)
2 把容易变化的项目分离出来。
3 把看起来容易变化的项目隔离起来。
(这绝不是超前设计,而是利用分层、设计模式来达到牺牲少许性能来降低耦合度,提高灵活性的方法。)
版本控制系统是保存文件多个版本的一种机制。版本控制软件可以帮助你得到以下益处:
- 别人正在修该某文件的同时,你可以同时修改这个文件。
- 你能方便地将你机器上的全部项目文件更新到当前版本。
- 你可以回溯到任何文件的任意版本。
- 你可以获得一份文件更改的历史记录。
- 你无需本地备份,因为版本控制提供了安全保障。
(一) 版本控制的内容
版本控制的目标是能够随时获取软件在整个生命周期中任意时间点的文件状态。因此,任何与项目相关的文件都应该进行版本控制。以下内容应该进行版本控制:
- 管理相关文档(包括项目章程、合同、项目范围说明书、各类计划、各类核对表等项目管理过程中涉及的文档)
- 开发相关文档(包括编码规范、需求分析、概要设计、详细设计、数据字典、API帮助手册、测试报告等涉及软件构建的文档)
- 系统原型
- 源代码
- 实例代码
- 测试脚本
- 数据库脚本
- 构建和部署脚本
- 配置文件
- 编译器及第三方工具
- 库、SDK
以上内容都应该纳入到版本控制行列,但根据项目规模可以有所裁剪,但以上加粗的内容必须进行版本控制。而有些内容不纳入版本控制,这些内容是:
- IDE、DBMS、CASE工具等 这类软件太大,而且管理他们没有意义。
- 源代码编译后的二进制文件 没有保存的意义,一些编译器会针对本地CPU进行优化。
- 项目无关的任何文件 禁止在公共版本库中签入自己的测试DEMO。
(二) 版本控制策略
- 保证频繁地提交可靠代码 只有频繁地提交代码才能体现出配置管理的好处(轻松回滚到某个版本),而且能够尽快释放独占的资源,方便其他成员编辑。但是,并不是无限制的肆意追求提交频率,而应该保证提交代码的质量。在提交前获取最新版本,并保证编码编译没有问题是最基本的要求。将缺陷签入版本控制只会浪费团队的时间。更好的做法是,集中进行代码审查,并将审查过后的代码签入版本控制库。
- 对提交权限进行限制 不应该对全员开放全部权限,否则存在因为人为操作而影响开发的各种事件。最常见的是,将未经审查的代码签入当前版本,并导致全员的编译错误。还有,就是由于对软件使用不熟练而引起的各种误操作。对不同成员,开放不同的权限是个比较稳妥的方案。
- 确保团队中所有人在下班前签入代码,并在开发中都能及时获取最新版本 如果你的团队在使用版本控制软件,那么你上班的第一件事就不仅仅的查收邮件,还有同时获取最新版本的工程。及时获取最新版本是件好事,因为多数开发人员是在进行C+V的操作,这也是为什么BUG只是出现集群现象的原因之一。当我们Copy代码时,复制新的往往比旧的更好,因为新版本可能修复了某些缺陷。
- 使用意义明确的提交注释 强制要求团队成员使用注释的原因是:当构建失败后,你知道是谁破坏了构建,以及可能的原因及缺陷位置。这些附加信息,可以缩短我们修复缺陷的时间。推荐的注释风格是这样的:第一段是简洁的概括性描述,之后是更新的具体细节。如果版本控制软件支持,可以插入链接,将其链接到相关资源。
- 及时同步并定义明确的基线 基线应该是当前可交付成果的一个已确认的稳定的,具有正确部署状态的版本。基线管理的主要内容包括: 应用程序的源代码、构建脚本、测试、相关文档、数据库脚本、库及配置文件等等。相关文档必须保持同步更新,例如测试脚本的版本应该与代码的版本一致。基线作为某个重要的里程碑,其可交付成果应该是已确认的,是经过实际测试过的,具有较高质量的稳定版本。基线必须被妥善的管理和维护,因为它将是今后开发的基础,并且是具有价值的存在,是重要的组织过程资产。
- 使用增量的方式对当前代码进行修改,尽量避免创建分支 创建分支存在以下问题:
1 推迟了新功能的整合,对分支进行合并存在发生集成问题的风险。
2 多个分支难以管理,并会增长集成的风险。
3 很难避免重名问题。
4 代码整合将会随着分支数量的上升,变得越来越困难。
备份计划指的是定期备份工作的成果。你的备份计划应该包括定期进行备份,并且定期地将备份介质转移到脱机存储介质中。
在制定备份计划的时候,人们常常忽略的一点是:要测试你的备份过程。应该对数据恢复进行演练,以确认备份数据中包含了所需要的全部数据,并且可以成功恢复。
必须由专人负责,做到责任到人并且明确定义需要备份的内容以及备份的周期。负责人往往具有版本控制软件最高的权限,可以查阅所有文档,而且其维护的是企业的重要组织过程资产,责任重大,应该谨慎选择。
(一) 介绍
SVN是一款非常知名的中央版本控制系统。(与中央版本控制系统对应的是分布式版本控制系统,如DVCS。分布式版本控制系统支持大型开源团队的需要,一般项目使用中央版本控制系统足以。)版本控制系统用于维护应用程序每次修改的完整历史,包括源代码、文档、数据、脚本等等。它的另一个重要用途,让团队一起工作在应用程序的不同部分,同时维护系统记录,即应用程序的权威代码基。SVN由商业组织Collabnet维护,它提供收费支持。
SVN中,版本控制的单元是修订(revision),它由多个目录内文件的一组变更构成。可以把每个修订看成当时版本库中所有文件的一个快照。在SVN中每次提交都会应用所有更改,这一过程是原子性的,并且创建一个新的修订版本。SVN版本号是针对整个代码库的,而不是每个文件对应一个版本号。SVN用对待文件的方式对待目录、文件属性和元数据。
每个SVN版本库在创建时,可以选择生成3个默认目录:trunk(主干)、tags(标签)和branches(分支)。当创建分支(标签)时,只是在branches(tags)目录下创建一个新目录,并将trunk上你想创建分支(标签)的那个修订版本的内容复制到该目录中。刚刚创建的分支(标签)和trunk指向同一组对象级,直到它与主干开始不一致为止。SVN不区分标签与分支,它们只是用途不同而已。
SVN的一些限制或者说是需要注意的地方:
- 只能在线提交变更。
- SVN在本地客户端用于更正变更的数据被保存在版本库每个文件夹的.svn目录下。
- 尽管服务端是原子性操作,但客户端的操作不是。
- 修订号在知道的版本库中是唯一的,在不同版本库之间不唯一。
- SVN支持乐观锁和悲观锁。
(二) VisualSVN-Server(服务端)
1 安装
第一步:
第二步:
2 使用
主界面如下:
第一步:创建版本库
第二步:创建用户
第三步:创建分组
第四步:配置权限
(三) SVN(客户端)
1 安装SVN
安装过程唯一需要注意的是,在安装完成中文语言包后,要在SVN中设置使用语言,否则还是会按英文显示。
第一步:点击“Settings”。
第二步:设置使用语言。
2 SVN的使用
SVN的定义的图标及其含义如下:
无版本控制。
正常状态。
表示本地文件与版本库不同步。
本地文件与版本库存在冲突。
表示只读文件(被他人加锁的文件)。
表示该文件已加锁。
表示当前文件夹下有文件或文件夹从版本库中删除或者丢失了。
表示有文件或目录被添加到了版本库中。
- 连接SVN版本库
第一步:新建一个存放版本库的目录。
第二步:点击目录,右键弹出菜单选择“SVN检出”。
第三步:输入URL。
第四步:输入用户名及密码。
- 更新文件
获取最新版本 在右键菜单中点击“SVN更新”。
获取特定版本 在右键菜单中点击“更新至版本”。
- 添加文件
第一步:新增文件。新增文档会显示图标,表示无版本控制。在该文件上右键选择菜单选择“增加”,以声明对该文件进行版本控制。
第二步:声明对其应用版本控制。添加之后,图标会变为,此时文件只是存在于本地,只有“签入”后才会上传到版本库中。
第三步:“签入”修改。
- 删除文件
处在版本控制下的文件,不能在本地删除,需要从版本库中删除才能彻底删除文件。
第一步:选中目录或文件,在右键菜单中选择“删除”:
第二步:“签入”修改。
- 签入文件
选中目录或文件,在右键菜单中选择“”,会出现提交对话框,按需填写相应信息,并选择需要签入的文件:
点击“确定”开始签入文件,签入成功后会返回本次操作的结果:
- 重命名文件
第一步:右键菜单选择“改名”。
第二步:输入新名称。
第三步:“签入”修改。
- 创建分支
第一步:选中目录,点击右键菜单中的“分支/标记”。
第二步:输入分支保存路径以及日志信息。
第三步:获取最新版本。如果不获取最新版本,在目标位置是找不到分支目录的。
- 合并分支
第一步:在相关目录下的空白处(即合并起始位置的URL,可以从分支合并到主干,也可以从主干合并到分支),点击右键菜单中的“合并”。
第二步:选择合并类型。
第三步:合并。
第四步:如果存在冲突请解决冲突。(这里不做详细解释,下文会详细解释冲突产生过程及如果解决。)
- 其它常用功能
- 使用浏览器查看版本库
输入正确的服务地址,用户名密码即可。
3 版本冲突
- 版本冲突的产生过程
以下流程模拟了一个版本冲突的产生。
用户B在签入时会失败。
- 版本冲突的解决方法
- A放弃自己的更新。(回避问题)
- B放弃自己的更新。(回避问题)
- 解决A与B的冲突。(解决问题)
前两种做法比较极端,且在回避问题,以下说明第三种做法的步骤。
第一步:用户B获取最新版本。获取后,会发现冲突文件被会显示。
第二步:选中冲突文件,点击右键菜单中的“编辑冲突”。
第三步:将修改后的文件标记为已解决。
第四步:用户B“签入”文件。
我在“三 版本控制”中阐述版本控制的基础知识,其中主要包括的版本控制的内容与策略。为了能将理论联系实际,以下给出了一些比较好的实践,以供参考。
(一) 工作之前先更新
应该频繁地更新工作有关的文件,频繁获取最新版本,这对一个团队活动来说十分有益。因为当你在发现返回结果莫名其妙的时候,可能你的队友已经提交了对某个缺陷的修改。很多开发人员都知道新的版本一般会包含更少的缺陷,但仍有两方面,在困扰这开发人员去频繁获取最新版本:
第一,队友在肆意破坏构建。可能你更新一次,就会因为别人引人的某个缺陷而无法继续工作。
第二,不稳定的网络。更新时间很长,长到难以忍受。
可能最多的情况还是因为第一点。对于这种情况,如果这种肆意破坏构建的现象已经普遍了,那认命吧,脱机使用可能更好一些。
(二) 构建失败了不要提交新代码
正确的步骤是在提交修改之前,应该先获取相关文件的最新版本,然后重新编译工程。待本地测试并通过代码审查(如果有)后,再提交新代码。如果构建失败了,你应该修复代码,而不是把问题扩散到版本库中。我曾目睹过可笑的事情:一位开发人员,把编译不通过的代码签入服务器的原因竟然是想让别人帮他看看是哪里写错了。不要让这种闹剧在你的团队中上演,如果你的某位成员经常性地破坏构建,那么你应该果断地收回权限,以维持版本库正常的构建,减少不必要的修复时间。
(三) 提交有受测的或已审查的代码
版本控制要求成员提交可靠代码。测试和代码审查均是代码质量保障的有效手段。代码审查的软件质量控制最有力的手段,其缺陷检出率也是最高的,但也将消耗掉相当大的成本。并不是所有团队都能做好代码审查,代码审查需要技术硬手,要有一定的重构经验。事实是,很多小项目没有配置相应代码审查人员,或因团队能力有限而放弃代码审查步骤。导致缺陷在项目中蔓延,代码的味道越来越坏。代码审查与测试的重要性,并不是本文讨论的重点。我要说明的是,我们进行配置管理的对象是可靠的代码,可靠的代码不等于编译通过即可,它至少意味着更少量的缺陷。所以,即使你的项目团队没有配置代码审查,没有人写单元测试,最起码地请保证在警告数量为0后签入代码,因为IDE的作者们比你更了解语言。
(四) 提交后测试主版本是否可以工作,成功后再干别的
这是很多团队都在采用的方法。道理很简单,你的代码没问题了你才可以下班。很多团队都是在快下班时,要求成员提交代码。实践证明并不理想,因为经常会发生一件最最痛苦的事情——下班了,活儿来了。我更倾向于在下午3:00发布系统。第一,每位成员都清楚程序发布的时间,在早晨他们就会处理手上的代码(为了按时回家),一般在上午就能完成调试和测试工作,很多事情没有必要非拖到下班来干。第二,因为下午2:00~3:00,人会犯困,在开发人员等待测试人员的反馈时,他们可以休息一下,毕竟发布前的准备可能已导致了数日的赶工。第三,也是最重要的一点,一旦构建出现问题,我有足够的时间处理缺陷,并能确保当天测试团队能拿到一个可测试的版本。
(五) 签入代码时最好进行一次代码走查并确保签入了所有相关代码
在签入代码时进行一次差异比较是一种好的实践,成员可以最后一次查看自己的代码,进一步降低缺陷别引入的几率。切忌不要仅仅签入部分代码,这样做很可破坏构建。你自己可能没有察觉,因为本地构建是正常的,可是,其他人却因为缺少某个文件而无法通过编译。
(六) 时刻准备着回滚到候选版本
应该维护好我们的版本信息,清楚当前回滚的最佳选择。
(七) 在回滚之前规定一个修复时间
建立一个团队规则:如果因某次提交而导致构建失败,必须在某个时间之内修复它,如果无法修复,则回滚到之前的最好版本。
(八) 责任到人
将其与绩效考核紧密联系,让守规矩的成员得到褒奖,并让肆意破坏构建,违反团队规则的人,为他的行为负责。
(九) 乐观锁往往更高效
SVN默认使用乐观锁(不会以独占方式打开文件,他人也可以修改该文件),但同时支持悲观锁(独占文件方式)。两者的选取主要由成员的分工,系统架构的耦合程度来决定。如果,某些文件很少被公用,对于这些文件则使用悲观锁比较有利于解决合并冲突。相对的,如果团队成员可能会同时工作在同一个文件上,例如:所有成员均需要编辑DAL层某个类的代码,那么这个文件就需要使用乐观锁。事实上,越是大项目,人员越多,系统解耦越不彻底,越适合使用乐观锁。对于某个文档的修改时,可能是你使用悲观锁的少有的情况。
我在上文提到过,应该尽量避免创建分支,而是使用增量式的开发。每次创建分支,都要认识带它带来的成本。这种成本在于“增加了风险”,而唯一最小化风险的方法就是无论由于什么样的理由创建了分支,都要努力保证任何活跃分支合并到主干。
在开发过程中,通过频繁向主干提交的方式做增量式修改几乎总是最正确的做法,主干开发也是集成成本最低的模式。在主干开发中,开发人员几乎总是签入代码到主干,而使用分支的情况很少。主干开发有以下优点:
- 确保所有的代码被持续集成。
- 确保开发人员及时获得他人的修改。
- 避免项目后期的“合并地狱”和“集成地狱”。
主干开发并不排除分支。它意味着——所有的开发活动在某一时间点上都会以单一代码基线而告终。主干开发的一个可预测结果是:每次向主干签入并不都是可发布状态。任何人对主干的修改都可能破坏构建。因此,在使用主干开发的团队中,应该进行持续的过程改进以缩短修复构建的时间,保证主干在大多数时间处于可工作的状态。
推荐使用分支的情况包括:
- 为发布创建分支 为了发布应用程序的一个新版本需要创建一个分支。这使开发人员能够不断开发新功能,而不影响稳定对外发布版本。当发现缺陷时,首先在相应的对外发布分支上修复,然后再合并到主干上。而该分支从来不会合并回主干。
- 为研发创建分支 当需要调研一个新功能或做一次重构时——调研分支最终会被丢弃并且从来不会合并回主干。
- 大范围的修改 比如决定使用WPF代替WinFrom时,需要一部分人继续维护WinFrom版本,而另一些人开发WPF版本。类似这种大范围的修改,也可以使用分支。
下面介绍常见的分支模式。
(一) 为发布创建分支
在某个版本即将发布之前,为该版本创建分支,该版本的测试和验证全部在该分支上进行,而最新的开发工作仍旧在主干上进行。在这种模式中,要遵循如下规则:
- 一直在主干上开发新功能。
- 当待发布版本的所有功能都完成了,且希望继续开发新功能时才创建一个分支。
- 在分支上只允许提交那些修复缺陷的代码,并保证这些代码必须尽快(最好立即)并完整地被合并回主干。(我使用核对表,来确保不遗漏问题。)
- 当执行实际发布时,这个分支可以选择性地打一个标签。
- 不要在已有的发布分支上再创建更多的分支,所有的后继分支应该从主干上创建,分支的形状如下:
(二) 按功能模块分支
使明确分工的团队,在各自负责的模块上工作,在并行开发的同时,保持主干的可发布状态。使用该模式要做到以下环节:
- 每天都要把主干上的所有变更合并到分支上。
- 每个功能模块分支都应该是短生命周期的。
- 分支只有经过用户确认后,才能合并回主干。我这里所说的确认,应该理解为由关键用户对设计的认可,而不是走完完整测试流程后进行的验收确认。因为,在分支上,开发人员只能进行单元测试,其它大规模测试针对的是主干。所以,分支合并前,不建议做大规模测试,也很难有足够的资源做大规模测试。我建议,让关键用户确认其设计是否能满足用户当前需求,如果满足,则合并回主干进行大规模测试;如果不满足,则走变更流程。
- 重构必须即时合并,从而将合并冲突最小化。
- 保证主干的可发布状态。
- 不屈服于交付压力而合并为达标的分支。
- 尽量避免在分支上再创建分支。
使用这种模式太容易“将确保应用程序处于可发布状态所需要解决的痛苦”推迟到后期。应该做成分的估计,确保采用这种模式所取得的收益远比其开销更重要,而且要确保在按进度发布系统。
该模式分支形如:
(三) 按团队分支
该模式分支形如:
按团队分支的流程如下:
- 创建多个团队,每个团队自己都有对应的分支。
- 一旦该团队的阶段性工作完成,就让该分支稳定下来,并合并回主干。
- 每天将主干上的变更合并到每个分支上。
- 对于每个分支,每次签入后都要运行单元和验收测试。
- 每次一个分支合并回主干时,在主干上都要运行所有的测试。
非常重要的是,每个分支都要有一个责任人,一般是TeamLeader,由他负责定义和维护该分支的规则,包括管理谁可以向分支提交代码。
这种分支模式与按功能模块相比,优点是分支较少,而缺点是各分支很快会变得差异很大。主要风险是,在进度压力下,团队不能充分遵守关于合并回主干以及从主干更新代码的规则。团队分支很快会和主干变得不一样,彼此之间的差异也会很大,所以,合并会很耗时。
现实的项目中,需求变更以及在配置管理中使用分支几乎无法避免。好的SCM可以有效地使变更处于受控状态,降低合并冲突,保证主干代码尽可能处于可发布状态。但是,项目规模做大,管理力度失衡,就会发生种种失控的状态。变更控制会失效,成员深陷于痛苦的“变更>修改>发布>变更>修改>发布”的死循环中。分支之间相互嵌套,团队规范形如废纸。版本库形成非常复杂的网络已经无法维护。伴随着分支的积增,资源的分配也更加细碎,成员之间会彼此抱怨别人的代码破坏了构建。需求无法冻结,导致长期的赶工,资源开始流失。这种情况,很可能在我们的身边上演。不要寄希望于SCM能完全防御掉这些风险。SCM只是我们促成项目成功的方式之一。
我的建议是,在合理范围下,使用再多的有利于促成项目成功的措施也不为过。对于配置管理,应该以主干开发为首选,如果没有必要绝不要使用分支。此外,程序架构应该尽力解耦,松耦合的开发可以将影响和变化源缩减到很少。松散的耦合,更利于应对突如其来的变更。此外,越少的修改,合并冲突的几率就越低。因此,降低集成成本,不仅需要合理的SCM策略,也需要在程序架构方面动一番脑筋(例如面向组件开发,面向服务开发)。
主要参考书籍:
- 《代码大全》
- 《持续交付:发布可靠软件的系统方法》