翻译:廖斌
#//////////////////////////////////////////////////////////////////////
# 使用CVS进行版本管理
# 2000-6-30
# CVS 1.10.7
#
#
#
# Per Cederqvist et al 著
# 廖斌 译
# 感谢黄箐的输入和排版工作
# copyrigh: GPL
#
#//////////////////////////////////////////////////////////////////////
快捷内容索引
1. 概述.........................................................1
2. 代码仓库.....................................................7
3. 使用CVS开始一个项目..........................................29
4. 主干版本(Revision).........................................33
5. 分支与合并...................................................41
6. Recarsive behavior...........................................49
7. 增加,删除,更名文件和目录...................................51
8. 回顾开发历史.................................................57
9. 放置二进制文件...............................................59
10 多个开发者的同时工作........................................61
11 主干版本管理.............................................71
12 关键字替代................................................73
13 跟踪第三方代码...............................................77
14 你的系统如何同CVS交互........................................81
15 特殊文件.....................................................83
16 附录
1) CVS命令导向..................................................85
2) CVS命令快速参考............................................115
3) 管理文件的参考习册.........................................127
4) 影响CVS的所有环境变量......................................141
5) CVS各版本之间的兼容性......................................143
6) 遇到的问题.................................................145
7) Credits....................................................153
8) 对CVS和这本手册的总是处理..................................155
目录 ...........................................................157
1 概论
这一章为从未用过CVS的人写的,也许以前也从未用过任何版本控制工具。
1.1 什么是CVS?
CVS是个版本控制系统,使用它你可以记录你原代码文件的历史。
例如,当软件修改时有时会产生问题(*bugs这里被译为问题),并且你可能在做这
次修改后很长时间不会发现这些问题。使用CVS,你可以容易地回顾老的代码版本去
发现哪一次的修改导致这些问题。有时候这样会非常有帮助。
你可能会保留你每一次的代码版本,这可能会浪费你很多的代码空间。CVS使用一
种聪明的办法保存你的多个版本在一个文件中。它仅仅保留版本间的不同内容。如果
你是一个项目中的一组成员之一,CVS也能够帮助你。除非你特别仔细,你很容易覆盖其
他人的工 作。一些编辑器,例如GNUEmacs,试图去判定一个文件是否被两人同时修改。
不幸的是,如果一个人使用其它的编辑器时,这个安全方式将不再有效。CVS使用让不同
开发者独立工作的方式解决了这个问题。每一个开发者的工作都在他自己的目录内,并且
CVS将 在每个开发者的工作完成后进行合并工作。
CVS是由Dick Grune作为Shell脚本的一个分支而创建的,1986年10月,在它的第6个发行卷
时,它被投递到新闻组comp.soures.unix。然而现在的CVS冲突算法中没有任何代码是从这
些脚本中来的。
在1989年3月,Brian Berlinor设计并编写了CVS的代码。Jett.Polk在以后帮助 Brian 完成了CVS
模型的设计和商业版本支持。
你可以通过不同的方式得到CVS,包括在Internet上自由的下载。如果你想下载CVS和其它CVS
文章以得到更多的信息,请看:
http://www.cyxlic.com
http://www.loria.fr/~molli/cvs-index.html.
有一个叫 info-cvs 的邮件列表是有关CVS的内容。订阅或取消订阅这个列表请发邮件到
info-cvs-request@gnu.org
如果你更喜欢Usenet新闻组,CVS的讨论组为
Comp.software.confg.mgmt.
在将来也许会建一个
Comp.software.confg.mgmt.cvs
的新闻组,
但也许这会是Comp.software.confg.mgmt在有太多讨论之后的事情吧。
你也许想订阅bug-cvs的邮件列表,这在附录H[BUGS]中有更多的信息。订阅它请发Email到
bug-cvs-reqnest@gnu.org.
1.2 CVS不能做的事
(未译)
2
3 使用CVS开始一个项目
=======================
因为更改文件名并且把它们移动到另一个目录中不是经常发生的,因此你在开始一个新项
目时要做的第一件事是考虑你的文件组织。更改文件名或移动文件并非不可能,但增加了
理解上潜在的费解,并且CVS在更改名字的目录上特别的敏感。请参见7.4节[移动文件]。
(* 译者注: 在Unix中改名和移动是相同的)。
下一步做的事取决于手中的情况。
3.1 建立文件
第一步是在仓库中建立文件。这可以使用多种不同的方法来完成。
--------------------------------------------------------------------
3.1.1 建立一个目录树
当你开始使用CVS时,你可能已经有几个项目可以使用CVS进行管理了。在这种情况下,
最容易的方法就是使用: "import"命令。一个例子是最容易解释如何使用它的。假定你现
在有一些你想放到CVS中的文件在"wdir"中,并且你想让它们放在数据仓库中的如下目录:
"$CVSROOT/yoyodyne/rdir"
你可以使用如下命令:
$cd wdir
$cvs inport -m "Inported Sources" yoyodyne/rdir yoyo start
如果你没有使用"-m"参数记录一个日志信息,CVS将调用一个编辑器(*译者注:通常是vi)
并且提示输入信息。"yoyo"字符串是开发者标笺,"start"是发行标笺。他们没有什么特别
的意义,仅仅是因为CVS的需要才放在这里。
请参见第13章[跟踪代码],得到更多的这方面信息。
你现在可以检查一下它是否有效了,然后可以删除你原来的代码目录。
$cd
$mv dir dir.orig
$cvs checkout yoyodyne/dir
$diff -r dir.orig yoyodyne/dir
$rm -r dir.orig.
为了避免偶然进入到你原来的目录中去编辑文件,删除原来的代码是个好主意。当然,在
你删除之前保留一份备份到其它地方也是明智之举。
"checkout"命令能使用模块名作为参数(正如前面所有例子)或是一个相对于$CVSROOT的路
径,如同上面的例子。你应当检查CVS目录中的权限情况是否合适,应当使它们属于某一个
特定的组。请参见2.2.2.节[文件权限]。
如果你想"import"的一些文件是二进制代码,你可以使用一些特殊的方法表明这些文件是否
是二进制文件。请参见C.2节[Wrappers]。
------------------------------------------------------------------------------------
3.1.2 从其它版本控制系统建立文件
如果你有一个其它版本控制系统维护的项目,例如RCS,你也许希望把这些文件放到CVS中,
并且要保留这些文件的历史。以下是一些讨论。
从RCS:
如果你使用RCS,找到RCS文件??通常一个文件名叫"foo.c"的文件会有"RCS/foo.c,v"的RCS文
件。(但它有可能在其它地方,请看RCS的文档以得到相关信息)。如果文件目录在CVS中不存
在,那在CVS中创建它。然后把这些文件拷贝到CVS的仓库目录中(在仓库中的名字必须是带
",v"的原文件;这些文件直接放在CVS中的这个目录下,而非"RCS"子目录中)。这是在CVS中
一个为数不多的直接进入CVS仓库直接操作的情况,而没使用CVS命令。然后你就可以把它们
在新的目录下"checkout"了。
当你把RCS文件移动CVS中时,RCS文件应在未被锁定的状态,否则移动操作时CVS 将会出
现一些问题。
从其它版本控制工具
许多版本控制工具都可以输出"RCS"格式的标准文档。如果你的版本控制工具可以做到这一
点,直接输出RCS文件,然后按照上面的例子做就可以了。
如果你的版本工具不能输出RCS文件,那么你必需要写一个脚本文件来,每次取出一个版本
然后把它放到CVS中去。下面提到的"sccsarcs"脚本就是一个很好的例子。
从SCCS:
有一个"sccsarcs"的脚本文件可以做把SCCS的文件转化成RCS文件,这个文件放在CVS原代码
目录的"contrib"目录中。注意:
你必须在一台同时安装了RCS和SCCS的机器上运行它。并且,正如其它在"contrib."目录中的
其它脚本一样。(你的方法也许是变化多端的)
(*译者注:我并未查看过CVS的contrib目录:-(,因此不知道这下面都有些什么)。
从PVCS:
在"contrb"中有一个叫"pves-to-rcs"的脚本可以转换PVCS到RCS文件。你必须在一台同时有
PVCS和RCS的机器上运行它。
请看脚本中的注释以得到更多细节。
3.1.3从无到有建立一个目录树
建立一个新的项目,最容易的方法是建立一个空的目录树,如下所示:
$mkdir tc
$mkdir tc/man
$mkdir tc/testing
在这之后,你可以"import"这个(空)目录到仓库中去。
$cd tc
$cvs import -m "created directory structure"yoyodyne/dir yoyo
start
然后,当新增一个文件时,增加文件(或目录)到仓库中。请检查$CVSROOT中的权限是否正确。
-------------------------------------------------------------------------------------------
3.2 定义模块
下一步是在"moduyes"文件中定义模块。这不是严格需要的,但模块能把相关的目录和文件容易
关联起来。下面的例子可以充分演示如何定义模块。
1. 得到模块文件的工作拷贝。
$cvs checkout CVSROOT/modules
$cd CVSROOT
2.
编辑这个文件并写入定义模块的行。请参见2.4节[管理文件的介绍]。有一个简单介绍,参见C.1节
[模块文件]。有它的详细描述。你可以使用下面的行定义模块"tc":
tc yoyodyne/tc
3. 提交你的更改到仓库
$cvs commit -m "Added tc module." modules
4. 发行模块文件
$cd
$cvs release -d CVSROOT
4
=======
5 分支与合并
==================
CVS允许你独立出一个派生的代码到一个分离的开发版本---分支。当你改变一个分支中的文
件时,这些更改不会出现在主开发版本和其它分支版本中。
在这之后你可以使用合并(merging)把这些变更从一个分支移动到另一个分支(或主开发版
本)。合并涉及到使用“cvs update -j”命令,合并这些变更到一个工作目录。你可以确认
(commit)这次版本,并且因此影响到拷贝这些变更到其它的分支。
-----------------------------------------------------------------------------------
5.1 何时应当创建一个分支
假定tc.c发行版已完成。你正在继续开发tc.c,计划在2个月后发行1.1的版本。在不久以后你的
客户开始抱怨说代码有些问题,你检查了一下1.0的发行版(请参见4.4节[标笺])并且找到了这
个错误(这将会有一个小小的更正)。但是,这个当前的版本是处于一个不稳的状态,并且在下
一个月才能有希望稳定下来。这样就没有办法去发行一个最新的现有版本去更正问题。
这时就可以去创建基于这棵版本树1.0版的分支。你可以修改这棵树的分支而不影响到主干。当
修订完成时,你可以选定是否要把它同主干合并或继续保留在这个分支里。
-----------------------------------------------------------------------------------
5.2 建立一个分支
你可以使用“tag -b”去建立一个分支。例如,假定你在工作于一个工作拷贝中:
$cvs tag -b rel_1_0_patches
这将基于当前的拷贝分离出一个分支,并分配“rel_1_0_patches”的名字。
懂得分支是在CVS仓库中创建,而非在工作拷贝中创建的是非常重要的。正上面的例
子,创建一个基于当前版本的分支不会自动把当前的工作拷贝转到新的分支上。欲知
详情,请看5.3节[进入 一个分支]。你也可以不参考任何工作拷贝而建立一个分支。
你可以使用rtag命令:
cvs rtag -b -r rel-1-0 rel-1-0-patches tc.
“-r rel-1-0”说明这个分支是基于标志了rel-1-0的版本文件,它不是从最新的版本
开始分支.这对需要从老的版本进行分支很有用(例如:当修订一个过去的稳定版本时)
当使用“tag”标志,这个“-b”标志告诉rtag去创建一个分支(而非是这个版本的符号
连接)。注意标记“rel-1-0”可能对不同的文件有不同的版本数字。因此,这个命令的结果
是为tc模块建立了一个命名为“rel-1-0-patches”的新版本分支,它是基于标记为“rel-1-0”
的版本树。
-----------------------------------------------------------------------------------
5.3 进入分支
你可以通过两种方式进入分支:重新checkout或是从现存的拷贝进入。重新checkout使用
checkout命令并带上“-r”标识,后面是这个分支的标笺(tag)名。(请看5.2[创建一个分支]):
$cvs checkout -r rel-1-0-patches tc.
或者如果你已有了一个拷贝,你可以使用“update -r”命令转到这个分支。
$cvs update -r rel-1-0-patches tc.
或者使用另一个等同的命令:
$cd tc
$cvs update -r rel-1-0-patches
这对现有拷贝为主干代码或是其它分支都是有效的.上面的命令将把它转到命
名的分支。同“update”命令相类似。“update -r”合并你所做的任何改变,请注
意是否有冲突出现。
一但你的工作拷贝已经转向一个特定的分支。它将一直保持在这个分支内,除非你
又做了其它的操作。这意味着从这个工作拷贝checkin的变更将加到这个分支的新版
本中,而不影响到主干版本和其它分支代码。
想看一个工作拷贝是基于哪一个分支,可以使用“status”命令。在它们输出中查找
一个“sticky tag”的域(请参见4.9节["sticky tag"],第38页).那就是你的当前分支号。
$cvs status -v driver.c backend.c
====================================================================
File: driver.c Status: Up-to-date
Version: 1.7 Sat Dec S 18:25:54 1992
RCS version: 1.7 /u/cvsroot/yoyodyne/tc/driver.c,v
Sticky Tag: rel-1-0-patches (branch: 1.7.2)
Sticky Date: (none)
Stick Option: (none)
Existing Tag:
rel-1-0-patches (branck: 1.7.2)
rel-1-0 (revision: 1.7)
====================================================================
File: backend.c status: Up-to-date
Version: 1.4 Tue Dec 1 14:39:01
Rcs Version: 1.4 /u/cvsroot/yoyodyne/tc/
Sticky Tag: rel-1-0patches(branch:1.4.2)
Sticky Date: (none)
Sticky Option: (none)
Existing Tag:
Rel-1-0-patches (branch: 1.4.2)
Rel-1-0 (revision: 1.4)
Rel-0-4 (revision: 1.4)
请不要因为每个文件的分支是不同(“1.7.2”和1.4.2")而迷惑。分支的标笺(tag)
是相同的:"rel-1-0-patches",这些相同标笺的文件是相同分支的。在以上的例子中,分支建
立之前,"driver.c" 比 "backend.c"有更多的变更,因此它们的版本编号是不同的。请参见5.4节
[分支和主干版本号]去了解分支如何构建原理的细节。
--------------------------------------------------------------------------------------
5.4 分支与主干版本
通常,一个文件的主干版本历史是一个增长线(请看4.1[主干版本]页):
+-----+ +-----+ +-----+ +-----+ +-----+
! 1.1 !----! 1.2 !----! 1.3 !----! 1.4 !----! 1.5 !
+-----+ +-----+ +-----+ +-----+ +-----+
然而,CVS并不局限于线性的开发。主干版本可以分为不同的分支,每一个分支可
以是一个独立的自我维护的开发线。而在一个分支中的变更可以很容易的转移到主干中。
每一个分支均有一个分支号,由一个“.”分离的十进制奇数组成,分支号的编排依
赖于它分离出的主线版本。使用分支号允许从一个特定版本分离出多个分支。
所有的分支版本都依赖于它的原始分离版本号。下面的例子将展示这一点。
+-------------+
1.2.2.3.2 分支 -> +--! 1.2.2.3.2.1 !
! +-------------+
!
+---------+ +---------+ +---------+
1.2.2 分支-> +--! 1.2.2.1 !----! 1.2.2.2 !----! 1.2.2.2 !
! +---------+ +---------+ +---------+
!
!
+-----+ +-----+ +-----+ +-----+ +-----+
! 1.1 !----! 1.2 !----! 1.3 !----! 1.4 !----! 1.5 ! <- 主干
+-----+ +-----+ +-----+ +-----+ +-----+
!
!
! +---------+ +---------+ +---------+
1.2.4 分支-> +--! 1.2.4.1 !----! 1.2.4.2 !----! 1.2.4.2 !
+---------+ +---------+ +---------+
你是如何创建具体的分支号的细节通常不是你需要考虑的,但这里谈谈它如何工作。
当CVS建立一个分支号时,它先得到第一个未用的偶数,开始的数字是2,例如你
从6.4的主干版本创建分支时,分支号为6.4.2所有分支号码末位为0的号码用于CVS内
部,(例如6.4.0)。(请参见5.5节[内部分支号]44页)分支1.1.1有特别的含义,请看13章
[跟踪代码]。
-----------------------------------------------------------------------------------
5.5 内部分支号码
这一节描述CVS的内部分支(magic branches) (* 译者注:magic branch 译为内部分支)
特性。在大多数情况下,你不用考虑内部分支号码,CVS将为你进行管理。然而,在
一些特定条件下,它将显现出来,因此理解它如何工作将是有用的。一般的,分支号
码将由奇数个 "."分隔的十进制整数组成。请看4.1节[版本号码]。然而那并非完全是这
样的,由于效率的原因,CVS有时插入一个额外的“0”在右末的第二个位置(1.2.4
变为1.2.0.4,8.9.10.11.12变为8.9.10.11.0.12等)。
CVS将会很好的将这些变换隐蔽在背后进行,但在一些地方,这种隐蔽并不完全:
* 内部分支编号会出现在CVS的日志(log)文件中。
* 你不能够对 "cvs admin" 使用符号分支名。
你可以使用admin命令去为一个分支重新分配一个RCS希望的那样的符号名。如果
R4patches是一个分配给分支1.4.2(内部分支编号为1.4.0.2)的一个文件"numbers.c"的
命名,你可以使用如下命令:
$cvs admin -NR4patches:1.4.2 numbers.c
它将只在至少一个版本已经提交到这个分支时才会有效。请非常小心不要把一个标
笺(tag)分配给了一个错误标识号(现在没有看到昨天的一个标笺是如何分配的)。
----------------------------------------------------------------------------------
5.6 合并一个整个分支
你可以合并一个分支到你的工作目录在“update”命令中“-j 分支号”的标识。使
用“-j 分支号”将合并这个派生分支点与原版本的最新版之间的变更到你的工作目录
“-j”的意思是“join”。
我们现在来考察下面这棵树:
+-----+ +-----+ +-----+ +-----+
! 1.1 !----! 1.2 !----! 1.3 !----! 1.4 ! <- 主干
+-----+ +-----+ +-----+ +-----+
!
!
! +---------+ +---------+
R1fix 分支-> +--! 1.2.2.1 !----! 1.2.2.2 !
+---------+ +---------+
分支1.2.2分配了一个Rifix的名字.下面的例子假定模块"mod"只包含一个文件"m.c"
$cvs checkout mod # 得到最新的1.4版
$cvs update -j R1fix m.c # 合并所有在分支中的改变,即:1.2与1.2.2.2
# 之间的变化到这个文件的工作目
录.
$cvs commit -m "Included R1fix # 建立1.5版
在合并时可能会发生冲突,如果这种情况发生,你可以在提交新版本之前解决它。请
参见10.3节[冲突的例子]。
如果你的原文件中包含关键字(请看第12章[关键字替代])。你可能会得到比严格意义
上的冲突更多的冲突信息。请参见5.10节[合并和关键字],去了解如何避免这个问题。
"checkout"命令也支持使用"-j"参数。下面的例子同上面所用的例子有相的效果。
$cvs checkout -j R1fix mod.
$cvs commit -m "Included R1fix"
---------------------------------------------------------------------------------
5.7 从一个分支多次合并。
继续我们上面的例子。现在这棵树看起来是这样的:
+-----+ +-----+ +-----+ +-----+ +-----+
! 1.1 !----! 1.2 !----! 1.3 !----! 1.4 !----! 1.5 ! <- 主干
+-----+ +-----+ +-----+ +-----+ +-----+
! *
! *
! +---------+ +---------+
R1fix 分支-> +--! 1.2.2.1 !----! 1.2.2.2 !
+---------+ +---------+
正如上面所讨论的,分支1.2.2.2所引导的“*”号表示从Rifix分支到主干的合并。
现在我们继续开发Rifix分支:
+-----+ +-----+ +-----+ +-----+ +-----+
! 1.1 !----! 1.2 !----! 1.3 !----! 1.4 !----! 1.5 ! <- 主干
+-----+ +-----+ +-----+ +-----+ +-----+
! *
! *
! +---------+ +---------+ +---------+
R1fix 分支-> +--! 1.2.2.1 !----! 1.2.2.2 !----! 1.2.2.3 !
+---------+ +---------+ +---------+
然后你可能会希望合并新的变更到主干中去。如果你仍使用“cvs update -j Fifix m.c"
cvs将试图合并你已经合并过的东西,这可能写导致一些不希望发生的东西。
因此,你必须表达清楚你希望只合并未被合并的内容的意思。这样需要使用两个
“-j“参数。CVS合并从第一个“-j”的版本到第二个“-j”版本的变化。例如,在我们上面
的例子中:
cvs update -j 1.2.2.2 -j R1fix m.c
如果出现的问题是你需要手工指定1.2.2.2的版本号,一个更好的方法是使用:
cvs update -j R1fix:yesterday -j R1fix m.c
然而,更好的方式是在每一次合并后重新放一个标笺给Rifix分支,然后使用标
笺做后的合并:
cvs update -j merged_from_Rifix_to_trunk -j R1fix m.c
----------------------------------------------------------------------------------
5.8 合并两个任意版本之间的不同
使用两个“-j”标志,这个update(和checkout)命令能合并两个任意不同的版本
到你的工作目录。
$cvs update -j 1.3 backend.c
将把1.5版本恢复到1.3版本,所以一定要注意版本的顺序。
如果你在操作多个文件时使用这个选择项,你必须了解在不同的文件之间,版本的
数字可能是完全不同的。你必须使用标笺(tag)而不是使用版本号来完成多个文件
的操作。使用两个“-j”操作也能会恢复增加或删除的文件。例如,假定你有一个
叫“file1”的文件在在于1.1版本中,然后你在1.2版本中删除了它,下面是如何操作的例子:
$cvs update -j 1.2 -j 1.1 file1
file1
$cvs commit -m test
checking in file1;
/tmp/cvs-sanity/cvsroot/first-dir/file1 file1,v
new revision:1.3; previous revision:1.2 done
$
------------------------------------------------------------------------------------
5.9 合并能添加和删除文件
如果你在合并时的改变涉及到添加或删除一些文件,“update -j”将反映这些变化。
例如:
cvs update -A
touch a b c
cvs add a b c ; cvs ci -m "added" a b c
cvs tag -b branchtag
cvs update -r branchtag
touch d ; cvs add d
rm a ; cvs rm a
cvs ci -m "added d , removed a"
cvs update -A
cvs update -j branchtag
在这些命令之后(注意要commit),文件a将被删除,而文件d将被加入到主干。
-------------------------------------------------------------------------------------
5.10 合并和关键词
如果你合并的文件包含关键词(参见第12章[关键词替代],73页),你通常将会在
合并时得到 无数个冲突报告,因为在不同版本中非常不同。
因此,你常需要在合并时使用“-kk”(参见12.4节[替代模式],75页)选择项。使用
关键字名字,而非去扩展关键字的值的方法,这个功能选择项确保你合并的版本之间互相相
同,而避免了冲突。
例如:假设你有一个文件如下:
+---------+
br1 -> +--! 1.1.2.1 !
! +---------+
!
!
+-----+ +-----+
! 1.1 !----! 1.2 !
+-----+ +-----+
并且你的当前工作目录拷贝为主干(1.2版本)。那么对于以下的合并将会产生一个
冲突的结果。请看例子:
$cat file1
Key $Revision: 1.3 $
...
$cvs update -j br1
U file1
RCS file: /cvsroot/first-dir/file1,v
retrieving revision 1.1
retrieving revision 1.1.2.1
Meging differences between 1.1 and 1.1.2.1 into file1
rscmerge: warning: conflicts during merge
$ cat file1
$<<<<<<< file1
Key $Revision: 1.3 $
=======
Key $Rerision: 1.1.2.1 $
$>>>>>>> 1.1.2.1
...
冲突发生在试图将1.1和1.1.2.1合并到你的工作目录中去的时候。因此,当这个
关键词从“Revision:1.1"到"Revision:1.1.2.1"时,CVS将试图合并这个变化到
你工作目录, 这就同你的目录中的变更“Revision:1.2"发生了冲突。
以下是使用了:“-kk”后的例子:
$cat file1
key $Revision: 1.3 $
...
$cvs update -kk -j br1
V file1
RCS file: /cvsroot/first-dir/file1,v
retrieving revision 1.1
retrieving revision 1.1.2.1
Merging differences between 1.1 and 1.1.2.1 into file1
$ cat file1
key $Revision: 1.3 $
...
在这里版本“1.1”和“1.1.2.1"都扩展为单纯的 "Revision",因此,合并时就不会
发生冲突了。
然而,使用 "-kk" 参数还一个主要的问题。即,它使用了CVS通常使用的关
键字扩展模式。在特殊情况下,如果模式使用针对二进制文件的 "-kb"
参数。这将会产生问题。因此,如果你的数据库中包括有二进制文件,你将
必须手工处理这些问题,而不能使用 "-kk"。
10 多个开发者同时工作
---------------------
当多个开发者同时参与一个项目时,常常会发生冲突。一般经常发生的情况是两个人想
同时编辑一个文件的时候。它的解决方法之一是文件锁定或是使用保留式的checkout,这种
方法允许一个文件一次只允许一个人编辑。这是一些版本控制系统的唯一解决方式,包括
RCS和SCCS。现在在CVS通常使用保留式checkout的方法是使用"CVS admin-1"命令(参见A-6-1AB
[admin选择项])。在下面将解释这不是一种好的智能的解决方式,当它是许多人喜欢使用的
一种方式。下面也将讲述可以使用适当的方法来避免两个人同时编辑一个文件,而非使用软件
的方式强迫达到这一点。
在CVS中默认的方法是"unreserved checkout"--非保留式的导出。在这种方法下,开发者
可以同时在他们的工作拷贝中编辑一个文件。第一个提交工作的没有一种自动的方法可以知道
另一个人在编辑文件。另一个人可能会在试图提交时得到一个错误信息。他们必须使用CVS命令
使他们的工作拷贝同仓库的内容保持更新。这个操作是自动的。
CVS可以支持facilitate多种不同的通信机制,而不会强迫去遵守某种规则,如"resered
checkouts"那样。以下的部分描述这些不同的方式是如何工作的,和选择多种方式之间涉及到
的一些问题。
10.1 文件状态
基于你对导出的文件使用过的操作,和这些文件在仓库中的版本使用过的操作,我们可以
把一个文件分为几个状态。这个状态可以由"status"命令得到,它们是:
up-to-date:
对于正在使用的这个分支,文件同仓库中的最后版本保持一致。
Locally Modified:
你修改过文件,但没有"commit"。
Locally added:
使用了"add"命令增加文件,但没有"commit"
Locally Removed:
你使用了"remove"命令,但没有"commit"
Needs checkout:
其他人提交了一个更新的版本。这个状态的名字有些误导,你应当使用"update"而非
"checkout"来更新新的版本。
Needs Patch:
象"Needs checkout"一样,但CVS服务将只给出Patch(补丁)文件,而非整个文件。而
给出Patch和给出整个文件的效果是相同的。
Needs Merge:
一些人提交了一个更新版本,而你却修改过这些文件。
File had conflicts on merge:
这同"Locally Modified"相象,只是"update"命令给出了一个冲突信息。如果你还没有
解决冲突,那么你需要解这个问题,解决冲突的方法参见10.3节[冲突的例子].
Unkown:
CVS不知道关于这个文件的情况.例如,你创建了一个新文件,而没有使用"add"命令
为了帮助弄清楚文件的状态,"status"也报告工作版本(working
vevision),这是这个文件是从哪个版本来的,另外还报告"仓库版本"(Repository
vevision)。这是这个文件在仓库中的这个版本分支的最后版本。
"status"命令的选择项例在附录B[invoking cvs]。有关"sticky tag"和"sticky
date"输出内容的信息,参见4.9节[sticky tags]。有关"sticky options"输出内容参见"-k"选择项,
A.16.1节[update选择项]。
你应当把"update"和"status"命令放在一起来认识。你使用"update"使你的文件更新到最
新状态,你使用"status"命令来得到"update"命令将会做何种操作。(当然,仓库中的状态将可
能会在你运行update之前变化)。事实上,如果你想使用一个命令得到比使用"status"正式的状
态信息,你可以使用:
$cvs -n -q -update
这里"-n"选择项表示不真正执行update,而只显示状态;"-q"选择项表示不打印每个目录的
名字。有关更多的关于"update"命令的住处参见附录B[使用CVS]。
10.2 使一个文件更新到最版本
当你想更新或是合并一个,使用update命令。对于一个不是最新版本的文件,这个命令大略等
同于"checkout"命令:最新版本从仓库中提出并放到工作目录中。
当你使用"update"命令时,你修改过的文件在任何情况下不会受到损害。如果在仓库中没有更
新的版本,"update"时你的代码没有任何影响。当你编辑过一个文件,并且仓库中有更新版本,那
么"update"将合并所有的变更到你的工作目录。
例如,想象一个你导出了一个1.4版的文件并且开始编辑它,在某一时候其他人提交了1.5版,然
后又提交了1.6版,如果你运行update命令,CVS将把1.4版到1.6版之间的变更放到你的文件中。
如果在1.4版和1.6版之间的改变太靠近于的你一些变更的话,那么一个"覆盖"("overlop")冲突
就发生了。在这种情况下将输出一个警告信息,然后结果保留的文件中包含了有冲突代码的两个版
本,由特别的符号所分隔开。请参见A.16节[更新],可以得到关于"update"命令的一个完全的描述。
来源:中国Linux论坛
在学习《软件工程》前,我个人倒是着实作了点项目,个人做的和团队合作的都有。但无论是个人做或是团队合作,给我印象最深的就是分工不明,虽然这种组织项目开发的方式快速,但与此对应所带来的恶果常常是混乱和持续不断的错误,并使得开发热情迅速消耗殆尽,最后变成了磨洋工。学了《软件工程》之后,觉得自己的思路开阔了不少。不过对于《软件工程》中所说的知易行难,所以结合自己的经验和当前的主流思想,总结了一些自己在做软件项目中所遇到的问题。
在软件项目中经常会遇到以下几个方面的问题:
1. 需求不充分同时经常变更;
2. 缺乏与客户的沟通;
3. 团队内部沟通不畅;
4. 不必要的复杂度;
5. 团队工作缺乏效率;
上面的第1条好理解,因为这是软件项目成败的基础,还软件是用来干什么的都没搞清楚,那么怎么可能指望它能得到客户的满意呢?为什么会出现需求不充分这种情况呢?主要有两点原因:其一是客户常常自己不能明确地了解自已的需求;其二是就算客户有能力了解一个复杂系统需求的所有细节,他也可能不知道如何有效地组织需求文档。同时就算是在项目开发将近结束的时候,仍会发现新的需求或是要进行需求更改。比如随着程序开发过程的深入,客户对亿们需要什么有了更清楚的了解;客户认识到最初的需求无法满足他们的需要;客户在需求阶段可能提出了一些他们并不真正需要的需求,然后在开发过程中又不想要了;客户需求的改变;客户方的职员或经理发生变更,由于想法不一致,导致需求的改变等等。
第2条也比较容易理解,因为要准确定义需求是一项困难的工作,因此,与客户保持开放和有建设性的交流是很重要的,那样做可以确保你所做的工作正是客户想得到的。一定要通过与客户的交互来完成需求分析。另外,每个项目都是从一些不完善的信息开始的,对于项目将如何进展也是处于一系列设想之中的。通常在随着项目进度的展开,这些设想将不得不进行调整,你和客户将分担这些调整所带来的影响,因此交流的结果最终必须达成协议。
第3条主要是当开发人员增多时,每个开发人员也相应地要与更多的人进行同步,根据《人月神话》这本书上所指到的那样,开发人员的增长,会导致团队里用于沟通的信息量呈二次级数增长。不畅的沟通有两种:太少或太多,太少会导致不能消除系统分歧,太多则浪费太多时间在协调而非代码开发上。
第4条的复杂度增加会占用大量资源和时间进行开发、维护和扩展,从而导致团队工作越来越繁杂且收效甚微。当一个程序规模扩大,内部交互增多时,就会使出错的机会增大,且这些漏洞在开发过程中难以发现。同时复杂性还会导致代码的难以理解。一个项目一般包含很多组件,当构件数目增加时,就会增加每个组件与更多组件交互的机会。当交互数量大大增加时,就不太容易理解程序正在做什么,也使得没有人能够修改或扩展这个程序了。
第5条是在开发中很常见的,因为没有哪个程序员是通过培训成为团队成员的,学校里也不会教学生如何参与团队系统的开发。作为团队成员,沟通、协作和建立共识的技能比个人的编程技能要重要得多,这对于学校出来的程序员来说是认识不到的,他们通常认为自己能写出没有毛病的代码,同时觉得别人的代码都不够完美或严谨。沟通、协作和建立共识是花时间且没必要的,他们崇敬的不是杰出的团队成员,也不是什么团队领导,而是个别有创造力的程序员。而结果是他们的代码难以理解并潜伏着错误,同时带来的问题就是开发时间的延长。
在开始做http://133.newsky.cn之前,我已经明白网站的开发与产品开发没有什么不同。不过在2004年离开微软中国研发中心Office组的时候,我对网站开发仍一无所知,这主要是因为我之前没有任何互联网研发的背景。虽然对传统软件产品的研发管理比较有经验,但从未接触过Internet相关的项目。
从零开始与网站开发亲密接触
去年我接手第一个网站项目http://www.okooo.com开发时,并没有做网站的经验,只能试着按照以前我参与做Microsoft Office时的方法来做:
首先是打造一个便于公司内部沟通交流的内部网,其中包含“传统软件”研发需要的三个工具:文档库(存放公司各项目的文档)、CVS(保存项目的各种源代码)、BugFree(记录项目的各种缺陷);
然后,抓住“需求、开发、测试”三个环节:
l 要做好规划、明确需求。为什么要做这个网站、要达到什么目标?特别是需求,要详细到每个页面的每个区域放置什么内容。网站需求应该由对业务最熟悉的人来定义,他负责按照我要求的规范(详细程度)来写出每一部分需求文档,并放入文档库中。每完成一个页面定义,我就召集开发、测试人员来阅读、讨论,这样全部需求写完的时候,项目组成员对整个网站就有了一个清晰的认识。
l 需求明确才进入开发阶段。首先是定义数据库——有多少张表、每张表中有多少个字段。我和开发组长反复讨论,搞清楚这些表定义能否涵盖全部需求,这是最关键的一步,决定着下面编码能否顺利进行。数据库定义后,就是网站后台管理的编码实现,也就是对一张张表进行管理(增、删、改)。当后台管理完成时,项目的大部分就大功告成了。用户看到的前台页面仅仅是内容展示——把一张张表中的数据取出来按照最初的需求放置到页面的各个位置。所有的代码都用CVS管理起来。
l 网站测试和开发同步进行。后台管理每完成若干张表的管理,测试人员立即开始测试。这就像流水线,开发完一部分,立刻测试;同样的,网站前台展示开发时也一样需要测试人员跟进。发现的每一个Bug都用BugFree记录下来跟踪处理过程。
l 数据统计跟上。网站后台各个表的任何改动要准确记录,决不允许出现不知道谁修改了数据库内容的情况。其次,网友访问网站的日志要做好统计,每天结束的时候就能准确的看到当天的用户访问数据。这些数据对网站运营极其重要。
四个月后,我的第一个网站项目顺利上线。所有参与该项目的同事感觉都很新鲜,因为以前他们在做网站时,基本上是一个人“包干”一个频道,简单构思一下就开始写程序、边写边想、相互独立。后来,我跟一位曾在某门户网站工作过的高级工程师朋友介绍了上面的做法,他非常认同和赞赏,得到他的认可我也很兴奋。
随后接触到的很多网站技术人员,让我发觉作坊式做法同样存在于互联网公司,网站在重复多年前传统软件的老路:一个“大虾”很厉害,搞定一个频道或一个网站的方方面面,离开他谁都玩不转;代码中处处留着他的灵感,人走了,网站维护就成了大难题:没有文档、没有统一的编码规范、没有测试记录。
其实无论传统软件、网站、还是游戏等等软件产品/项目,都是程序员用一行行代码敲出来的,只要像微软软件研发那样抓住需求、开发、测试这三个环节,其管理都极其类似。因此当我进入http://133.newsky.cn网站项目的时候,信心十足:我能把它管好!
打造一个网站开发的品牌项目
在我加入金环天朗的时候,这个网站就已经存在了,而最开始的计划也只是对原有的网站进行局部改版。但是等我深入了解后,大吃一惊:
u 规划/需求:原有网站没有经过认真规划就匆忙上马,只有部分的简单示意图,对于每个页面具体区域的功能描述和逻辑过程还是依赖口头沟通。没有独立的后台管理,依赖于WAP业务的后台,内容展示力不从心。
u 页面设计:美工因为还有其它工作所以有一定程度的拖延,没有时间观念,整个设计方案没有经过整体评估,导致后来许多细节没有按照计划实现,页面设计先后由两人分头独立完成,导致部分风格不一致。
u 开发:技术实现一直处在救火的状态,没有规划,没有步骤,没有主次之分,没有时间观念。代码的结构非常散乱,没有可用的文档查询,开发人员走了,给以后接手的人带来极大的麻烦。代码没有规范、没有注释。归结起来就是可读性很差。
u 测试:没有任何测试,开发人员简单试一试就直接上线了!
u 内容:网站内容维护没有专人负责,逐渐处于无人答理的状态。
总之,原来的网站有太多不尽人意之处,和同类网站比起来差距较大,市场人员无法推广,技术人员很难维护,动不动就出错。只能另起炉灶,推倒重做一个全新的网站。
对一家SP公司而言,做网站是打通让用户消费的通道。从常远看,内容为王,但短期内通道为王:就是让用户很容易找到公司提供的内容。因为WAP业务非常依赖于运营商的门户排名,一个业务放在运营商WAP门户上,第一屏和第二屏有着本质的不同,愿意翻到第2屏上的用户可能少一半或更多!所以SP要想尽一切办法来摆脱对门户的唯一依赖,必须能用别的通道让用户很方便的找到你的业务。而网站就是最好的宣传通道,是公司产品最重要的展示平台。网站研发的目标就是尽快打通联通、移动用户的消费通道,把公司生产出来的产品(图、铃、文字)方便地展示给更多的手机用户。
这个http://133.newsky.cn网站是面向中国联通用户的,其设计目标是:
? 1~3年内不需要改动大框架
? 公司业务内容的精美展示、销售平台
? 在同行中有很强的竞争力
? 老板可以拿出来给投资人演示
为了达成这个设计目标,我和项目组花了近一个月的时间来制定完整规划。
| 规划 | 需求 | 美工 | 开发 | 测试 | 运营 |
收到老板Email,项目启动 | | | | | | |
完成规划 | 启动前台展示需求的定义 | | | | | |
| 开始后台管理需求定义 | | | | | |
| 完成需求定义。确定后面的时间进度:6/15正式上线运营! | 开始后台管理页面设计 | 开始网站数据库的设计 | | | |
| | | 完成“后台管理详细设计”的文档 | | | |
| | | 开始后台管理的编码 | | | |
| | 开始前台展示页面设计 | | | | |
| | | 完成后台管理的编码 | | | |
| | | | 引入测试组,开始后台管理的测试 | | |
| | | 两名新人到岗,开始前台展示的编码 | | | |
| | | | | 确定运营组成员及分工 | |
| | | 主要编码结束 | | | |
| | | | 测试完毕 | 开始录入内容 | |
| | | | | 内容全部上线 | |
http://133.newsky.cn正式上线运营,向公司全体同事通报! | ||||||
完成Postmortem(项目总结),为下个移动网站项目做准备 |
明确的研发流程应该是一个开发团队的固定资产,从这点上,我建立了一套项目研发流程,并为其提供工具支撑:
? 认识老网站的现状、确定新网站的设计目标;对新网站的总体设计图纸进行反复讨论,确定网站研发的四个总原则(灵活的后台、以专题为网站细胞、丰富的资讯、翔实的内容);明确人员分工、并预告项目执行的几个关键点。
? 在没有公司内部网的情况下,我先搭建两个工具:用于保存各种文档和源代码的TortoiseCVS(客户端)+CVS(服务器端),用于缺陷管理的BugFree。为每个项目搭建一个CVS模块,其中都有四个子目录:Spec(需求文档)、Design(设计文档)、Code(源代码)、Test(测试文档)。
示意图:网站项目的CVS目录
然后是人力资源,我在规划中提出了非常明确的人力资源需求:
? 前台需求定义:1人(蔡志宏)
? 后台需求定义:2人(刘振飞、朱伟波)
? 美工设计制作:1人
? 开发:3人
? 测试:5人
? 运营:5人
然而,当时的情况却是项目组人员迟迟无法到位:美工只有一个兼职的、时间无法保证;只有一个开发组长;没有测试人员;网站运营人员不能确定。针对这样的情况,我的任务还包括了招聘相关人员及时到位。
在整体上完成上述工作以后,时间已经是
网站需求特别难以确定,为了解决这个问题,我将整个需求定义划分为三个主要的部分:
1.网站前台展示的定义
我首先和负责定义需求的蔡志宏确定了需求Spec文档模板,然后他根据首页、二级、三级页面逐个页面、逐个模块的去定义:展示什么内容,大概的模样(最终样式由美工负责)。这样每个页面都被分解成一块块的“部件”,一个“部件”由一份Spec描述,比如下面是“首页公告栏区域需求定义”Spec的示意图。
示意图:首页公告栏区域的需求定义Spec
每完成若干相关联的Spec,我就召集美工、开发人员开会讨论(本应该也叫上测试和运营人员,但当时还没有人),大家站在不同的角度去看看有无问题,并最终确认下来。
2.联通用户消费流程的定义
用户消费流程涉及到收费问题,必须把每个细节都要搞清楚。这个需求由我负责,先形成一份PPT文档,在大范围内征求大家的意见,然后细化每个细节:从用户访问我们的首页开始,如何登录,如何转向联通网站,如何扣费等每个细节必须想到。
3.网站后台管理的定义
根据网站前台的需求,我和开发组长朱伟波来设计数据库定义,确定多少张表、每张表中有什么字段。然后从运营人员的登录页面开始设计,用PPT把每张页面的示意图以及逻辑关系都展示出来,然后把需求、开发、美工召集起来一起讨论,看看是否符合运营人员的习惯、是否有遗漏的地方。
需求文档要想清楚后再写下来,让别人读得懂。定义好的需求Spec是整个项目开发的“合同”,马虎不得。在需求定义的3周中(其中前台展示的需求用了2周、后台管理的需求用了1周),每写出来若干相关的需求文档,就在项目组内讨论一次,最终明确下来。需求文档一旦成型以后,就必须严格按照需求文档编写设计代码,尽量控制需求的变化。这不但要求我们在最开始的需求分析阶段做好最充分的准备工作,而且还需要作为项目经理的我,顶住一些来自各方意见的压力。幸运的,我们团队还是非常好的坚持下来了:
示意图:上线后的首页公告栏区域——完全根据Spec中的需求定义来实现
然而,另外的一个问题是,需求文档很容易“老旧”、跟不上最新的变化,需求定义人也懒得去更新,因为开始编码后谁都不去注意需求文档了。为了解决这个问题,我就在后台管理的每一张表的维护页面上,增加一个“Spec”按钮,点击后就可以看到相关的需求文档Spec列表了。这样做有两个好处:一方面运营人员可以很方便的看到最初是怎么设计这块功能的;另一方面也把需求定义者的工作暴露在全体同事面前,文档写的好坏是一目了然。
示意图:每个后台表管理页面上都有个Spec按钮,指向对应的需求文档列表
充分的需求定义保证了整个项目能够准时完工,这也是我们这个项目能够取得圆满成功的原因之一。需求确定之后,后面开发、测试的时间就基本明确下来了。
有了完整的需求文档后,接下来就进入开发阶段。如同前面提及的,首先需要完成的是数据库的设计。其实早在需求定义期间,我和朱伟波就已经开始数据库定义,确定多少张表、每张表中有什么字段。我们花费了三天左右的时间来对后台数据库进行详细的设计,并产生出设计文档。
示意图:新天地网站2005版后台数据库定义.doc
然而,光有需求和详细设计文档还不够,开发团队需要保持要一种一致的风格,这一点要求所有的程序员对代码有责任感。因此在这个阶段之前(3/16~4/12),我就让公司所有的Java工程师多次讨论,并最后确定一份“编码规范”,这样网站真正开始写代码的时候,就有一个明确的规范来约束代码的书写。
对于软件项目来说,经常会有一些出乎意料的情况发生。比如,本来计划有两个开发人员做后台管理,结果因为沈阳联通的一个合作项目需求紧急配合,只好临时抽调一个人去支援,毕竟网站是公司内部可以控制的,导致后台开发只有朱伟波一个“光杆司令”,那一段他连续十余天加班到晚上11:30!这样高强度、高压力的工作状态,不是每个程序员都能承受的。经过朱伟波的努力,终于在十天时间内将所有的后台编码全部完成(
紧接下来,从
在这个项目之前,整个公司是没有测试人员的!这不得不让我大为惊讶,一个SP公司没有测试怎么行!所以在这个项目进行的同时我启动测试人员招聘工作,最终成立了一支5人组成的测试组,负责所有业务的测试。
当网站后台管理编码完成后,4/28立即启动测试工作:后台管理中的首页管理、动画、声音、彩图、专题、资讯由专人负责测试,发现一个问题就在BugFree中记录一个Bug。通过BugFree的跟踪和记录,可以让某些问题不必累积到最后才解决。随着网站前台展示开发在5月中旬启动,测试工作也在并行跟进:每个频道、每个页面都有专人负责检查,这样尽可能的把各种潜在的问题揪出来,免除后患。
示意图:用BugFree来管理网站项目中的Bug
很遗憾的是,因为测试组搭建的比较晚、测试任务又比较重,他们需要花费很长时间去熟悉公司的各种业务,所以在这个网站项目中,对测试文档部分(比如测试用例)我并没有要求,只要把问题发现出来上Bug就好了。这就是项目管理中的Trade-Off:抓住主要矛盾、抓大放小。这个项目结束后,测试组已经逐步成熟、磨合好了,我才开始强调测试文档的重要性,每个业务测试时一定要同步完成相关的测试文档(计划、用例、测试结果等),测试时就按照相关的测试文档进行。这样以后复测就能省掉很多时间,换个人测试也很方便上手。
经过一个多月的努力,测试组的同事基本上完成了网站所有频道、页面的检查工作(
研发人员做出来的网站只是一个空空的框架,没有实际的内容填上去,网站就无法上线——打个比方,研发人员把“大楼”盖好了,还需要运营人员把“内部装修”做好。然而面对人员的稀缺和内部调整,一直到
在此期间,整个项目组都进入了最后的冲刺阶段。为了确保
示意图:项目最后突击的日志
值得庆祝的日子到来了。
ü 做出来的网站符合最初的规划和需求定义;
ü 按照需求定义完成的时候(
ü 整个项目执行过程中,规划、需求、开发、测试等环节均按照预定轨道前进,没有出现大的纰漏。
整个项目组成员在网站上线后都非常兴奋,这应该是公司到目前最成功的一个项目管理实践。公司领导对这个项目的研发表示非常满意。现在的情况是,休整2周后,
网站和产品开发没有什么不同!
按我整理的时间表和项目计划,对照微软的流程,你会发现,我完全是按照微软“传统软件”的研发流程去管理这个网站项目,略有不同的地方是,这个网站项目的时间跨度比较小(只有4个月),而且人力资源有限,美工、开发、测试三个环节我只能是并行处理、流水作业,以尽量缩短项目的整体时间。
| 规划和需求阶段 | 开发阶段 | 测试阶段 | 发布阶段 |
主参与人 | Planner与PM驱动 | 开发人员推动 | 测试人员推动 | PM,产品经理,运营管理等执行 |
阶段成果 | 目标描述 (Vision) 详细需求文档 (Spec) 日程进度表 | M1, M2, … Code Complete | 集成测试 Bug-Fix, Check-in Dogfood Beta1, beta2, … (Triage) Zero Bug Release | Show-Stopper bug Release Candidate(RC) Sign-off RTM (Ready To Release) |
我也算是“把微软先进的软件研发理念和中国中小企业的具体情况相结合”吧,其中最难的是把项目研发流程的理念灌输给全组同事以统一认识,并能有效的执行下去。很多时候要靠我不断的去PUSH各个环节,做的比较累,但在完成之后,很有成就感,尤其是针对一个团队不断发展和成熟,所做的努力是显而易见的。(未完待续)
1.软件工程三要素的价值
思考问题的方法可以是由点及面的,也可以是统揽全局的。换成业界最常用的词汇,就是“自上而下”还是“自下而上”的区别。
“牛屎图”中描述的工具、方法与过程也被称为软件工程的“三要素”。在本书中他们被分解开来思考——并不是要孤立这三个层面,它们实际上是相互作用的。例如“过程”问题,既有实施过程的工具,也有相关的过程方法理论。虽然说方法是“基于一种数据结构的编程实践的结果”,但这是一种非常狭义的定义。这个定义在过程的开发环节是有效的(或者说对“开发方法”的定义),然而“需求”、“设计”、“测试”等其它环节也有各自的方法论。即使站在具体环节之外,过程本身也有方法论的问题,这还不包括管理方法等等在内。
由于方法在过程环节以及过程总体层面上具有贯通性,因此保证“方法(或其行为)”实施的“工具”也会出现在过程的各个环节和层面上。这样得到的软件工程模型将不是经典的、层状的“牛屎图”,而可能像太极图一样由阴阳交汇而生万物。为了不使读者认为我已经入了道家理论的歧途,这样的一副图还是交由你们自己去画吧。只不过应该清楚一点,即使画出了太极图的软件工程模型,所见到的仍旧是工程的细部环节,就如同以管窥豹一般——斑是斑,豹是豹。
把每一个“管见”拼合起来,得到的才能是“豹”,而不是“斑”。所以尽管本书割裂了软件工程的各个要素,并从每个孤立的层面来审视。然而实质上,应该回归到软件工程的本体上来思考问题,而不是仅关注于每一个局部的要素。
工程的整体问题仍旧是“实现”。
2.RUP就是“杂物箱”
我也许总是在批评RUP,但是不得不承认它是对前人在软件过程思想方面的高度包容。请注意我用“包容”这个词,而不是按照语言习惯那样用“概括”。因为如果是“高度概括”,那就应该把目光投向瀑布模型,而RUP其实就像一个杂物箱一样“包容”了全部的已知理论。
可以把RUP定制成其它任何模型所表述的过程形态——RUP本身的特质决定了这一点——因而它也如同一个杂物箱一样放满了各种希奇古怪的东西:你可能从这个杂物箱里面拿出了一把剪刀,或一只苍蝇拍,或者是一根钓杆……
面对“软件开发”这样的需求,钓杆能有什么作用呢?在你扔掉它之前,请转换一下思维:钓杆可能带给你的团队以精神上的激励。如果你能意识到这一点,那么它将立即转化为生产力——请把钓杆挂在开发部的墙上。
RUP能不能被用起来,将取决于你刚才那个挑挑捡捡的行为,以及在你拿到“钓杆”后的辨识能力与组织能力。
3.UML与甲骨文之间的异同
在你真的打算用“甲骨文”来写项目文档之前,请先弄明白UML与甲骨文之间的异同。在本书里,它们都被做为沟通的工具。因此,目标是沟通,而不是“选用工具”。更进一步的推论是:即使你因为个人喜好而选择了甲骨文,也不要试图在结绳记事的原始人面前去用它。UML与甲骨文都是符号文字,都具有像形含义。然而,这并不表明UML符号本身能表达多么丰富的含义。如果要像甲骨文一样用几代人、上千册的论著去解释它,那么UML图的价值也就只剩下象征性意义了。
出于沟通的必要,UML语言的象征意义在一个图中应当被表述得足够准确和详细,以致对于不同的阅读者来说都提供了充足的信息。然而,一方面UML的规范中没有提供一个标准来衡量“怎样的UML图是描述充分的”;另一方面,UML作为一个语言,也无法直接在某个硬件平台中被语法检错和调试。所以在工程中使用UML图,应该有相应的文字来描述它。而且,这种描述与图之间的对应关系要持续地维护下去。如果这种关系松散了、断裂了,那么下一个阅读UML图的人所面对的将是无异于甲骨文出土时的困境。好在做UML图的那个工程设计人员(在辞世之前)还有机会为这些古怪符号写下规约。
4.经营者离开发者很远,反之亦然
使我第一次意识到EHM模型反应了角色所关注的不同视角的人,是我的老板。
事实上,他是一个完全不懂软件技术的老板。在EHM模型中,他所处于的位置在最右端,而开发者在最左端,在二者之间没有相同的关注界面(关注点)。EHM真实地反应了“老板不懂技术”的合理性,同样也真实地反应了“开发者转型为老板”的道路将是相当地漫长与艰难。
于是,担任中间角色的项目经理就有了一种使命:协调经营者与开发者之间的沟通。例如招来一名开发高手,对于公司的运作并不会有深入的影响(当然,如果你招来了Anders Hejlsberg就另当别论)。因此,我甚至不需要与BOSS讨论这名高手的来历及作用。同样,与一个技术分析人员讨论一个产品的技术价值、市场价值之间的差异,以及市场运作方式与技术实现手段的无关性是毫无必要的。
你要理解这种根源:角色的关注层面完全不同。
5.矛盾:实现目标与保障质量
在需求阶段我们会面临“目标”的问题,然而在大多数时候,与此相反的是我们会在项目交付和试用时才会碰到客户在质量上的投诉。
需求人员会把所有的责任归咎到开发人员,而开发人员又不停地埋怨需求的不清不楚或者变更的没完没了。如果正巧需求和开发都是由同一个人或者同一小组来做的,那么他们便会开始埋怨客户的苛刻以及工期的紧张。
总之一件事,没有人会跳出来说:我们原本就错了。然而,事实上根本问题可能是:我们把目标定错了。
可以看到,在项目的平衡三角(时间、资源和功能)中讨论的是目标问题,但并不讨论质量问题。也就是说,经典教材中总是关注如何更快的完成项目,并减少资源占用,以及实现更多的功能。但是,即使平衡了这种关系,项目的结果仍可能产生一个天生的残障。因为目标可能在平衡中确立,但质量却要在过程中控制。即使在时间、资源和功能三者中取得了平衡,并且客户、项目组和公司同样满意于这个平衡“目标”,但它仍然有可能是“不能实施”的。
如果原定的目标本身就过大,那么无论如何平衡这三者之间的关系,其结果仍旧是保障不了质量。
问题是:又有谁愿意在最初签订协议的时候,就降低或者放弃协议标准呢?
6.枝节与细节
前面说到目标和质量的问题时,提及“平衡时间、资源和功能三者的关系”。这其实是一个实施过程中的细节。或者说,它是一个具体的方法,而不是目的。
所以我们通常所说的细节,其实是对实施方法的一些有限量的描绘。比如“软件工艺”概念本身的提出,就是考究“细节问题”的。从这个角度上来说,我并不反对“细节决定成败”的观点。但请注意一个前提:这是技术或方法的细部。
我在前文中一再地混用了“细节”与“枝节”这两个词。枝节是事实发展的次要的分枝,它不涉及行为本身,也不是对行为本身的考量。因此我在前面的文字中说到“跳出细节”,本意是“跳出枝节”——细节只有做到何种程度的问题,而并不是关不关注(或做不做)的问题。
大多数情况下,管理人员有责任去审核、评估其它成员的工作成果。这个时候可以讨论“细节决定成败”类似的问题,因为这决定了产品的最终质量,而质量是工程的目标之一。
而在另一些情况下,例如管理人员做事件决策的时候,就必须要学会忽略枝节问题。
混淆这两个名词的使用,其根本原因在于一大部分读者并不能区分“细节”与“枝节”。从惯于“实做”的程序员一路走来的工程人员,很难分清自己什么时候是在“工作”,而什么时候是在“决策”。
因此我只好用最笨的方法提示管理者:别管它是细节还是枝节,只要你感到你的脚趾已经沾上了泥淖,就快点回头。
用脚趾去感觉,有时比用头脑去思维来得有效。
7.灵活的软件工程
并不像现代人想象的那样,古诗词一定是“逐字论平仄”的。变化或者变通,其实是常见之事。因此古词谱中,才常会见到冠以“摊破”、“减字”、“添字”等字的词格。然而古人在词格上的这种变通,是基于“音律”的。通常说的词律是指词格,这与音律是两回事。词律(格)是平仄,音律则是乐器、音调与歌舞。古词中用来吟唱与歌舞的词牌就不能混用,律不同,调不同,如是之。然而古词的音律(亦即是律谱)已经失传了,也就是说,今天的词是用来读的,不是唱,也不是舞,甚至连吟哦也不是。所以今人总是拿普通话中的一、二声作为平声,三、四声为仄声来填词,并以此论平仄,而全然不想词的格律的根基是“词律”与“音律”这两个部分的融合。
我曾经参与过一个讨论,叫“古人是如何说话的”。在我看来,古人做文章和说话是两回事,文章中之乎者也,日常交流中还是市井俚语的。因此评论中会说“以俚语入词”。也可见填词做文章与说话毕竟是不同。再者,说话也存在方言的问题,因此方言之间平仄音调也不尽相同。古代的歌妓是要求会“官话”的,这相当于现在“普通话”的地位,她们歌唱起来,也是用的“官话”。
更进一步的推论是:古代的词律中的平仄是以官话为基础的。然而如今的普通话毕竟不是古时的“官话”。也就是说,即使我们以普通话的四声为基础讨论平仄,在古人看来,也是可笑的,这样做出来的词依旧不可唱,也不可读。因此今人做词的标准是应该重定的了,除了词格(这里仅指字句的格式)和用韵之外,其它的部分是无法遵循的了。在各自的平仄以及句式上,应当以“能通顺”和“能品味”为准,风格上则以古雅为益。
仅此而已。
对于我这样的格律观点,一位网友曾有一句“未蕴而变,自欺也;知律而变,智者之道也”,实为良言。变向不变求。不变者,万变之所源,亦万变之所归。习诗词之法度,若蚕虫之结茧,若无结茧于前,何有破茧于后?故,知律而变,智者之道也。
“知律而变”中的“律”字,若解释为“规律”,便是可以用于软件工程中了。“道”是规律,如果明“道”,而可以变化无穷,这样做软件工程才是活的。就如同今人难于填词一样,不明道,则不明智,不明智则无所以为,因而在软件工程实施中不可避免地盲目与停滞。
“知律”的另一层意思,是在于“知道原理”。明白“为什么要这样”或者“为什么不是那样”。这在软件开发中是常见的问题,大多数人不知究竟地使用着技巧和方法,而一旦出了问题,则归咎于这些技巧和方法的不好。而真正的问题在于,这些人(我们通常叫做Copy&Paster)并不知道这些技巧、技术和方法的原理,因而不知道变通,也不知道回避错误。
死读一本《软件工程》的人不会做真正的软件工程,所以我写了本书,聊做软件工程实践者的思想之著。