敏捷方法的兴起对设计提出了新的要求,其最核心的一点是针对无法在项目一开始就固化的需求进行演进型的设计。在项目一开始就进行细致、准确的架构设计变得越来越难,因此,架构设计在项目的进展中被不断的改进,这相应导致了编码、测试等活动的不稳定。但是,软件最终必须是以稳定的代码形式交付的。因此,架构设计必须要经历从不稳定到稳定的过程。而架构设计能够稳定的前提就是需求的稳定。
需求冻结
敏捷方法和传统方法的区别在于对待变化的态度。传统的做法是在编码活动开始之前进行充分、细致的需求调研和设计工作,并签署合同。确保所有的前期工作都已经完成之后,才开始编码、测试等工作。如果发生需求变化的情况,则需要进行严格的控制。而在现实中,这种方法往往会由于对开发人员和客户双方需求理解的不一致,需求本身的变化性等问题而导致项目前期就完全固化需求变得不现实。结果要么是拒绝需求的改变而令客户的利益受损,要么是屈从于需求的改变而导致项目失控。敏捷方法则不同,它强调拥抱变化。对于易变的需求,它使用了一系列实践,来驯服这只烈马。其核心则是迭代式开发。应该承认,做到掌握需求并不是一件容易的事,而迭代开发也很容易给开发团队带来额外的高昂成本。要做到这一点,需要有其它实践的配合(下文会提到)。因此,我们在迭代开发进入到一定的阶段的时候,需要进行需求冻结。这时候的需求冻结和上面提到的一开始就固化需求是不一样的。首先,用户经历过一次或几次的迭代之后,对软件开发已经有了形象的认识,对需求不再是雾里看花。其次,通过利用原型法等实践,用户甚至可能对软件的最终形式已经有了一定的经验。这样,用户提出的需求基本上可以代表他们的真实需求。即便还有修改,也不会对软件的架构产生恶劣的影响。最后,需求冻结的时点往往处于项目的中期,这时候需求如果仍然不稳定,项目的最后成功就难以得到保证。
在需求冻结之前,不要过分的把精力投入到文档的制作上,正确的做法是保留应有的信息,以便在稍后的过程中完成文档,而不是在需求未确定的时候就要求格式精美的文档。在格式化文档上很容易就会花费大量的时间,如果需求发生改变,所有的投入都浪费了。文档的投入量应该随着项目的进行而增大。但这决不是说文档不重要,因为你必须要保留足够的信息,来保证文档能够顺利的创建。
确保有专人来接受对变更需求的请求,这样可以确保需求的变化能够得以控制。这项工作可以由项目经理(或同类角色)负责,也可以由下文所说的变更委员会负责。小的、零散的需求很容易对开发人员产生影响,而他们有更重要的任务――把项目往前推进。此时项目经理就像是一个缓冲区,由他来决定需求的分类、优先级、工作量、对现有软件的影响程度等因素,从而安排需求变更的计划――是在本次迭代中完成,还是在下一次迭代中完成。
建立需求变更委员会是一种很好的实践,它由项目的不同类型的涉众组成,可能包括管理、开发、客户、文档、质量保证等方面的人员。他们对需求变更做出评估及决定,评估需求对费用、进度、及各方面的影响,并做出是否以及如何接受需求的决定。由于委员会往往涉及到整个项目团队,因此效率可能会成为它的主要缺点。在这种情况下,一方面可以加强委员会的管理,一方面可以保证委员会只处理较大的需求变更。
在项目的不同时候都需要对需求进行不同程度的约束,这听起来和我们提倡的拥抱变化有些矛盾。其实不然。对需求进行约束的主要目的是防止需求的膨胀和蔓延,避免不切实际的功能列表。我们常常能够提到诸如 "这项功能很酷,我们的软件也要包含它"以及"我们的对手已经开发出这项功能了,最终的软件必须要包含这项功能"之类的话语。这就是典型的需求蔓延的征兆。在项目开始时正确的估计功能进度,在项目中期时控制需求,在项目晚期是杜绝新增需求,甚至剪切现有需求。通过三种方法来保证软件能够保时保质的推出。
稳定架构
即便是需求已经成功的冻结了,我们仍然面对一个不够稳定的架构。这是必然的,而不稳定的程度则和团队的能力,以及对目标领域的理解程度成反比。因此,架构也需要改进。前一个模式中,我们讨论了对架构的重构,其实这就是令架构稳定的一种方法。经验数据表明,一系列小的变化要比一次大变化容易实现,也更容易控制。因此在迭代中对架构进行不断重构的做法乍看起来会托慢进度,但是它为软件架构的稳定奠定了基础。重构讲究两顶帽子的思维方式,即这一个时段进行功能上的增加,下一个时段则进行结构的调整,两个时段决不重复,在对增加功能时不考虑结构的改进,在改进结构时也同样不考虑功能的增加。而在架构进行到稳定化这样一个阶段之后,其主要的职责也将变为对结构的改进了。从自身的经验来看,这个阶段是非常重要的,对软件质量的提高,对加深项目成员对目标领域的认识都有莫大的帮助。而这个阶段,也是很容易提炼出通用架构,以便软件组织进行知识积累的。
在这个阶段中,让有经验的架构师或是高级程序员介入开发过程是非常好的做法。这种做法来自于软件评审的实践。无论是对于改进软件质量,还是提高项目成员素质,它都是很有帮助的。
架构稳定的实践中暗含了一个开发方法的稳定。程序员往往喜欢新的技术、新的工具。这一点无可厚非。但是在项目中,采用新技术和新工具总是有风险的。可能厂商推出的工具存在一些问题没有解决,或者该项技术对原有版本的支持并不十分好。这些都会对项目产生不良的影响。因此,如果必须在项目中采用新技术和新工具的话,有必要在项目初期就安排对新事物进行熟悉的时间。而在架构进入稳定之前,工具的用法、技术的方法都必须已经完成试验,已经向所有成员推广完毕。否则必须要在延长时间和放弃使用新事物之间做一个权衡。
保证架构稳定的优秀实践
在文章的开头,我们就谈到说在项目起始阶段就制定出准确、详细的架构设计是不太现实的。因此,敏捷方法中有很多的实践来令最初的架构设计稳定化。实际上,这些实践并非完全是敏捷方法提出的新概念。敏捷方法只是把这些比较优秀的实践组织起来,为稳定的架构设计提供了保证。以下我们就详细讨论这些实践。
在不稳定的环境中寻求稳定因素。什么是稳定的,什么是不稳定的。RUP推荐使用业务实体(Business Entity)法进行架构设计。这种方法的好处之一是通过识别业务实体从而建立起来的架构是相对稳定的。因为业务实体在不稳定的业务逻辑中属于稳定的元素。大家可以想象,公司、雇员、部门这些概念,几十年来并没有太大的变化。对于特定的业务也是一样的。例如对于会计总帐系统来说,科目、余额、分户账、原始凭证,这些概念从来就没有太大的变化,其对应的行为也相差不大。但是某些业务逻辑就完全相反了。不同的系统业务逻辑不同,不同的时点业务逻辑也有可能发生变化。例如,对于不同的制造业来说,其成本核算的逻辑大部分都是不一样的。即便行业相同,该业务逻辑也没有什么共性。因此,稳定的架构设计应该依赖于稳定的基础,对于不稳定的因素,较好的做法是对其进行抽象,抽象出稳定的东西,并且把不稳定的因素封装在单独的位置,避免其对其它模块的影响。而这种思路的直接成果,就是下一段提到的针对接口编程的做法。例如对于上面提到的成本核算来说,虽然它们是易变的、不稳定的,但是它们仍然存在稳定的东西,就是大部分制造业企业都需要成本核算。这一点就非常的重要,因此着意味着接口方法是相对固定的。
保持架构稳定性的另一种方法是坚持面向接口编程的设计方法。我们在分层模式中就提到了面向接口编程的设计方法,鼓励抽象思维、概念思维。从分层模式中提到的示例中(详见分层模式下篇的面向接口编程一节),我们可以看出,接口的一大作用是能够有效的对类功能进行分组,从而避免客户程序员了解和他不相关的知识。设计模式中非常强调接口和实现分离,其主要的表现形式也正是如此,客户程序员不需要知道具体的实现,对他们来说,只需要清楚接口发布出的方法就可以了。
从另一个方面来看,之所以要把接口和实现相分离,是因为接口是需求中比较稳定的部分,而实现则是和具体的环境相关联的。下图为Java中Collection接口公布出的方法。可以看到,在这个层次上,Collection接口只是根据容器的特性定义了一些稳定的方法。例如增加、删除、比较运算等。所以这个接口是相对比较稳定的,但是对于具体的实现类来说,这些方法的实现细节都有所差别。例如,对于一个List和一个Array,它们对于增加、删除的实现都是不一样的。但是对于客户程序员来说,除非有了解底层实现的需要,否则他们不用了解List的add方法和Array的add方法有什么不同。另一方面,将这些方法实现为固定的、通用的接口,也有利于接口的开发者。他们可以将实现和接口相分离,此外,只要满足这些公布的接口,其它软件开发团队同样能够开发出合用的应用来。在当前这样一个讲求合作、讲求效率的大环境中。这种开发方法是非常重要的。
java.util Interface Collection | |
boolean | add(Object o) |
boolean | addAll(Collection c) |
void | clear() |
boolean | contains(Object o) |
boolean | containsAll(Collection c) |
boolean | equals(Object o) |
int | hashCode() |
boolean | isEmpty() |
Iterator | iterator() |
boolean | remove(Object o) |
boolean | removeAll(Collection c) |
boolean | retainAll(Collection c) |
int | size() |
Object[] | toArray() |
Object[] | toArray(Object[] a) |
重构。代码重构是令架构趋于稳定的另一项方法。准确而言,重构应该是程序员的一种个人素质。但是在实际中,我们发现,重构这种行为更加适合作为开发团队的共同行为。为什么这么说呢?最早接触重构概念的时候,我对面向对象的认识并不深入。因此对重构的概念并不感冒。但随着经验的积累,面向对象思维的深入。我渐渐发现,重构是一种非常优秀的代码改进方式,它通过把原子性的操作,逐步的改进代码质量,从而达到改进软件架构的效果。当程序员逐渐熟练运用重构的时候,他已经不再拘泥于这些原子操作,而是自然而然的写出优秀的软件。这是重构方法对各人行为的改进。另一方面,对于一个团队来说,每个人的编程水平和经验都不一而足,因此软件的各个模块质量也都是参差不齐的。这种情况下,软件质量的改进就已经不是个人的问题了,而该问题的难度要比前一个问题大的多。此时重构方法更能够发挥其威力。在团队中提倡使用、甚至半强制性使用重构,有助于分享优秀的软件设计思路,提高软件的整体架构。而此时的重构也不仅仅局限在代码的改进上(指的是Martin Fowler在重构一书中提到的各种方法),还涉及到分析模式、设计模式、优秀实践的应用上。同时,我们还应该看到,重构还需要其它优秀实践的配合。例如代码复审和测试优先。
总结
令架构趋于稳定的因素包括令需求冻结和架构改进两个方面。需求冻结是前提,架构改进是必然的步骤。在面向对象领域,我们可以通过一些实践技巧来保持一个稳定的架构。这些实践技巧体现在从需求分析到编码的过程中。稳定化模式和下一篇的代码验证模式有很多的关联,细节问题我们会在下一篇中讨论。