2001年,为了解决许多公司的软件团队陷入不断增长的过程泥潭,一批业界专家一起概括出了一些可以让软件开发团队具有快速工作、响应变化能力的价值观和原则,他们称自己为敏捷联盟。敏捷开发过程的方法很多,主要有:SCRUM,Crystal,特征驱动软件开发(Feature Driven Development,简称FDD),自适应软件开发(Adaptive Software Development,简称ASD),以及最重要的极限编程(eXtreme Programming,简称XP)。极限编程(XP)是于1998年由Smalltalk社群中的大师级人物Kent Beck首先倡导的。
极限编程(XP)是敏捷方法中最箸名的一个。它是由一系列简单却互相依赖的实践组成。这些实践结合在一起形成了一个胜于部分结合的整体。
下面是极限编程的有效实践:
1、完整团队
XP项目的所有参与者(开发人员、客户、测试人员等)一起工作在一个开放的场所中,他们是同一个团队的成员。这个场所的墙壁上随意悬挂着大幅的、显著的图表以及其他一些显示他们进度的东西。
2、计划游戏
计划是持续的、循序渐进的。每2周,开发人员就为下2周估算候选特性的成本,而客户则根据成本和商务价值来选择要实现的特性。
3、客户测试
作为选择每个所期望的特性的一部分,客户可以根据脚本语言来定义出自动验收测试来表明该特性可以工作。
4、简单设计
团队保持设计恰好和当前的系统功能相匹配。它通过了所有的测试,不包含任何重复,表达出了编写者想表达的所有东西,并且包含尽可能少的代码。
5、结对编程
所有的产品软件都是由两个程序员、并排坐在一起在同一台机器上构建的。
6、测试驱动开发
编写单元测试是一个验证行为,更是一个设计行为。同样,它更是一种编写文档的行为。编写单元测试避免了相当数量的反馈循环,尤其是功功能能验证方面的反馈循环。程序员以非常短的循环周期工作,他们先增加一个失败的测试,然后使之通过。
7、改进设计
随时利用重构方法改进已经腐化的代码,保持代码尽可能的干净、具有表达力。
8、持续集成
团队总是使系统完整地被集成。一个人拆入(Check in)后,其它所有人责任代码集成。
9、集体代码所有权
任何结对的程序员都可以在任何时候改进任何代码。没有程序员对任何一个特定的模块或技术单独负责,每个人都可以参与任何其它方面的开发。
10、编码标准
系统中所有的代码看起来就好像是被单独一人编写的。
11、隐喻
将整个系统联系在一起的全局视图;它是系统的未来影像,是它使得所有单独模块的位置和外观变得明显直观。如果模块的外观与整个隐喻不符,那么你就知道该模块是错误的。
12、可持续的速度
团队只有持久才有获胜的希望。他们以能够长期维持的速度努力工作,他们保存精力,他们把项目看作是马拉松长跑,而不是全速短跑。
极限编程是一组简单、具体的实践,这些实践结合在形成了一个敏捷开发过程。极限编程是一种优良的、通用的软件开发方法,项目团队可以拿来直接采用,也可以增加一些实践,或者对其中的一些实践进行修改后再采用。
敏捷软件开发宣言:
n 个体和交互 胜过 过程和工具
n 可以工作的软件 胜过 面面俱到的文档
n 客户合作 胜过 合同谈判
n 响应变化 胜过 遵循计划
虽然右项也有价值,但是我们认为左项具有更大的价值。
敏捷宣言遵循的原则:
n 我们最优先要做的是通过尽早的、持续的交付有价值的软件来使客户满意。
n 即使到了开发的后期,也欢迎改变需求。敏捷过程利用变化来为客户创造竞争优势。
n 经常性地交付可以工作的软件,交付的间隔可以从几个星期到几个月,交付的时间间隔越短越好。
n 在整个项目开发期间,业务人员和开发人员必须天天都在一起工作。
n 围绕被激励起来的个体来构建项目。给他们提供所需的环境和支持,并且信任他们能够完成工作。
n 在团队内部,最具有效果并富有效率的传递信息的方法,就是面对面的交谈。
n 工作的软件是首要的进度度量标准。
n 敏捷过程提倡可持续的开发速度。责任人、开发者和用户应该能够保持一个长期的、恒定的开发速度。
n 不断地关注优秀的技能和好的设计会增强敏捷能力。
n 简单是最根本的。
n 最好的构架、需求和设计出于自组织团队。
n 每隔一定时间,团队会在如何才能更有效地工作方面进行反省,然后相应地对自己的行为进行调整。
当软件开发需求的变化而变化时,软件设计会出现坏味道,当软件中出现下面任何一种气味时,表明软件正在腐化。
n 僵化性: 很难对系统进行改动,因为每个改动都会迫使许多对系统其他部分的其它改动。
n 脆弱性: 对系统的改动会导致系统中和改动的地方在概念上无关的许多地方出现问题。
n 牢固性: 很难解开系统的纠结,使之成为一些可在其他系统中重用的组件。
n 粘滞性: 做正确的事情比做错误的事情要困难。
n 不必要的复杂性: 设计中包含有不具任何直接好处的基础结构。
n 不必要的重复性: 设计中包含有重复的结构,而该重复的结构本可以使用单一的抽象进行统一。
n 晦涩性: 很难阅读、理解。没有很好地表现出意图。
敏捷团队依靠变化来获取活力。团队几乎不进行预先设计,因此,不需要一个成熟的初始设计。他们更愿意保持设计尽可能的干净、简单,并使用许多单元测试和验收测试作为支援。这保持了设计的灵活性、易于理解性。团队利用这种灵活性,持续地改进设计,以便于每次迭代结束生成的系统都具有最适合于那次迭代中需求的设计。
为了改变上面软件设计中的腐化味,敏捷开发采取了以下面向对象的设计原则来加以避免,这些原则如下:
n 单一职责原则(SRP)
就一个类而言,应该仅有一个引起它变化的原因。
n 开放-封闭原则(OCP)
软件实体应该是可以扩展的,但是不可修改。
n Liskov替换原则(LSP)
子类型必须能够替换掉它们的基类型。
n 依赖倒置原则(DIP)
抽象不应该依赖于细节。细节应该依赖于抽象。
n 接口隔离原则(ISP)
不应该强迫客户依赖于它们不用的方法。接口属于客户,不属于它所在的类层次结构。
n 重用发布等价原则(REP)
重用的粒度就是发布的粒度。
n 共同封闭原则(CCP)
包中的所有类对于同一类性质的变化应该是共同封闭的。一个变化若对一个包产生影响,则将对该包中的所有类产生影响,而对于其他的包不造成任何影响。
n 共同重用原则(CRP)
一个包中的所有类应该是共同重用的。如果重用了包中的一个类,那么就要重用包中的所有类。
n 无环依赖原则(ADP)
在包的依赖关系图中不允许存在环。
n 稳定依赖原则(SDP)
朝着稳定的方向进行依赖。
n 稳定抽象原则(SAP)
包的抽象程度应该和其稳定程度一致。
上述中的包的概念是:包可以用作包容一组类的容器,通过把类组织成包,我们可以在更高层次的抽象上来理解设计,我们也可以通过包来管理软件的开发和发布。目的就是根据一些原则对应用程序中的类进行划分,然后把那些划分后的类分配到包中。
XP活用原则之一
(一)发挥过程和人的力量
XP作为敏捷方法的一种,拥有很多优秀的实践,用好这些实践,在软件组织中能够起到很好的效果。问题在于,要用好这些实践并不简单,本系列文章的目标就是围绕着 XP 的实践,讨论隐藏在实践内部的敏捷性实质,研究如何灵活的应用 XP 的实践,从而达到改进软件过程的目的。
软件开发虽然有多个环节,但是我们不能只强调某些环节,任何一个环节出问题最终都会影响产品的质量。因此我们在软件开发中应该考虑整个过程,并且重视人这个因素。
质检员的工作
在以前的工厂作业流程中,产品在生产出来之后,都需要经过质检员的检查。在质检员身边,有两个筐,一个筐上写着合格,一个筐上写着不合格。对于合格的产品,贴上合格证,进入销售的环节,对于不合格的产品,禁止出厂。很久以来,我们一直采用在产品的最终阶段进行质量检验的方式,这种方式用来避免有质量缺陷的产品出厂,但是既没有办法提高产品的质量,也没有办法降低差错率。这种质检方法的基本思想是,产品出现废品是正常的,只要能够找出废品,产品的质量就不受影响。
那我们看看软件开发的工艺流程。当软件经历了需求、分析、设计、编码之后,质检员同样需要检验软件是否满足质量要求。各种各样的测试人员共同担任了质检员的角色。而质检员的工序也不简单。黑盒测试、白盒测试、集成测试、用户测试、并行测试。和工厂不同的是,出现问题的软件并不能够简单的扔到不合格的产品堆中。另一个不同,软件产品一定会出现质量的问题。既然不能简单的抛弃产品,那么只好把产品退回到生产线上。于是,需求人员、分析人员、设计人员、编码人员开始对软件进行调整,力图使软件能够出厂。这个反复的过程往往要持续上一段时间。幸运的软件可以顺利出厂(交付),否则,可能会遭到项目失败的命运。
很明显,我们发现这种做法不够聪明。把问题堆积起来,直到最后才来集中解决,这种做法的代价是非常高昂的。软件开发的特性,决定了越是后期的变更,成本越高。那么,我们应该如何调整我们的做法,来降低成本,提高质量呢?
精益原则
软件开发总是从其它学科中借鉴管理思路。最早的软件工程从土木工程中借鉴经验,但是后来人们发现建筑和软件开发有很大的差异性。故而新的软件开发方式开始兴起,其中就包括了XP方法论。同样的,新的软件开发方式仍然在理论上寻找立足点,这一次众人的焦点落在了现代管理理念上。土木工程管理的一个很大的问题就在于忽视了人的作用,而现代的管理理念将人的作用提到了一个新的高度,这和新兴的软件开发思想是相同的。而对软件开发思路影响最大的,应该算是丰田公司提出的精益生产(Lean Production)的概念。
二战后的美国,以福特公司为首的汽车制造公司在大肆提倡规模制造(Mass Prodution)的同时,东方的日本,丰田英二等人在考察了美国的制造思路之后,认为美国的制造方式不适合日本,提出了自己的精益制造(Lean Production)的思路,精益制造成就了一代霸主-丰田公司,丰田的制造方式被人称为TPS(Toyota Production System)。丰田公司的丰田英二和大野耐一等人进行了一系列的探索和实验,根据日本的国情,提出了一系列改进生产的方法:及时制生产、全面质量管理、并行工程,逐步创立了独特的多品种、小批量、高质量、低消耗的生产方式。这些方法经过30多年的实践,形成了完整的"丰田生产方式",帮助汽车工业的后来者日本超过了汽车强国美国,产量达到1300万辆,占到世界汽车总量的30%以上。
回顾这段历史,和软件开发的历史何其相似。大规模制造理论认为,一定程度的浪费,一定程度的废品是正常的,允许的。而在软件开发中,浪费、成本居高不下也同样成为阻止软件开发迈向工程化的一大障碍。像XP这样的敏捷方法从精益制造的思路中吸取了很多的优秀思想,例如,不断改进质量,设计决策应该交给最贴近生产的人员,根据客户的需求来推动生产。虽然我们一直在强调软件开发和制造行业截然不同,但是,处于变革的十字路口的软件开发行业,总是不断的从其它的行业中寻找可借鉴的理论。这种借鉴来的思路就被称为精益编程(Lean Programming)。精益编程的思路包括:
消除浪费。 任何不能够为最终的产品增加用户认可的价值的东西都是浪费。无用的需求是浪费,无用的设计是浪费,超出了功能范围,不能够被马上利用的代码也是浪费,工件在不同的开发组之间无意义的流转,也是浪费。
强化学习,鼓励改进。软件开发是一个不断发现问题,不断解决问题的过程。而学习能力的强化,能够令软件开发工作不断的获得改进。
延迟决策。软件开发如果工作在一个不确定的环境中,变化性会对软件开发本身造成伤害。延迟决策,当环境变得逐渐清晰之后,才有充足的理由来进行决策。而对软件设计而言,如何构建一个可支持变化的系统则成为关键的问题。
尽快交付。自从互联网流行之后,速度成为了商业中的至关重要的因素,从而直接影响了快速软件开发的成熟。软件阶段性交付的周期越快,软件的风险就越容易识别,用户的需求就越清洗,软件的质量就越高。
谁做决策。谁做决策?是高高在上的高级经理,还是贴近代码的编码人员。决策取决于准确的信息,但是掌握这些信息的权威者往往就是做实际工作的编码人员,将编码人员的建议、决定和实践纳入到决策的范畴来,是成功决策的关键。
精益编程代表了一种思想,很多的Agile方法都从各自的理论基础出发,支持了这种思想。而在我们的讨论中,讨论的重点就是放在XP上。XP方法论中最有价值的是他的思想。我们研究、学习XP,不能够光了解他的实践活动,还需要时刻注意XP的价值观,并仔细的思考,在实践活动的背后,到底隐藏着什么样的思想。就好像我们在阅读设计模式一书的时候,书中给出的是各种各样的关于面向对象的设计方法,但是书中仍然有一条主线贯穿其中,那就是面向对象的编程原则。
过程
前一段时间,书店中很畅销的书大多数都和6σ相关。6σ是全面质量管理理论的发展。其中一个很重要,和软件开发非常类似的思路是,过程的每一个步骤,都会对产品最后的质量产生影响,要提高质量,降低成本,提升客户的满意度,最关键的是要对过程进行研究和分析,发现对产品影响较大的步骤,并制定改进的措施。
一家专门提供外卖的公司,常常被客户投诉说送货的时间太慢了。于是他们加强了送货的力量,包括使用更好的工具,雇佣更多的送货人员。但是成本增加了,客户的投诉依然不断。问题出在了哪里?在对整个流程进行了量化评估之后,他们发现,送货的时间对整个的时间影响很小,而更多的时间是花费在了制作外卖的过程中。所以,原先投入对送货流程改进的投资,算是白费了。
做任何一件事情,都需要经历一个过程。从外卖店接到客户的订货电话开始,一个过程就已经启动了。记录客户的地址、地址特征、菜名,给厨房下单,分配外送人员,将地址信息传递给外送人员,送货,寻找目的地,交付外卖并收款,后续过程忽略。一个似乎平常的生活活动,其背后都包含了复杂的过程。对软件开发而言也是一样的。从客户提出软件的构想,一直到客户真正开始使用软件,其间的过程非常的复杂,充满了各种各样的不可预测的因素。
送外卖的过程,每一步都会对最终的质量(客户满意度)产生影响。对客户来说,从打电话到收到外卖的时间,外卖的好吃程度,这些都是属于满意度的组成成分。接到电话后,可能客户的口音较重,记录员听错了地址,导致后续过程全部白费,除非客户等的不耐烦,打电话来重新记录一次地址。下单给厨房之后,可能厨房的电风扇会将单子吹到了地上,客户的要求就被忽略了。记录员把客户的地址描述信息写的很潦草,送货人员可能看不懂,这时候他需要打电话回来问,这就担搁了送货时间。送货人员可能对客户所在不熟悉,找到地址花费了很多的时间。好不容易送到了客户手上,客户已经等的不耐烦了,更糟的是,由于时间太长,外卖已经凉了。客户决定,下一次更换一家外卖店。虽然每一个环节出错的概率都不是很大,但是各个环节组合起来之后,出错的概率就大的惊人。在分析了这样一个流程之后,我们的感慨往往是,居然能够送到,真是不容易!软件开发的过程难道不是这样吗?每一个环节都有可能出问题,需求未必就代表了客户的需要,设计不能够很好的代表需求,反而对编码增加了一些不稳定的因素,由于进度较紧,编码的工作也比较马虎。这样的过程,我们能够开发出客户满意的软件,那么只有一个解释,以前客户接触的软件开发人员,比我们还要烂。
好吧,我们如何改善这一情况呢?对了,对过程进行改进。既然记录员可能会和客户之间出现错配的情况,那我们就要求记录员在听完客户的要求之后,重复一遍。既然,菜单可能会遗失,我们就在厨房中专门设计一个位置,按先进先出的顺序排列这些菜单,而且保证菜单不会被遗失。既然送货员可能会看不懂记录员的字,那么就让送货员和记录员花费一些时间沟通,要么送货员能够习惯记录员的字,要么记录员写出送货员能够理解的字。既然送货员可能未必认识路,那么就对送货员划片,有专门送A区的,有专门送B区的,每个人熟悉的范围减小了,熟悉的程度自然就上升了。好吧,有了这样的思想,我们也准备对软件过程进行改进了。不过,并不是现在,本文的剩余部分将会围绕着这一点来进行。
过程中的人
除了过程的重要性,我们还需要考虑人的因素,过程是要依靠人去推动的,没有人,过程就没有任何意义。对软件开发更是如此,开发过程的每一个环节都需要人的参与。从来没有一个方法论象XP这样充分的强调人的作用。因此,在XP的全过程中,人的因素是始终处于首位的。而XP的实践也是根据人的优点和弱点进行精心的设计。我们这里做一些简单的讨论:
计划游戏:我们常常挂在嘴边的一句话是计划赶不上变化计划,往往都是很多软件组织的一块心病。所有人都知道计划的重要性,可是计划又是最难做的。没有计划,软件过程无从遵循;有了计划,软件过程又常常偏离计划。在变化越来越频繁的现在,计划更是难上加难。对待捉摸不定的计划,XP的态度是:与其在一开始就费时耗力地制定一堆不切实际的计划,倒不如花费少量的精力做一个简单的计划,然后在随后的软件过程中不断的进行调整。
这就好像我们骑自行车,设定一个500米外的目标,然后我们把车把固定住,选取好起点,并预先制定好角度和标准路线,然后骑着车子,严格的按照原定路线前进。车子能到终点吗?可能性不大,设定好的标准路线上可能会有障碍物,这是你原先没有想到的;由于无法调节方向来保持平衡,车子可能会摔倒。
车子,应该这样骑。看看远处的目标,估算距离和时间,得出一个粗糙的速度,然后我们就上路了。在前进的过程中,我们不断的目测目标,察看时间,并调整速度和方向。目标越来越接近,我们的调整越来越熟练,最后,我们成功的抵达的目标点。
传统的计划方法和第一种骑车方法一样不切实际,花费大量的时间思考几个月后发生的事情是很难的。只有根据变化不断的调整,才是正确的态度。 注意,不把时间花费在计划上,并不等于不重视计划。计划仍然是软件开发的核心。你必须有一个当前的迭代计划,当前的周计划,甚至当前的计划。只有保证每一个小计划的严谨性,才能够保证整个项目计划的成功。
XP对计划的态度是:你不需要把计划做的多么精密,但是你必须去做计划。计划赶不上变化,这句话说的一点都没错,我们不需要逃避变化,花大力气进行精确的计划既浪费,又没有意义。但是这并不是说不做计划。凡事预则立,我们需要简单明了的计划,然后在软件开发的过程中,不断的修正并完善计划。 学习变化:XP最适合那些需求容易发生变化的过程,在现实中,我们发现这种情况实在是太多了。可能软件的目标市场发生了变化,要求软件同步变化;可能客户对需求没有充分的了解,导致需求的变化;可能客户的组织或业务流程发生了改变,要求软件变化。种种的可能性表示,在一个一成不变的环境下开发软件已经称为一种奢望。在这样一个残酷的环境中,我们不得不去学习变化。
变化包括两个方面:软件过程如何适应变化,以及软件设计如何适应变化。传统的软件过程往往要求上游的软件阶段确定之后,才能够进行下一个软件阶段。但是变化的需要要求软件过程在多个软件阶段之间切换。 由于变化的残酷性,XP建议开发人员必须建立变化的意识。你必须去改变你的心态,接受变化,变化是合理的,一成不变的东西压根就不存在。
这里插一句题外话。强烈建议在XP的项目中使用面向对象技术。虽然面向对象并没有对软件过程或是软件管理提出任何的要求。但是,一个使用面向对象的团队(注意,是使用面向对象技术,而不是使用面向对象语言,这么说是因为有着大量的开发人员使用面向对象的编程语言编码面向过程式的代码),其管理过程也会随之变化。用户参与、迭代开发、重用、使用框架。这些都是在使用了面向对象技术之后自然而然出现的变化。使用面向对象技术,能够和XP方法进行更加紧密的衔接。
除了上面讨论的两个简单的思路,本文的其它部分都会针对XP中过程和人两方面的因素进行讨论。
本文的定位
本文不是一篇介绍XP基本知识的文章,这方面的资料已经很多了,要想全面的了解XP,人民邮电的一套XP系列丛书是非常好的一个开始。而本书的定位是讨论在实际的软件开发中,如何灵活的应用XP,如何遵循XP的思想,但又根据实际情况进行折衷。虽然本文没有介绍任何的XP基础知识,但是仍然适合XP的初学者阅读,刚接触XP的人往往都有各种各样的困惑,而从国外翻译过来的注解却未必适合国内的环境,因此阅读本文能够从实践的角度更深的理解XP的思想。
和其它的方法论一样,XP不是万能的。一个软件组织能否从XP中获益,不是取决于XP,而是取决于这个软件组织自身。正如我们在一开始就强调的,学习XP,关键在于学习思想。软件组织应该根据自身的情况,活学、活用XP,而不是人云亦云。XP可不是制作一堆卡片。切记,切记。
文章没有全面的介绍XP的所有实践。因为作者并不是XP的绝对拥护者,我们以一种客观的态度审视XP,我们介绍的内容,是在采用了XP的实践或是吸收了XP实践中的思想之后的经验;我们没有介绍的部分,是因为环境原因无法实践或是不对其表示赞同(但并不是不赞同)。 其实本文介绍的很多知识并不是XP的专利,其它的敏捷方法也都提到了这些优点,例如自适应软件方法。所以,更准确的描述是本文如何从XP中学习先进的软件开发理念。
(二)考核和评估之别
螺旋、迭代、增量,不同的名词代表了同样的含义-分阶段开发软件。众多的方法学都采用了这种思路设计软件过程。但是在实践中,更多时候,分阶段开发软件带来的是痛苦。看来,我们常常被书中优美的叙述所迷惑,却没有真正想过实施中的难题。那么,如何管理分阶段的软件开发呢?如何应对现实中的难题呢?
考核和评估之别
在绩效管理中,有两个名词:考核和评估,分别表示了绩效考核和绩效评估两种绩效管理方式。这两者有什么区别呢?
我们说考核是一种制度,而评估是一个过程。怎么理解呢?很多的公司都有绩效考核的制度,这个制度一般是在年底的时候,对员工今年的工作做一个评定。考核是一个点。但是评估不一样,评估是针对某一段时间中员工工作中的不足之处,需要改进之处进行评价。不论是考核还是评估,它们两者虽然都是为了达到评价并改进员工行为的目的而设计的,但是做法是不同的。考核针对过去的事情进行评定,容易实现,但是效果不佳,因为时间一长,大家可能忘记了以前的事情,而要公平的对过去一年的表现做一个评定也不是一件容易的事,评估则不同,评估是不断进行的,针对刚刚发生的事情做出评价,并找到改进方法。就好像我们在第一章中举的外卖店的例子,不断地对过程进行分析和改进,这就是一种评估。评估的效果不错,但难以实现。
软件开发中的考核和评估
这一思路在软件过程中,直接表现为里程碑和迭代的思路。我们可以想想,里程碑是不是一种制度。在需求结束的时候,我们需要需求规约文档,风险列表等等一系列的文档,在设计结束的时候,我们也需要另一些文档。这种处理方式就是考核的思路。但是很多时候,这种考核起到的作用是有局限性的:
工件的设计原本是为了辅助生成最终的代码,但是往往会演变成为了通过里程碑而设计;
里程碑的设计不能够完全捕获所有的问题,部分的风险被隐藏了;
难以对工作量和工作价值进行评估;
里程碑揭露问题的时间要远远落后于问题出现的时间;
这里对里程碑的方式做一些分析。我们对问题的理解往往是逐步深入的。在项目一开始的时候,业务和技术上都存在问题,存在不确定性和风险,这时候往往是最需要评估和验证的。但是里程碑方式往往要求必须深入的分析需求,很多的问题并没有得以解决,而是被悄悄的有意或无意的掩盖了。需求毕竟不是软件,它是一个不同人具有不同理解的模型,这时候,项目中各个角色对它的理解都不相同,但是这并不影响他们做出一致的决定-通过需求里程碑。问题到了设计阶段依然存在,这时候需求阶段隐藏的一些问题开始出现,导致我们不得不补充一些工作量。但是所有的问题也没有得到解决,依然存在未知的风险。那么风险到了什么时候才会暴露出来呢?最乐观的情况是在编码时期发现,最悲观的情况是在交付期发现。我们把这种过程称为固化考核过程。 问题在哪里?除了软件本身,模型也好、文档也罢,都不能够代替最后的代码。在精益原则中,我们说,必须消除浪费。当我们在开发工件的时候,我们的浪费行为已经或多或少的出现了。
与固化考核过程相对的,我们认为存在另一种动态评估过程。里程碑或是检查点并不是不重要。但是我们需要转换思路,来将里程碑的实践做的更好一些。我们上面提到说里程碑方式最大问题就在于一定要等到问题都积累起来了才解决问题,而这个时候往往已经错过了解决问题的最佳时机。而动态评估过程的含义就是在过程进行中不断的发现并解决问题,而不是等到问题累积到一定程度才批量解决。过程随着环境的变化不断的调整,以适应变化性和不确定性的需要。而里程碑实践重在提供一个复审的机会,能够从一个较高的层次上来评价软件。 这种过程就是分阶段开发软件的思路,我们也可以称呼它为迭代、螺旋、增量,都没有关系。关键在于,我们需要不断的发现导致客户不满意的问题,发现改进接电话的方法,发现改进做菜的方法,发现更快送货的方法。
实现策略
动态评估过程有一些基本的实现思路,第一个基本思路是尽可能早的发现所有的问题,如何发现呢?进行一次探险式的过程。这个过程周期不能够太长,太长的周期容易失控,而且项目初期人员未必能够全部到位;但这个周期也不能够太短,太短的周期无法发现足够数量的风险,无法为后续的过程提供丰富的数据。
有时候,我们运用原型法来实现这个Mini过程。原型法包括了需求原型和技术原型,分别用于解决业务风险和技术风险。一个典型的需求原型是建立一个界面原型,来帮助客户理解未来的软件,避免抽象的思考。我看过很多界面原型的做法,有使用HTML的,有使用画图软件的,有使用规范的XML的。但是不管如何,界面原型能够帮助用户直观的理解需求。技术原型的主要目标是解决技术风险,任何一个项目都可能存在这样或那样的技术风险。对待风险的基本态度是尽早的评估风险并制定解决方案,而不是束之高阁。技术风险的解决方案视具体情况而定,但是,值得注意的是,一个项目中,技术风险不能够过多。如果确实存在这种情况,想办法找到有经验的导师或培训师,这要比自己摸索节省许多的成本。
XP对探险式过程的评估主要包括两个方面,spike solution和迭代。spike solution其实就是我们在上面提到了的技术原型。它的目的是让不明确的评估成为明确的评估(参见XP的过程图中的Spike)。只有评估准确了,计划才能够准确。因此它是计划和迭代的输入项。
至于迭代,它是XP中的重要概念。迭代选取了用户需要的功能(称为用户故事),并进行设计、开发、测试,迭代不断重复,并解决各种各样的问题。在通过用户的测试和认可之后,最终产生了一个可以运行的版本。这个版本的产生,标志着一组迭代周期的完成。第一个小版本正是我们所强调的探险式的过程。它的成功和教训,足以让你了解项目的各种知识,包括客户的复杂组织关系,投资方的准确意图,找出所有的涉众,发现用例,令团队成员和客户达成初步的共识,验证技术方案,建立一个初步的软件架构,或是针对现有的架构进行初步的映射,程序员需要额外的培训,测试力量似乎不足够,部署环境的风险需要提前解决。只有你按照完整的生命周期真正的去做这项工作,这些问题才会在一开始都暴露出来,否则,其中的很多问题会在后续的阶段中给你制造大麻烦。 第二个基本思路是增量开发。增量和迭代有什么区别呢?Alistair Cockburn在Surviving Object-Oriented Projects一书中将增量描述为修正或改进开发过程,形象的说法是逐步的完成软件开发。注意到,XP的过程图中的小版本正是一个增量。XP认为,一个增量应该是可以发布的。做到这一点固然很好,但是并不是所有的项目都能够达成这一目标。例如,第一次的增量目标可能主要是定义一个架构,这个架构并不包含用户需要的功能,但是它是项目开发的基础。架构中可能包括业务实体基础结构、数据操纵基础架构等一系列的框架。但是对于XP来说,在用户无法发现价值的框架上花费大量的时间是不值得的,XP提倡的做法是根据需求的发展来逐步完善架构,而不是在项目一开始就花费精力开发架构。很难评价哪一种说法正确,我比较倾向于前期花费时间进行架构设计,但是实践中确实发生过设计过于复杂导致高昂成本的情况。在花费了大量的时间开发了一个属性处理框架之后,我发现其实简单属性就能够处理大部分的情况,毫无疑问,前期的设计投入打了漂。因此,重要的是权衡前期的投入时间。理想的情况是,你已经拥有了一个可重用的框架(或是架构),这样,你可以将项目的需求映射到框架上,而不是在项目一开始的时候花时间来开发框架。如果你没有框架,在项目一开始的时候,花费一定的时间来开发架构,但是这个时间不宜过长,你完全可以在后续的增量中对架构进行改进,所以不用急于一时。而且,单纯的框架(架构)开发是没有办法进行用户接受测试的,你的测试不得不推迟到第二次增量。这个理由也促使我们尽可能的缩短框架设计的周期。
而迭代则是改进或修正软件质量。这也是第三个基本思路。我们注意看XP过程图中的迭代,多次的迭代才构成一次的增量(小版本),每一次的迭代都是对上一次迭代的改进,其中可能是修正了设计错误,或是需求缺陷。值得注意的是,迭代中可能会出现新的需求变更(新需求或需求改变),并令项目人员对项目的进展速度更加的了解(Project Velocity),这些将会反过来影响计划的修正。这体现了我们在上一章所讲述的XP对待计划的态度。
并没有法律规定迭代需要和增量一起使用,但很明显,结合这两种方式是一种有效的做法。增量的目标是让项目得以向前推进(这就像是修路的时候,路的长度变长了),而迭代的目标是令软件的质量更优(就像是在一段路上架设路基、铺上水泥,建设路面设施)。这让我们想起了什么,不错,重构的两顶帽子。一顶帽子是为软件增加新功能,一顶帽子是改进软件的质量。非常的相似,只不过一个是过程级别的,一个是程序级别的。这里有一个基本的假设,不要同时增加功能和改进质量。团队也好,个人也好,一次只完成一个目标效率是最高的。
思考
和传统的先定义问题,然后再解决问题的做法不同,XP偏重于逐步的精化问题。软件开发中的问题定义和数学中不同,它往往是模糊的,动态的,需要在解决问题的过程中不断的调整解题的思路。对XP来说,这种解题思路,体现了其反馈的价值观-尽快获得客户对软件的反馈。
(三)实践迭代
在了解了分阶段开发软件的基本思路之后,紧接着就需要考虑实施的问题。分阶段开发最难的,并不是在过程的控制上,而是在软件设计能力上。
应用迭代的问题
有一则故事说的是一个人肚子疼,去看医生,医生给他开了眼药,理由是眼神不好,吃错了东西,所以才会肚子疼。软件开发中出现的问题往往不是单纯的问题,头疼医头,脚疼医脚的做法未必适合于软件开发。
应用迭代并不是一件简单的事情,懂得了迭代和增量的概念,并不等于你能够用好它们。为什么这么说呢?很多的软件组织尝试着运用迭代开发,但是结果却不尽人意,于是将问题怪罪在迭代的方法不切实际上。软件工程中有句著名的话?quot;没有银弹"。迭代和增量也不是什么银弹。要想做好迭代,缺乏优秀的软件设计思想和高明的软件设计师的支持是不行的。在XP中,非常强调各项实践的互为补充。在我看来,迭代能够顺利实行的思路需要重构、测试优先、持续集成等的直接支持。而这些实践,体现了软件设计和软件过程中的关系。
迭代实践出现问题往往是在项目的中期。这个时候,软件的主体已经形成,代码的增长速度也处于一个快速增长的情况。这种状态下的软件开发对变化的需求是最没有抵抗力的,尤其是那些设计本身存在问题的软件。软件开发到这个阶段,代码往往比较混乱,缺乏一条主线或是基础的架构。这时候,需求的变化,或是新增的需求导致的成本直线上升,项目进度立刻变得难以预期,开发人员的士气受到影响。
迭代之外的解决方法
在这个时候,软件组织要做的,并不是在迭代这个问题上深究下去,而是应当从软件设计入手,找到一种能够适应变化的软件设计思路或方法。例如,你是否应该考虑在面向对象领域做一些研究呢?面向对象的思路很注重将变化的内容和不变的内容相区分,以便支持未来的变化和应对不确定性。然后你再来考虑相应的成本。
做好迭代有几个值得注意的地方:
代码设计优化
软件开发的能力并不体现为代码量的多少,而是体现为代码实现的功能,代码的可扩展性、可理解性上。所以对代码进行不断的改进,对设计进行不断的改进(具体的次数根据需要而定),使软件的结构比较稳定,并能够支持变化。这是迭代的一个前提。否则,每一次的迭代都花费大量的精力来对原先的设计进行修改,对代码进行优化,这样的迭代效率是不高的,也可以视为一种浪费。坚持不断改进软件质量的做法其实是将软件的集中维护、改进的成本分摊到整个过程中,这种思路,和全面质量管理的思路是非常类似的。XP中的重构实践有一个修饰词,称为无情。这充分表现了XP的异类,但是应该承认,只有设计和代码的质量上去了,才能够为后续的迭代过程打下一个基础,更何况,XP所处的往往是一个不确定的、变化多端的环境。正是因为这种环境对软件开发有着很大的影响,因此代码质量也被高度的重视。不同的行业,不同的项目,需要根据自己的特征进行调整,但是,只有保证代码的优美性,才能够顺利地达成迭代的目标。
代码设计优化同时必须保持简单的原则,不在一开始进行大量的设计投入。以嵝牛砑嗦胫埃细竦娜砑杓剖遣豢苫蛉钡摹5锹模曳⑾终庵炙悸肺幢厥钦返摹T谧芙崃艘恍┛⒕橹螅曳⑾郑芏嗟氖奔淦涫凳抢朔言诹松杓粕稀?
在一个软件的设计中,对界面结构有着很强的要求,而Eclipse的设计思路正当其时。因此,我兴奋的将Eclipse的设计思路注入到界面设计上来,在花费了大量的时间进行设计和实现之后,发现并不能很好的满足需要。更为糟糕的是,由于设计的复杂性,导致调试和变更的难度都加大,而团队的其它成员,也表示难以理解这种思路。最后的这个设计废弃了,但是损失已经是造成了,复杂的设计和实现,足足花费了一个星期的开发时间。
重构和审查
除了第一次的迭代,后续的迭代过程都是建立在前一次迭代的基础上。因此,每一次迭代中积累下来的问题最终都会反应在后续的迭代过程中。要想保证迭代顺利的进行,对代码进行重构和审查是少不了的工作。其中最重要的工作莫过于消除重复代码,重复代码是造成代码杂乱的罪魁祸首。消除重复代码的工作可不仅仅只是找出公函这么简单,其间涉及到重构、面向对象设计、设计模式、框架等众多的知识。这些知识的介绍并不是本文的重点,但是我们必须知道,只有严格的控制好代码的质量,软件的质量和软件过程的质量才有保证。
推迟设计决策
精益编程告诉我们,尽可能推迟决策。在一个变化的环境中,早期的决策往往缺乏足够的事实支持和实践证明。即便是再高明的软件设计师,难免会犯错误,这是非常正常的,那么,既然目前的决定是有着很大风险的,那为什么我们还要急于做出决定呢?在看待设计这个问题上,一种比较好的做法是,尽量避免高难度、高浪费的设计,以满足现有的需要作为实现的目标。未来的需求等到确定的时候再进行调整。
推迟决策其实是软件设计的一大能力,为什么我们会推荐使用面向对象技术呢?因为面向对象技术具有很强的推迟决策的能力,先将目前确定的问题纳入面向对象设计,并为未来的不确定性留下扩展。推迟决策并不是一个简单的问题,它需要很强的面向对象的设计思维能力。
设计模式中有很多这方面的例子,其中的装饰模式具有很强的代表性。
在设计刚开始的时候,没有人知道ConcreteComponent最后的发展会是什么样。很明显,这是一个处于不确定环境中的设计,我们唯一能够确定的,只有Component这个类体系一定会拥有Operate这个方法,所以,我们设计了一个接口Component来约束类体系,要求所有的子类都拥有Operate方法。另一个目的是为客户端调用提供了统一的接口,这样,客户端对服务端信息的了解到了最小的程度,只需要知道Operate这个方法,并选择适当的类就可以了。还可以对这个模型做进一步的改进,令耦合程度进一步降低。
在统一了接口之后,我们就可以根据需要来实现现有的功能,我们实现了一个ConcreteComponent类,它实现了Component接口,并实现了核心的功能。如果在未来,需求的变化,要求我们增加额外的行为,我们就使用ConcreteDecorator类来为ConcreteComponent添加新的功能:
public class ConcreteDecorator implement Component
{
private Component component;
public void Operate()
{
//额外的行为
component.Operate;
}
}
先找出共通点,然后实现共通点,并把不确定的信息设计为扩展,这就是推迟决策的设计思路。但是,应该指出的是,上面这个例子的设计,仍然有很多的限制,例如,增加的需求(也就是某个ConcreteDecorator)中可能拥有新的接口,例如需要一个AnotherOperate方法,这时候,原先的扩展性设计就又变得难以满足需要了。在软件设计中,针对接口设计的灵活性和扩展性虽然比以往的设计增强的许多,但它并不是万能的,而且取决于设计师对需求的理解能力和设计水平。此外,推迟设计决策要求我们学习抽象的思维,识别和区分软件中变化和不变的部分。
注重接口,而不是注重实现
Martin Fowler把软件设计分为三个层面:概念(conceptual)层面、规约(Specification)层面、实现(Implementation)层面。软件的设计应该尽可能地站在概念、规约层面上进行,而不是过分关注实现层面。之所以有时候我们发现在迭代的过程中,软件难以承受这种变化,那么,很大的可能是规约层面和实现层面出了问题。我们在前面一节讨论重构和审查的时候说,消除重复代码是一项复杂的工作,针对规约设计就是其中最有效,但也是最难的一种方法。
我们可以把规约层面想象为软件的接口或是抽象类,或是具体类的公有方法,而把实现层面想象为实现类、实现细节。那么,我们的原则应该尽可能设计稳定的规约层面,并为客户(可能是真正的客户,大部分情况下是使用你的代码的客户程序员)提供一个优秀的、简单的界面(接口)。社会发展到现在的水平,任何一个人都不会花费过多的时间来研究你的代码,如果你的代码不能够为他人提供便利性,那么最后被淘汰的一定就是你的代码。Java语言的成功,很大程度上就在于他在保证其强大功能的同时,还提供了一个简单、易用、清晰的规约界面。
在软件设计中,重视规约层面的设计是很普遍的。为什么我们提倡三层架构的软件设计?最重要的是因为他为软件结构合理性贡献巨大,远远超过了他的其它价值。在现代的软件设计中,数据库、界面、业务建模其实是三种差异较大的技术,这就导致了三者的变化度是不同的。根据区分不同变化度的原则,我们知道,必须对三种技术进行区分。而这正是三层架构的主要思路。从这个思路扩展出去,我们还可以根据变化度的需要,将三层架构演变为四层架构、甚至多层架构。而多个层次之间,正是通过优秀的规约界面来达到最松散的耦合的。
在精益编程中,为了避免浪费,要求每位程序员提高代码的规约层面的稳定性是非常有必要的。一个系统中,设计优良的规约界面能够拥有比较好的抗变化能力,能够较好的适应迭代过程。
回归
版本2的软件出现了版本1中不存在的行为,称为回归。回归是软件开发中的主要问题。在对现有功能修改的同时影响原有的行为,这是造成bug的主要原因。在迭代的过程中,必须避免回归行为的出现。而避免回归问题的主要解决方法是构建自动化的测试,实现回归测试。
成功构建回归测试的关键仍然在于是否能够设计出优秀的规约界面,并针对规约界面进行测试。这样,不但设计具有抗变化性,测试同样具有抗变化性。而唯一可能改变的就只有实现了。在回归测试的帮助下,代码的变化是不足为惧的。我们把有关测试的详细讨论放在测试一节中。
组织规则
在后续的章节中,我们会详细的讨论XP中的一项非常有特点的组织规则-结对编程。这里我们需要知道,不同的团队有着不同的组织,其迭代过程也需要应用不同的组织规则。例如,组织的规模,小规模的组织可以应用更快的迭代周期,如一周,在一个迭代周期中,团队可以集中力量来开发一个需求,强调重构和测试,避免过多的前期设计。对于大的组织来说,可以考虑迭代周期更长一些,更注重前期设计,并将开发人员和测试人员的迭代周期交错开来。团队的组织构成也是影响迭代过程的主要原因。团队是否都是由相同水平的人构成,每个人的专长是否能够互补,团队是否存在沟通问题。
(四)需求和故事
如何分析需求,如何记录需求,如何将需求映射为设计,这些永远是需求分析中最为重要的问题。XP提倡以一种简单实用的态度来对待需求,而在软件开发的历史中,需求分析从来都是最需要严谨对待的工作流程。究竟谁是对的?
故事
每个人都喜欢听故事,这也许是从小就养成的习惯。如果能够把需求分析工作变成听故事的过程,那该有多好。需求分析人员写出一个个优美的故事,开发人员边看故事,边实现故事。也许这就是XP的设计思路所在。用户故事,XP把需求变成了一个个故事,摒弃了枯燥无味的需求稳定。文档的作用是传递信息,如果失去这个意义,再优秀的文档也没有任何用处。但是,完整细致、厚达数十页的需求文档是否真的能够达到沟通的目标呢?对于大多数而言,恐怕看到文档的厚度就已经心生惧意了吧。好吧,我们通过很多的辅助手段,可以强制要求开发人员都投入大量的精力来研究、学习复杂的需求文档。但是这厚厚的需求文档真的能够完整的记录所有的需求吗?更糟糕的是,需求是会发生变化的,到时候如何维护这份需求文档呢?回想精益原则,我们可以判定,这种处理需求的方式一定会产生大量的浪费。将需求做的尽善尽美需要成本,项目组的人员熟悉需求需要成本,维护文档需求成本,解决不一致的问题也需要成本。那么,我们可以针对这几点做一个分析:
需求的文档是否要尽善尽美?需求文档的最大目标是将信息从业务人员传递给开发人员(当然也会存在其它的目的,例如作为合同的组成部分)。那么,文档是否完美和能否实现沟通效果并没有直接的关系。
开发人员怎么才能够快速理解需求?文档的制作融入了制作者的思想,因此他人理解总是需要一定的时间的。解决问题的思路有两个:一是提供标准通用的做法;二是简化文档,简单的东西总是要容易理解,但简单的东西并不等同于制作容易。
维护文档需要成本。不管如何,维护成本始终是无法避免的,关键在于,能否降低这部分的成本呢?维护成本和文档数量、复杂度成正比,因此文档的数量要尽可能的少、复杂度要降低。此外,减少维护的次数也是关键的因素之一,在讨论精益原则的时候我们说尽可能推迟决策就是这个意思。
针对以上的几点,XP提出了自己的实现思路-用户故事。用户故事简单,每个人都会写,每个人也都能理解,改变起来也很容易。但用户故事只是对系统功能的一个简单的描述,他并不能提供所有的需求内容,因此,在XP中,用户故事的实践需要现场客户的支持。用户故事之所以简单,是因为它只是开发人员和客户之间的一种契约,更详细的信息需要通过现场客户来获得支持。 从XP的观点来看,用户故事有这么几点作用:
客户自己描述、编写需求。对于任何一个需求来说,最理想的状态都是开发人员教授客户编写需求。最差的情况是开发人员代替客户编写需求。毫无疑问的,XP要求的就是最优秀的做法。客户要能够自己开发需求,前提条件是编写需求的技巧应该足够简单,能够很容易掌握,或是经过培训很容易掌握。用户故事就是这样一种简单的机制。
用户的观点。优秀的需求应该是站在用户的角度来思考问题,是用户能够利用系统完成什么,而不是系统自己完成。用户故事很好的达成了这一原则。因为用户故事是用户站在自己立场上编写,表现了用户对系统的期望和看法。
重视全局,而不是细节。需求有精度上的差别,软件开发初期最关键的,是建立一个高阶的需求概况,而不是立刻深入细节。对于XP来说,最主要的细节需求获取的方法是经过现场客户。现场客户随时提供对需求细节的指导。因此,用户故事的重点在于,尽可能全面的发现需求,以及,维持一个简单的需求列表。
评估的依据。用户编写的需求为软件的估算提供了依据。虽然这个依据是比较粗的,但随着项目的发展,开发速度的估算会越来越精确。在需求初期就进行适当的估算,其目的是让用户能够有一个比较直观的成本概念。这为用户制定需求实现的先后次序提供了指导。
用户自己的统筹安排。制定用户故事就像是上商场购物,虽然每件物品都是有用的,但是最后购买的次序和数量则要取决于钱包的厚度。在每一个用户故事具有了成本(即上一条中的估算)之后,用户就能够权衡实际成本和需要,并排定需求的座次。
迭代计划的输入。用户对用户故事的选择直接影响到迭代计划的制定,在第一个版本中,用户希望能够实现哪一些的需求(通过选择用户故事),经过估算,这些需求是不是能够在这个版本中实现,计划需要多长的时间。这些都是需求对迭代计划的影响。
故事的弊端
在收到国外汇款时,业务人员需要记录汇款的相关信息,如果汇款指定的收款人帐户为本行帐户,进行入帐处理,如果收款人帐户属于同城同业(本地的其他银行),则通过同城同业转汇给收款人(后续如何处理?),如果收款人帐户属于异地同业(异地的其他银行),则通过银行的帐户行将汇款转汇至异地,并支付帐户行转汇的费用(后续如何处理?)。
以上是一个银行的国际结算业务中款业务的例子。简短的叙述和非正式的形式体现了XP强调的简单原则。故事帮助开发人员和用户理顺流程的关系。在上述例子中,我们看到开发双方对流程仍然存在一定的疑虑(即括号中有问号的部分),但是这并不影响到用户故事的创作,因为这个版本的用户故事还会变化多次。但从这个简单的例子上来看,我们发现故事的形式仍然存在着一些不足: 故事的形式更容易被人接受,但是也有不规则的缺点。任意描述需求虽然节约了培训的成本,但是却造成了不一致性。不同的人对故事有着不同的理解,对需求也就有了不同的理解。需求故事虽然看起来很简单,但是要讲好一个需求故事绝对不是一件容易的事情。需求规约过于形式化和正式化,导致了需求规约难以使用,但是完全不要形式也不是一个好的做法。在形式和可用性之间保持平衡,是讲好需求故事的关键。 需求故事虽然容易阅读,但是却很难写得好。如何控制需求的描写精度,如何分解需求,如何组织,如何确定边界。但是XP并不关心这个问题,只要能够起到沟通的效果,怎么做都行。这种态度是否正确我们暂不去评价。但在实践中,由于缺乏系统的指导,一个新手往往需要花费很长的时间才能够学会故事的写法。
对于XP来说,需求的开发只有先后次序之分。而先后次序的制定由客户来负责。但是在实践中,识别出先后次序并不仅仅是客户的责任,开发人员同样需要提供需求优先级和风险的建议。这里有几点需求优先级的建议:
需求中包含了主要的设计,或是包含了大量的业务实体和典型的业务规则。对于这样具有系统代表性的需求,应该赋予较高的优先级。
需求中存在重大的技术风险,例如需求中包括了OCR开发包,而开发团队原先并没有相关的经验。
需求中包含了重要的业务流程,代表了本软件的主要设计思路。
需求具有代表性,并且难以估算,急需对需求进行试验性的评估,以获得较为精确的速度值。
需求的时间紧迫。
采用用例技术
用例技术保持了需求的简单原则,用例和形式和用户故事非常的相似,但是用例具有自己的格式,虽然这个格式也是可以任意定义的。用例的重点是表示系统的行为。我们看看上面的例子如何用用例来表示: 主要角色:业务人员
层次:业务流程级别
前置条件:收到汇款
基本流程:
1 业务人员选择汇入汇款业务。
2 业务人员输入必要的汇款相关信息。
3 业务人员将汇款转入收款人帐户。
3.1 如果收款人为本银行帐户,直接入帐。
3.2 如果收款人为同城同业(本地的其他银行),则通过同城同业转汇给收款人(后续如何处理?)
3.3 如果收款人帐户属于异地同业(异地的其他银行),则通过银行的帐户行将汇款转汇至异地,并支付帐户行转汇的费用(后续如何处理?)。
备选流程
暂缺
可以看到,用例表示的内容和用户故事并没有太大的差别,但用例比较强调格式。虽然不同的团队有不同的格式,但是在同一个团队中,尽可能使用相同和相似的格式(不同的用例可能需要不同的用例格式)基本流程中的每一个步骤都代表了业务人员和系统一次交互,流程非常的简单,但是已经覆盖了一个成功的流程。我们看到,流程的每一步都高度抽象的原因是该用例的层次是业务流程级别的。(业务流程级别也仅仅是一种约定,并不是标准)。利用层次的概念对用例进行精度的划分。在上面的例子中,低精度的用例主要的目标是把握系统的全貌。在RUP中,这种用例也被称为业务用例(Business Use Case)。在原先的用户故事中,对分支情况描述比较含糊,但采用了用例的这种描述形式,分支情况就一目了然了,和前面一样,分支情况的表述也有很多种的形式。
用例技术从提出到现在,已经有了大量的经验积累。在XP项目中采用用例技术并不是什么新鲜事。但在XP 项目中采用用例技术并不是什么新鲜事。但在XP中应用用例也必须遵循XP的原则,以及精益编程的思路。所幸的是,这些思路是非常自然的,使用用例技术是完全可以实现的。本文并不打算详细的描述用例技术,如果要深入了解用例技术,有几本书是非常值得一看的(见附录)。
先把握系统的全貌:在做需求的时候,常常出现的一种情况是需求分析人员花费了很多的心思来精华、完善某个用例。对XP来说,这种做法并不推荐,而根据精益原则,这种行为存在浪费的可能性。我们对软件、对项目的认识是不断深入的。因此,我们在项目一开始就深入到需求、故事、或用例的细节,分析人员的能力可能很强,能够正确的捕捉到用户的实际需要。但是一个星期之后我们对需求的认识就有可能发生变化,也许是原先对用例范围的界定出现了问题,也许从另一个角度分析用例效果会更好,也许原先处理用例的思路不正确。不管如何,需求变化的可能性是非常大。用例越详细,发生变化的可能性就越大。这时候,原先花在精化用例上的时间就被浪费了。
因此,不要在一开始就精化需求,一开始的工作重点应该是放在尽可能全面的收集用例,了解整体的业务流程,分析主体业务流程等工作上。在获得了系统的全貌之后,你会发现你原先对系统的认识是不充分的,用例需要根据新的思路进行重新排列,用例的优先级需要调整,在UML图中,往往有一张系统的用例概览图,这张图所表示的就是系统行为的一个概述。
寻找优先级高的用例进行精化:我们在上文提到了需求优先级的判断,用例的优先级判断和需求的优先级判断相似。在讨论迭代的时候我们说过,前几次迭代的主要目的是要识别出项目风险。因此,寻找有代表性、优先级高的用例进行精化,能够帮助开发人员更快的理解领域知识,构建起初步的领域模型。 继续上面国际结算的例子,在完成总的用例图之后,我们发现,银行的业务非常的复杂,如果缺少领域专家,要在短时间内领会领域逻辑是非常困难的,同时,我们发现,汇款的业务在日常业务中所占的百分比是非常高的,而汇款业务涉及到了大多数的领域知识,而业务流程却相对简单。因此,我们决定,先把汇款的用例作为一个突破口,在完成了这个用例之后,我们的开发人员就会对业务领域有着比较深入的认识,也就能够进行更复杂的工作了:
主要角色:业务人员
层次:业务流程级别
前置条件:收到汇款
基本流程:
1 业务人员选择汇入汇款业务。
2 业务人员输入必要的汇款相关信息。
3 业务人员将汇款转入收款人帐户。
3.1 如果收款人为本银行帐户,直接入帐。
3.2 如果收款人为同城同业(本地的其他银行),则通过同城同业转汇给收款人(后续如何处理?)
3.3 如果收款人帐户属于异地同业(异地的其他银行),则通过银行的帐户行将汇款转汇至异地,并支付帐户行转汇的费用(后续如何处理?)。
备选流程
2.A在任何时候,业务人员都可以应客户的要求对向汇款银行进行查询。
2.A1在收到汇款银行的查询答复之后,记录答复信息。
2.B在任何时候,业务人员收到汇款银行要求退回汇款的授权。
2.B1如果汇款未被提走,根据要求将汇款退回汇款银行。
2.B2如果汇款已被提走,通知汇款银行无法处理,用例结束。
注意到,在这个例子中我们对用例优先级的判定条件和上文的稍有不同,我们选择有代表性,但又相对简单的用例作为高优先级的用例。这样做是因为对业务领域比较陌生,一开始实现复杂的需求有很大的难度。所以,虽然我们提供了一些制定用例优先级的思路,但是实践的时候仍需要根据实际情况权衡。 迭代精化:用例的编写过程是一个对业务领域不断熟悉的过程。随着调研的深入,不断有新的问题显露出来,需要补充或修改原先的用例。这里有两种情况,一种是在同一个增量内,在对用例B精化的时候,发现用例A中忽略了一种情况,这时候我们就需要补充用例A。例如,我们在精化其它用例的时候,发现汇款用例中忽略了报表的需求,这样我们的工作又必须回到汇款用例上。这样的情况是非常普遍的,这就要求我们不要过分的修饰用例,不要把精力花在用例格式上,这样只会造成浪费。
第二种情况是在不同的增量中,这时候用例往往会加入新的需求、新的情境。我们如何去控制不同增量期间的迭代呢?一般来说,有两种方法,一种是对原有的用例进行增补,增补的部分用不同的颜色或标记。另一种方法是为用例建立版本,不同版本的用例对应于不同的增量周期。这样,对应对N个增量周期就有了n个不同版本的用例(n≤N)。不管是哪一种情况,都要求我们采用迭代的思路来处理用例。
形式不是最重要的:在团队中强制要求统一的用例书写格式是有意义的,但有的时候,这个意义并没有想象中的那么大。可以约定条件的编写形式、也可以约定层次的划分。但是过分的强制形式就没有什么意义了。
(五)测试管理
无论从那一点上来看,要保证软件的质量,测试工作是少不了的。而测试往往又是经常被忽略的。对于敏捷方法,精益编程而言,如何保证测试的有效性?如何减小测试的成本?是测试中首要考虑的两个问题。
测试过程
要做好测试可不是一件容易的事情。测试工作和软件开发密切相关,却又自成体系。测试并不是一个单独的阶段或活动,测试本身就是一个过程,具有自己的生命周期,从测试计划开始,到测试用例的制定,测试的结构设计,测试代码的编写。测试的生命周期和软件开发生命周期拧在一起,相互影响。当然,我们还是那句老话,罗马不是一天建成的。对我们来说,还是从简单的开始。
在我们谈及精益编程理论的时候,曾经讨论过全面质量管理的概念:生产过程的每一个环节都需要为质量负责,而不是把质量问题留给最后的质检员。这对于软件开发有着很好的借鉴。软件开发中最头疼的就是质量问题,因为人的行为过于不确定了。在经过漫长的软件开发周期之后,软件渐渐成型,但是缺陷也慢慢增多,试图在最后的关头解决长期积累的问题并不是一个好的做法。软件开发到了这种时候,发现和修改缺陷需要付出很大的代价。
我们说,最后关头的测试并不是不重要,但是,软件质量问题应该在整个软件过程中予以重视。
测试的最小单位
测试问题的很重要的思路在于测试的管理上,如何管理一个项目中所有的测试,以及它们相关的文档,相关的代码,如何定义测试人员的职责,如何协调测试人员和开发人员之间的关系?
XP的测试优先和自动化测试实践是一个非常优秀的实践,我们也曾不止一次的提到该实践,但是对XP强调的单元测试,很多人都有一些误解:
XP中提供的例子过于简单,无法和生产环境相结合。XP中的单元测试只是为测试提供了一个具体的操作思路,但是它并不能取代其它的测试原理。如何进行测试,如何组织测试,如何管理测试,这些都要由不同的软件组织自己来进行定义。
测试代码本身不能够适应变化。黑盒测试的理想状况是外部行为不因为内部行为的改变而改变。当需求或是设计发生变化的时候,一段代码的内部行为需要改变,但是外部行为却不需要变化,这样,针对外部接口进行的单元测试同样不需要改变,但是这个规则一旦被违反,我们就需要付出同时改变测试代码的双重代价了。因此,测试代码的设计本身就是很讲究的。
单元测试(有时候也称为类测试)是代码级别的测试,是测试的最小单位。XP非常看重这个最小单位。我们观察测试优先框架XUnit,发现它使用组合模式将大量的最小单位的单元测试组织起来,形成完整的测试网。所以,XP的思路非常的简单:最小单位的测试能够做好,全系统的测试就能够做好。这个思路未必就正确,但是注重最小单位的测试的思路是绝对正确的。每个部件都正确,最后的软件未必正确,但任何一个部件不正确,最后的软件一定是不正确的。
测试优先
测试优先和单元测试在XP中属于同一个实践,但是它们仍然是由区别的。测试优先强调行为,在写代码之前写测试,单元测试主要指的是测试的范围或级别。我们说,测试优先实践真正关心的,并不是测试是否要先于代码,关键在于你是否能够编写出适合于测试的代码,是否能够从测试的角度来考虑设计,考虑代码。 从另外的一个角度上说,坚持测试优先的实践,可以让你从一个外部接口和客户端的角度来考虑问题,这样可以保证软件系统各个模块之间能够较好的连接在一起,而开发人员的思考方式,也会逐步地从单纯的考虑实现,转移到对软件结构的思考上来。这才是测试优先的真正思路。而坚持先写测试,只不过是帮助你转变思维习惯的一种措施而已。对于一些优秀的程序员来说,只要能达成目的,是否测试优先,倒并不是最关键的了。
其实做测试是一件很难的事情,因为很多时候,我们不能够完全的模拟出测试环境,或者是完全模拟出测试环境的代价太高。软件开发总是在一个固定的时间和成本的前提下进行,因此我们必须尽可能用小的成本来达成我们的关键目标。很多关于测试的书中都提到诸如磁盘出错之类的错误是很难进行测试的,但实际上,还有很多很多的内容是难以进行测试的。例如,一个业务逻辑,它使用到了14个业务实体和其它的一些配合的类,如何测试它?使用Mock Object方法,建立测试Fixture的代价将会很高,此外,如果实体类是可以控制的(例如,该实体类可以使用程序来初始化数据,而不是从数据库中获取数据),这个测试的成本还可以接受,如果不是(例如,第三方提供的技术),这个成本将会更高。类似的情况还有很多,但是为什么会出现这些问题呢?其中一个很大的原因就是我们并没有真正的把测试作为软件开发的一个重要的组成部分, 坚持测试优先的思考方式,可以大幅度的降低测试成本。现代的软件开发往往都依赖于特定的中间件或是开发平台,如果这些第三方产品没有提供一个强大的测试机制的话,要对最终的产品进行全面的测试往往是很难的。例如,在J2EE提供的Jsp/Serverlet环境,模拟Http的输入和输出是一件很难的事情。如果在软件设计阶段不考虑测试,那么最后的测试将会是寸步难行的。但是实际上,如果在软件设计时考虑到测试的困难程度,并将业务代码和环境控制代码区分开发,使之彼此之间没有过大的耦合。这样,测试工作就可以针对独立的业务代码进行,而这个成本就会低很多。
public class UserLog
{
public Service()
{
//难以进行测试的代码
//需要测试的业务代码
}
}
注意到,在上面的示例类中,提供服务的代码分为两个部分,一部分是框架提供的、难以进行测试模拟的代码,这类的代码有很多,例如对HttpRequest的处理,模拟http的数据是比较复杂的。这就增大了测试的难度。而这部分的处理往往是平台提供的功能,不需要进行测试。第二部分是关键的业务代码,是测试的核心。那么,一方面构建测试环境难度较大,另一方面又需要对业务代码进行测试。因此我们自然就想到将待测的业务代码分离出来:
public class UserLog
{
public static void Write(String name)
{
//写入用户信息;
}
}
public class UserLogAdapter
{
public Service()
{
//难以进行测试的代码
UserLog.Write(Name);
}
}
这样,测试就可以针对UserLog进行,由于不需要复杂的测试环境,对UserLog进行测试的成本是很低的。在J2EE核心模式一书中,提到了一种向业务层隐藏特定表示层细节的重构思路:
虽然,这种重构方法的出发思路是避免界面层次的细节暴露给业务层,但是从另一个角度来说,也提高了业务层组件的可测试性。毕竟,构建一个用户信息,要比构建一个HttpServeltRequest要容易的多。
因此,最合理的引入测试的阶段是在需求阶段。需求阶段的测试工作的重点是如何定义测试计划,如何定义接受测试并获得客户的认可,在需求阶段结束的时候,必须保证所有的需求都是可测试的,都拥有测试用例,需求阶段另一个重要的测试任务是准备构建测试沙盒,建立一个测试环境,以及这个软件项目所需要的测试数据;在设计阶段,测试工作的重点则在于如何定义各个模块的详细测试内容,最好的方式是实现测试代码,并构建测试框架,对于一些比较复杂的项目,甚至还需要编写一些测试工具。实践中我们发现,在XUnit的基础上扩展出一个测试框架是一种简单但又实用的方法。XUnit的重点是对自动化测试提供了一个通用的框架,捕获异常,记录错误和失败,并利用组合模式对Test Case和Test Suite进行管理。实际上,还有很多工作是可以在XUnit框架上继续开展的,例如,软件开发中是不是存在较为通用的测试用例?这样,你就可以定义一些抽象的测试用例,并以此作为测试框架的基础。再比如,我们希望每天晚上在进行日集成的时候,测试结果能够通过短信直接发送到负责人的手机上,那么我们可以在框架中嵌入这部分的功能。这些都属于对测试框架的积累。对一个软件组织来说,很有必要花费时间对测试框架进行积累。这可以简化测试的工作量,并提升软件的质量。
测试过程
我们一开始说,测试有其自己的过程,虽然XP并没有花费太多的笔墨来描述自己的测试过程,但经过细心的观察,我们可以发现,在XP中同样存在着一个测试过程:
这个过程是从用户故事(或者是我们在上一章中推荐的用例)开始的,用户故事不但为版本计划提供了需求,而且为接受测试提供了测试场景。而对于客户参与的接受测试来说,它为每一次的迭代提供了反馈,包括bug的反馈和下次迭代信息的反馈。只有客户认可了接受测试,软件才能够发布小版本。这是XP过程最高层次的测试过程。
在上文中,我们提到引入测试最好的时机是在需求分析阶段。因为测试生命周期的起源活动-测试计划和测试用例都需要需求的支持。我们再参考RUP的过程:
我们看到,RUP建议在先启阶段就开始测试活动。在开发过程的前期就进行测试活动,其目的是为了提高软件的可测试性。软件设计如果没能够考虑软件的可测试性,那么测试的成本就会升高,软件质量随之下降。有时候,单元测试或是组件测试是很难进行的。因此,我们需要专门针对类或组件的可测试性进行测试。例如,对于一个实现企业流程的组件,之间涉及到大量的状态、事件、分支选择等等因素。对这样的组件进行组件测试的代价是非常高的。如果能够在组件设计的时候,能够考虑到测试性,例如,将组件拆分为粒度更小的子组件,或是在组件中内嵌供测试使用的方法,能够直接操纵组件的状态。在设计时充分考虑可测试性,是降低测试成本的关键。而设计测试的源泉,正是先启阶段中对需求的分析。对流程组件测试的依据,正是源于项目涉众对流程的需求。
测试的一些实践问题
严格按照先维护测试,再维护代码的顺序要实现变更。在实践中,测试优先常常发生的一个问题是,设计变更影响到测试代码的时候,开发人员往往会绕过测试代码,直接修改代码。
在刚刚接触测试优先思路的时候,我严格按照先写测试的做法编写代码,但是当代码需要修改时,有时候只是一些非常小的修改,这时候我仍然保持原有的习惯,直接对代码进行了修改,在完成代码的修改之后,我突然意识到测试代码需要修改,于是我又修改了代码,由于只是一个小修改,我认为没有必要再运行测试了。这件事情很快被我遗忘了,但隐患就此埋下。到了两天后的集成测试时,测试程序捕捉到了这段代码的错误,经过调试,发现当时认为简单的修改忽略了一种极端的情况。定位错误,调试代码,并通过测试的时间远远超过了当初贪图省事节省的时间。所幸的是,代码在下一个检查点(集成测试)被发现出来。 完善测试网。在我学习并实践测试优先的时候,我所处的团队正处于项目的中期,已经有大量的没有实现测试的代码被创建出来,当时我采取的思路是,新编写的代码必须遵循新的测试方法,旧有的代码保持现状。这样做可以节省一定的成本,但是很快我们发现,投入力量把现有的代码加上测试是绝对值得的。加上测试的代码能够迅速回应变化,仅仅这一点,就值得我们重建测试网。此外,由于需要构建测试,我们还发现了原有代码中一些接口定义不合理或是不规范的地方。 而在另一些一开始就采用测试优先思路的项目中,往往遇到的问题是,随着项目的进展,后期的测试代码越来越优秀。这时候,我们需不需要对原有的测试代码进行改进呢?答案是肯定的,你一定会从中获益的,对于自动化测试来说,修改测试代码并重新运行测试的代价并没有你想象中的那么大。
完美的测试是不存在的,但是测试可以越来越完美。我们在文章一开始就提到了全面质量管理(TQM)的思路,TQM认为,产品生产的每个过程都会对最后的产品质量产生影响,每个人都需要对质量负责。对于软件开发也是一样,开发过程的任何步骤都会对软件质量产生影响,要提高软件质量,并不是加强测试力量就能够做到的,需要在整个过程中保证软件的质量。构建测试并不断改进测试的行为贯穿于整个开发过程,为质量提供了基础的保证。
自动化测试
自动化测试是XP测试活动的另一个优秀思路。在我们讨论迭代的时候,曾经简单讨论过回归和自动化测试。只有测试实现了自动化,回归测试才能实现,重构才能够贯彻,而迭代也才能够进行。所以XP一直强调它的实践就像是拼图,只有全部实现才能够完全展现其魅力。单单从这个角度,我们就能够体会到这句话的含义了。
对于一个自动化测试系统而言,有几个部分是特别重要的:
数据准备:对于一个简单的TestCase而言,数据准备的工作在Setup中就完成了处理(参见JUnit),但是现实开发过程中的测试数据通常比较复杂,因此有必要准备单独的数据提供类。对于一个完整的企业应用系统而言,往往包含数千的测试用例,而相应的测试数据量也极为庞大,这时候,我们还需要有专门的机制来生成和管理测试数据。
测试数据和特定的项目有关,因此不存在一个标准的建立测试数据的规范。所以我们在XUnit框架中看到,框架仅仅只是把建立数据这个活动给抽象出来,并未做额外的处理。但对于自动化测试而言,为各个单元测试建立独立的测试数据是很有必要的。测试数据的独立性是测试用例独立性的前提。测试数据大部分采用脚本的形式建立,包括输入数据和输出数据两个部分。例如,对于一个业务实体,就可以使用一个脚本来对它的属性赋值。脚本文件的形式有很多,例如配置文件、数据库数据脚本等。
验证:验证是将待测试的方法返回的结果值和预定的结果值进行比较,以判断该方法是否成功执行。结果值总是和输入值相匹配,因此,我们经常将结果值和输入值放在同样的脚本中处理。比较通用的验证方式是采用断言机制,此外,还包括错误记录、浏览测试结果,产生测试报告等功能。
桩:桩(Stub)是自动化测试中常用的一种技巧。在OO设计中,类和类之间往往都有关系,我们如何对一个依赖于其它类的类进行单独的测试呢?很多的软件设计中都存在难以模拟错误的现象。例如对磁盘出错、网络协议出错的情况就难以模拟。测试桩的思路就是为了解决这些问题,一个桩并不是真正的对象,但是能够提供待测对象感兴趣的数据或状态,这样,待测试对象就能够顺利的使用依赖对象,或是模拟事件。