敏捷,把纪律留下(上)

(本文发表于《程序员》2009年第6期,转载请注明。


在软件行业,大部分经理们都希望自己率领的团队能像军队一样具有铁的纪律性。在一次敏捷培训中,我们与众多来自国内软件公司的项目经理们讨论了敏捷,以及他们现在各自的开发方法和问题。闲谈中,一位学员冒出一句,开发团队应该像军队,不仅要整体阵法严密,而且每个兵都要纪律分明。这次培训主要是介绍敏捷的技术实践,比如测试驱动开发、持续集成、用户故事等,该学员认为这些敏捷实践不仅可以提高员工的技战术,还可以塑造团队成员的纪律性。如果这些敏捷实践在日常开发中都能落在实处,势必将提高团队成员的战斗素养战术素养。一言以蔽之,相较于其他软件开发模式,敏捷方法对团队成员的纪律性提出了更高的要求,鼓励团队成员成长为项目经理心中的合格军人

其实,抛开敏捷方法,哪一种软件开发方法又何尝不强调团队成员的纪律性?计划驱动的传统型开发方法给软件过程制定了严格的计划书和检验标准,希望能提高团队的纪律性。它们的出发点是对的,但因为缺少了具体的技术实践导致计划书并不能匹配团队的真实状态;检验标准大多是着眼于与最终交付软件无关的中间文档,这些都使得成员在工作中对项目开发的约束力感受不深。比如,很多项目里面的规范说明书、WBS表和甘特图都画得非常详细,但大多数时候这些东西与项目真实情况的落差太大,很难指导督促成员的日常开发工作。而且,这些文档与需要交付的软件产品的关联性不强,也很难能让成员和其他人通过这些文档建立对软件交付的信心。长期看到团队的表现与计划的不相符,项目经理们往往会感叹团队的纪律性不行。那么,为什么说敏捷方法能相对一定有效地提升团队成员的纪律性呢?我们先来看看纪律的定义。

纪律

一般来说,纪律有三种基本涵义:1.纪律是指惩罚;2.纪律是指通过施加外部约束达到纠正行为目的的手段;3.纪律是指对自身行为起作用的内在约束力。这三层意思概括了纪律的基本内涵,同时也反映出良好纪律的形成过程是一个由外在的强迫纪律逐步过渡到内在自律的过程。从纪律的含义来看,达到自律是最终的目标,也是施加外部约束的目的所在。通过奖惩来达到一定的纪律性,比如程序员开发过程中的bug率等等,这是生活中最常见的一种形式。这种方法因为检查的结果与具体生产过程差得太远,而且评判标准还是比较粗放,所以应该是最低级的方式。施加外部约束,比如检查列表,指导产品的具体生产,可以评判成员的各个环节是否符合标准,应该算中级方式。只有自律,真正让成员把纪律的观念贯穿在生产的每个环节,主动改进,从而改善生产。而这,也是纪律性的高级方式。

对比这个标准,我们可以看到:计划驱动型的软件方法学强调更多的是奖惩,也涉及到一些外部约束(代码复审等),这也是为什么它们在培养团队成员纪律性上难度比较大。而敏捷方法,通过强调承诺,强调每个成员都是理性人的事实,借助于成员的自律性来达到严明的纪律性。国内有一家业内非常有名的技术网站InfoQ,四月份在北京举行的QCon
Beijing
就是由InfoQ主办。除了有限的几位全职员工,大部分的中文编辑都是社区的活跃分子,他们走到一起,通过之间的承诺和信任维持着日常工作,也给全国技术爱好者传播国内外的业界最新新闻和技术。撇开具体项目团队而言,这就是敏捷团队最好的写照。但是,我们也应该看到,InfoQ类型的团队是可遇不可求的。现实中,大部分的开发团队还是良莠不齐,项目经理们很难去完全授权给手下的员工。为什么敏捷方法又能有效地提升成员纪律性呢?答案在于敏捷方法不仅仅强调承诺,也包含了丰富的技术实践:不仅给个人带来更短更频繁的反馈,也给团队和组织带来了多层次较全面的反馈。而反馈的频繁程度,则是外部约束发挥作用的重要基础,也即提升纪律性的重要手段。

戴明环(PDCA

我们来看看被认为是组织或团队改进或解决质量问题的基本准则的戴明环。戴明环(PDCA)是由美国质量管理博士戴明在20世纪50年代提出,PDCA是英语单词Plan(计划)、Do(执行)、Check(检查)和Action(行动)的第一个字母,PDCA循环按照计划-执行-检查-行动的顺序进行,并且循环不止地进行下去,是一个立体多层级的螺旋式的演化过程。PDCA循环最开始是用在质量管理领域,但实际上它是有效进行任何一项工作的合乎逻辑的工作程序,是放之四海而皆准的指导性原则。下面我们就用它来说明外部约束对纪律养成是如何影响。

PDCA循环里面的Action是一个循环的关键,对Check的结果进行处理,成功的经验加以肯定并适当推广、标准化;失败的教训加以总结,未解决的问题放到下一个PDCA循环里,但它必须以上一环节的Check结果为基础。如果Check不到位,不能具体到实际工作,Action的正确性和出发依据就很值得商榷。软件开发是一个以不确定性为主要特征,强调知识的活动。为了采取的Action具有较高的正确几率,更需要强调开发过程的Check比较频繁、具体,不断给团队提出直接的反馈,这样才能减少不断累积的不确定性最后带来成不可控制的后果。如此来看,短而频繁的反馈对外来约束真正施加到个人的效果是非常重要的。

PDCA循环还有一个特点是多层级性:各层级质量管理都有一个PDCA循环,形成一个大环套小环,一环扣一环,互相制约,互为补充的有机整体。软件开发通常会把个人、团队和组织都牵扯进来:个人完成功能的开发,团队完成软件的开发,组织负责完成客户需求的完成。传统的软件开发方法更多的是强调团队、组织层级的计划、图表等文档,关注于团队与组织层级的反馈。对于真正创造产品质量的日常开发环节,则缺少必要的检查和反馈。与之相反,支撑敏捷方法的敏捷实践,就从个人-团队-组织的几个层面都提供了相应的反馈:低层次的反馈,为上一层次的反馈提供了依据,同时也作为上一层次反馈的落实和具体。


--未完待续--

 

迭代经理是什么角色?(下)【译】

(节选自本人翻译中的《ThoughtWorks Anthology》一书的第7章“What Is an Iteration Manager Anyway?”)

7.6 迭代经理与迭代

迭代经理也有与迭代相关的职责。迭代经理与客户、团队一起工作,对每次迭代做出计划,包括:

  • 帮助客户排列优先级
  • 整理协调团队成员的建议
  • 计划团队的交付能力

迭代经理指导、鼓励和激发团队的士气。迭代经理通过健康检查来保持团队的坦诚。这些检查不是用来确保团队符合敏捷的所有方面,而是看团队能从敏捷提供的哪些技术中受益。

迭代经理肩上最后一个与迭代相关的职责是会议促进(facilitation)。迭代经理主持和引导计划会议,包括迭代计划会议和发布计划会议。适当的促进迭代计划会议和发布计划会议可以指引团队走上成功。测量指标、进行中的各项事宜,以及其他不像计划那么顺利的事情,都必须公开坦诚地进行讨论。

在发布计划会议上,迭代经理展现出他们的洞察力,与客户对下一个发布需要交付的高层次功能特性做出计划。一旦计划得到认可,而且客户认同计划可能改变,迭代经理帮助团队进行高层次的工作量评估(例如,故事的点数),让客户知道下一个发布里面会交付哪些东西。

在迭代计划会议上面,迭代经理常常要防止团队成员签署超出他们交付能力范围的工作。同时,通过复审测量指标,迭代经理帮助团队成员“掌控”他们的手段,改进最后的会议结果。

最后,迭代经理负责推动回顾会议,这样团队能“快速发现做得不对的地方”,找出需要在下次迭代里面进行改进的地方。迭代经理需要引导团队的讨论,使其关注于本次迭代里做得好、以及做得不好的地方,不时提醒成员重点是如何改进做得不好的事情。这能营造出一个负责任的氛围,也能让团队成员提升自己。

7.7 迭代经理和项目

正如本文讨论,迭代经理承担了一系列项目相关的职责,但有时他们也会被要求负责团队文化建设。在让商业客户满意的同时,迭代经理促进形成满意、愉悦、高效和受尊重的团队成员。Fred George说,“作为次要目标,我期望看到因为迭代经理的工作,团队成员在项目结束的时候变得更为优秀。团队内充满信任,持续提高技能——这是迭代经理的工作。”

迭代经理致力于创造一个专业和敢于承担责任的氛围。这样的氛围体现在恰当的行为和习惯上,如下所示:

  • 对自己、他人和客户互相尊重。
  • 庆祝成功。
  • 失败乃成功之母。

迭代经理争取把团队拧成一根绳,所有的成员同舟共济。

7.8 结论

构建一台“润滑良好”的交付“机器”,持续地补充新的故事,调整机器:这构成了一份全职的工作。沟通清晰、实用主义至上以及处理变更的能力属于不同的技能,需要挖掘和培养。2000年之后,ThoughtWorks培训了很多迭代经理,派到客户现场,提高和巩固了敏捷项目的成功。迭代经理留下了可复用的流程,可以用以改进敏捷团队,完成设定故事范围的项目和改进团队文化。

这带来了愉快的、高效的团队成员。每日沟通、消除不和谐、保持客户对项目最新进展的了解,可以花上开发人员整天的时间,导致几乎没有时间编写代码。如果敏捷团队里面不存在迭代经理,团队很可能会失败。团队只需要关注手头的任务(或者故事),把这些杂事留给迭代经理。

--完成--

迭代经理是什么角色?(中)【译】

(节选自本人翻译中的《ThoughtWorks Anthology》一书的第7章“What Is an Iteration Manager Anyway?”)

7.3 迭代经理不做什么

迭代经理(IM)并不是项目经理(PM)。与项目经理不同,迭代经理需要在工作第一线,与团队成员一起面对每日的工作活动。如果你是迭代经理,就把项目预算、资源管理、承诺、以及大层面上的问题留给项目经理。只关注团队!

此外,迭代经理只是一名项目成员,而不是人力经理或者资源经理。迭代经理不负责给团队成员写年度总结。这可能会影响他们的中心任务,即保持一个中立的态度——既维护团队的权利,又保持团队关注于对客户优先级最高的功能。团队成员不应该绞尽脑汁以求给迭代经理留下好印象,而应该是在需要帮助的时候要求迭代经理给予帮助。

迭代经理也不是客户。通常由团队中的商务分析师或者架构师扮演客户,这取决于故事的性质,或者真实客户的可获性。但是,迭代经理不应该扮演客户,如果他们作为客户来下决定,就无法帮助团队恰如其分地解决问题。

最后,迭代经理不保证技术的完整性,也不保证对标准的遵守,抑或提供技术基础支持(例如,对构建、部署或者数据库的支持)。项目的实现活动,比如协调多个项目、协调部署或者演示,通常是技术负责人或者首席商务分析师来处理。

7.4 迭代经理与团队

虽然没有明确规定,但是迭代经理要承担一些日常职责。下面列举了其中一些:

  • 收集花在故事开发上的时间
  • 使交付过程中的瓶颈显现出来
  • 向客户汇报团队状态
  • 解决在每日站立会议上提出的问题、阻塞和障碍
  • 控制所有流向团队的工作,管理工作的分派以保持一个可持续的速度

收集单个故事的实际所花时间可以得到很多测量指标。收集这些时间,和其他不同的数据点比较,有助于迭代经理提高团队的产出。首先,比较迭代内已完成故事的实际所花时间和故事的点数,迭代经理就可以知道团队有多少时间花在真正的交付故事上面,又有多少花在团队会议和其他活动上面。其次,比较项目已完成故事的实际所花时间和团队计划的项目时间,迭代经理就可以明白团队的产能,以及团队对于项目的可用度。最后,比较已完成故事的实际所花时间和故事的预估时间,可以得到故事评估的准确度。上述所有的测量指标在不同的环境下都很有用,迭代经理用它们来帮助团队形成一个稳定的交付速度。

稳定的交付速度是计算未来迭代团队产能的基础。有了团队在每次迭代里面经过充分测试的产出物和每位团队成员的计划可用率,迭代经理可以基于实际的已验证的数据来规划团队的产能。产能是不受团队支配的,也不会因为明确的交付时间就自动达到。产能是预先计划的,这样团队成员才能自我管理好。如果交付速度与业务需求不同步,可以调整其他的项目杠杆,但仍然需要使用实际的产出物来预测将来的产能。

很多测量指标和精心地排列故事墙能识别出项目的瓶颈。假设一个故事预估的工作量是一天,但在经过三天开发以后,却还摆放在故事墙的开发栏里面:这说明遇到了瓶颈需要团队一起讨论。Fred George创造了一种成功的测量指标,就是手指图。这种图表使用了堆积图,每块区域分别代表了迭代周期中的一个阶段。每天更新故事的状态,团队可以观察到图上每个区域的增长,以及交付周期中故事的流转。当图上的所有区域成比例地增长时,这些区域就能形成手指的形状。当图上某块区域与其他相比不成比例时(比如说“等待开发”区域比“开发”区域宽),团队能发现瓶颈的存在。此时,团队可以讨论如何消除瓶颈以使团队的交付速度回复稳定。

在每日站立会议上面,迭代经理排除掉不必要的干扰,保持团队成员使用正确的方式:过去24小时做了什么,接下来的24个小时准备做什么,遇到了什么障碍。迭代经理留心倾听每人的任务项和当天需要排除的障碍,这样团队成员可以完成故事。如果有人在每日站立会议上霸占时间,谈论标准汇报内容之外的东西,迭代经理需要带领团队重新回到关注点上面。通常的做法是建议那人在站立会议之后做一个较大的问题解答。

7.5 迭代经理与客户

正如前面讨论的,测量标准可以帮助迭代经理判断团队可持续的速度。这允许团队定期做出承诺,并最终兑现。但是,为了团队成员能维持承诺,迭代经理必须保持客户在迭代阶段不去更改故事。迭代经理要像守门人,帮助客户对即将来临的工作排列优先级,而不是经常更改优先级使得团队无所适从。

作为守门人,迭代经理保护团队不受分心之扰,也防卫客户不经意间影响团队的生产率。在迭代之外,客户可以而且应该不断地更改优先级。直到迭代开始之前,所有影响决定的因素都是可以改动的,而且要经常介绍新的信息。

项目中即时决定的概念并不是一个全新的概念。由丰田知识工程发展而成的精益开发系统在多方案同步进行的开发工程方面已经有多年的成功经验。多方案同步进行的开发工程被描述成“很谨慎地延迟决定,直到必须做出决定;努力维持各种可能的选项,尽可能延迟决定至开发团队收集到足够的决策支持信息,而不是为了所谓的果断而快速排除其他选项,从而更快地达到最优方案。”

--未完待续--

迭代经理是什么角色?(上)【译】

(节选自本人翻译中的《ThoughtWorks Anthology》一书的第7章“What Is an Iteration Manager Anyway?”)

第7章 迭代经理是什么角色?

行业日新月异,敏捷、迭代式和迭代这些热门词已是“飞入寻常百姓家”,一个定义模糊的新角色——迭代经理,也浮出水面。这是新一代的项目经理么?抑或是美其名的团队带头人?又或者是管理上的一个新阶层?谁会被冠以这个“经理”头衔?

本文将着重阐述迭代经理作为软件团队成员的工作内容和价值。我们将分析迭代经理的职责范围,同时讨论作为一个不可或缺的角色,迭代经理在面对组织和文化挑战的情况下,如何维持一个健康的工作环境。

7.1 什么是迭代经理?

通常在大型的敏捷团队里面,项目经理不可能同时关注项目团队每次迭代的成功,和整个项目最终的成功。2000年,有一个项目团队困扰于甄选高优先级的工作,最终的解决办法是选出一个人以一个稳定的节奏给交付团队提供持续的高优先级的功能“流”:这就是迭代经理(IM)的雏形。

在迭代式开发的世界里,总是需要有人对项目团队提供支持,促进与业务客户的日常交流,使整个团队保持关注于高优先级的工作。ThoughtWorks的高级架构师Fred George把迭代经理描述成“面向内部的管理角色。迭代经理负责保证故事在团队中流动的顺畅,这涉及到合理分配任务、在技能需求改变的时候建议更换团队成员。”

7.2 怎样成为一个好的迭代经理?

迭代经理可以来自不同的能力背景——可以是技术能力(加上很强的人际能力!),也可以是分析能力(加上很强的人际能力!),甚至他们也可以从业务专家或者行政专家(加上很强的人际能力!)里面产生。他们必须拥有前瞻思考、乐观进取的态度和拥抱变化的才能。每天,这些面向内部的推动者使用他们的才能协助交付团队使之逐步完善。

比如,一旦明确了迭代的工作量,迭代经理就需要在迭代的整个过程中跟踪团队的进展,从实际出发,积极地在团队内部贯彻流程的改进。想象一下在站立会议上面,迭代经理听到开发人员说他已经在一个故事上面工作了三天,而这个故事原本估计是一天的工作量。因为要对每个团队成员的日常活动和迭代的进度负责,迭代经理需要去挖掘这个被低估故事的细节。如果迭代经理不能很快判断故事的真实状态,立即与客户就迭代计划的潜在变更进行沟通,团队就有可能面临承诺失信的风险。迭代经理可以从询问如下问题开始:

  • 开发人员弄清楚了故事的范围吗?
  • 故事任务在最初的评估之后是否发生了变化?如果发生了,是如何变化的?
  • 开发人员是否需要业务分析人员或者客户的帮助,以更好地理解故事所要求的完成结果?
  • 开发人员是否需要向技术负责人寻求帮助?
  • 是否有什么事情阻碍了开发人员完成故事(换句话说,是否存在硬件、软件,甚至基础设施的问题)?
  • 是否开发人员又被分派了另外一个项目,或者参加了太多琐碎的会议导致无法完成故事?

上述问题只是一个例子,说明为了保持团队按照预定的进度前进,向客户汇报项目的每日状态,迭代经理可能要承担的工作。每天,迭代经理都必须倾听团队的需要并且响应。其主要职责就是培养一台润滑良好的“机器”,能依据要求的质量在项目范围内交付功能。

迭代经理应该在技术熟练度和业务知识之间达到一个平衡。Mary和Tom Poppendieck写到敏捷领袖应该“同时对客户和技术都具有深刻的理解,这样才能赢得开发团队的尊重。”良好的沟通技巧是必须的。迭代经理的职责之一就是维护开发团队与客户,以及与管理阶层之间的关系。

同时,迭代经理必须推动、坚持和保障团队成员的权利。对于很多敏捷团队,这些权利来自于开发人员的“权利法案” ,经过了整个团队的同意。通常迭代经理都需要协助团队以确保这些权利得到了坚持。

通常这都是以在团队内部以及团队之间加强交流的方式出现。大部分的开发人员都不习惯于直接与客户交流,或者对故事的完成程度给出直接的判断。迭代经理通常需要提供数据、图表和图形的示例来推动开放的交流。

迭代经理也必须维护客户的权利。每次这样的诱惑——团队成员不按照优先级高低的顺序进行开发——稍微露出端倪,迭代经理就要作为客户代理人出现。还记得么,客户有权利只为那些符合他们所期望的优先级顺序的工作买单?整个过程自始至终,迭代经理都必须保持一个中立的态度。

--未完待续--

敏捷、生产力和商业价值

【注:本文是去年的旧文了,发表在《程序员》2009年2月期上。今日翻出来,炒炒现饭】

我们曾举办了一次为期三天的敏捷培训,学员主要是一些知名软件公司的项目经理和资深开发人员。培训期间,我们带领学员进行了丰富的游戏,通过寓教于乐的方式让他们体验了敏捷方法学的大部分知名实践,并讲解了敏捷方法学推崇的价值和原则。从学员的回顾以及意见表上可以看出培训效果是显著的,但是在培训过程中学员也提到一些问题,主要是对敏捷方法学的实践和价值比较疑惑。在回答问题的同时,我们能感觉到随着敏捷方法学在国内被引入、被宣传,很多软件组织或人员对敏捷方法学都已经有了基本的了解,但是对敏捷方法学向软件行业承诺的价值还存在不同程度的顾虑。

作为以“交付更多客户价值”为核心原则之一的方法学,敏捷方法学向软件行业承诺:敏捷方法学能提升软件团队交付能力,给客户交付更多商业价值。为什么敏捷方法能做到这两点承诺?软件组织如果没有实施过敏捷方法学,或者实施方法不得当,通常会对这两点产生怀疑,从而对是否决心在组织内部实施敏捷产生犹豫和观望情绪。那我们就来分析敏捷方法学是怎么做到的。

敏捷方法学提升交付能力

提升软件团队的交付能力,其实最好的办法就是释放团队的软件生产力,而不是试图去规范它、约束它。这里,我们用“软件生产力”来定义软件团队开发软件的能力,包括软件团队在软件开发过程中为了解决各种各样的问题而使用的开发语言、开发工具以及开发实践。

软件生产力是不断发展的。为了解决软件开发过程中的问题,方法学才被提出来。从某种意义上讲,只有方法学积极采纳并改造新的软件生产力,而不是因循守旧地墨守成规,才能真正释放软件团队的软件生产力。

那么问题就变成:敏捷方法学如何采纳并改造现在的软件生产力?

现在的软件生产力

在开发语言方面,现在JVM/C#.NET是转为平台语言,支持在其基础上多语言开发,比如Jruby,groovy,jython等语言就能在JVM上运行。原来不受重视的脚本语言,像ruby、python等也是重登大雅之堂,给软件团体提供了更多的选择。

IDE:现代IDE不只是集成了编码、运行和调试功能的工具,而且是把应用程序开发的整个生命周期都纳入管理,从分析建模、代码编写、配置管理、构建管理,再到测试的集成,突破了原来软件过程里定义的各个环节的边界。一些大受好评的IDE无不是内建了ALM的支持,并集成了软件开发的大部分最佳实践,如Intellij IDEA。

Build tool:自动构建越来越受到开发团队的欢迎,Ant、nant、rake、gant……各种平台各种语言的build工具不仅提供了丰富的build task,而且给开发人员进行扩展提供了足够的自由度。至于maven,更是希望充当项目底层设施的角色,成为管理项目不可或缺的一部分。

Revision management tool:在项目开发过程中,版本管理是非常重要的一个部分。随着技术的发展,VSS这种笨重的工具早已被冲进历史的车轮。而且除了CVS、SVN这些central server的版本管理工具,分布式版本管理工具,如git、mercurial也是日益成熟。

前面只是简单列举了现在软件开发过程会用到的一些工具,其实,现在的软件开发实践也有非常鲜明的特征。很多在日常开发中涌现出来的开发实践,并不专属于特定的软件方法,却早已得到业界的认可。

Developer unit test:自Kent Beck的junit框架发布以来,人们才意识到开发人员写unit test也是如此的轻巧和方便。在其他语言方面,testNG、nunit、runit等测试框架也极大地推广了单元测试的编写。

Refactoring:重构是优化代码设计的重要手段之一,在Martin Fowler出版Refactoring之后,才真正被大家所关注。up-front design就逐渐被日常开发中的incremental design所取代。而随着对重构的研究,越来越多的相关书籍给开发人员提供了更多的实践指导,比如Refactoring to Pattern等。

Periodic auto build:因为SCM工具和integration tools出现,自动构建越来越受到开发团队的欢迎:一方面提高了软件产品的质量,减少了产品对特定环境的依赖;另一方面减少了重复乏味的工作,也给开发团队节省了时间和精力。像Cruisecontrol这样的开源工具出现之后,自动构建就时时刻刻都在进行,并能及时给开发团队提供反馈。
敏捷方法学释放现在生产力

那么,敏捷方法学里面是如何对待这些软件生产力呢?是不闻不问,还是积极采纳它们,按照自己的价值观和理念改造它们,进一步提供更有效的开发实践呢?我们以一些知名敏捷实践为例,看看这些实践与当前软件生产力之间是什么关系。

Test-Driven Development

TDD的实践形式可以简单地归纳成:添加足够简单使得失败的测试,尽可能简单地修改功能代码使测试通过,重复这个过程;如果功能代码产生了bad smell,你需要重构来消除。用简单的方程式来描述,就是:

TDD = TFD + Refactoring

很明显,这个最有名的实践脱胎于unit test,又融合了重构的优点。“通过unit test可以改善代码质量”、“持续重构可以改善程序设计”,这些观点已经得到广泛认可。敏捷方法把developer unit test和重构吸收进来又加以改造,使得开发人员通过小步前进的方式获得更高的生产率,并交付更高质量的软件。而IDE集成ALM工具,更是让普通开发人员在应用TDD的开发过程中如虎添翼。

Pairing

Pairing 要求两个人在一起结对工作,结对的双方不要求都是开发人员,可以是开发人员与商务分析人员,或者开发人员与质量分析人员,又或者是商务分析人员与质量分析人员,甚至是团队成员与客户。

坚持代码评审以及保持团队概念一致性是两项被很多经验证明能提高生产率和交付质量的实践。Pairing就把这两者做到了极致:编码设计的时候,始终有另外一个人在评审你的代码,从一开始就杜绝了不良设计和代码的引入;工作的时候,始终有另外一个人和你保持对概念理解的一致性,便于团队保持概念一致性。

Continuous integration

有了持续集成服务器Cruisecontrol或者Cruise、自动构建工具ant等等、版本管理工具,以及其他代码分析工具,比如测试覆盖率工具、checkstyle工具,持续集成涉及的功能越来越强大,不仅能自动快捷地给开发团队提供构建结果,而且能提供其他更多的分析结果以帮组开发团队改进软件质量。这样,开发团队通过快速得到构建反馈而提升了生产率,也可以凭借分析结果验证交付软件的质量。

敏捷方法实践还有很多,比如Stories、story wall、stand up、retrospective等等。不管这些实践形式如何,其实我们都能从它们身上看到已经存在并被证明能提升生产率的开发实践。也就是说,敏捷方法学充分利用了已有的软件开发工具和开发语言的威力,极大地采纳了被证明了的软件开发实践,再按照一定的价值观和理念进一步改进这些开发实践。

从敏捷方法学对待现在软件生产力的态度,我们可以说敏捷方法学面对新兴软件生产力是积极采纳的,释放了软件团队最大的软件生产力,从而促进了软件团队交付能力的提升。

敏捷方法学给客户交付更多价值

不管软件开发中使用什么样的开发方法学,最终交付的软件是只有给客户产生尽可能多的商业价值,软件项目才能说是成功的。

对于传统的形式化的、戒律森严的软件工程方法学,不仅其要求生成的文档数目和种类,而且需要的管理资源投入、QA评审的程度和开发人员被要求遵守的严格流程1,都极大地偏离了“给客户交付价值”这个核心目标。因此,国际上一些著名的软件工程专家将传统的这些软件方法称之为“重(heavy)”方法,强调方法学对过程的极度严格要求。

与这些“重”方法不同,敏捷方法学就是以“给客户交付价值”为核心原则之一,而不是强调对过程的过度严格的管理。不仅在敏捷价值和原则里面紧扣“客户价值”,而且其所有实践都是客户价值驱动的。

对于软件开发来讲,客户关心的价值在哪些方面呢?在Amr Elssamadisy的Agile Adoption Patterns一书中,定义了如下的客户价值:

   1.  短的软件交付周期
   2.  及时的响应用户反馈
   3.  高的软件质量
   4.  软件的灵活性
   5.  项目团队的透明性
   6.  低的软件成本
   7.  高的投资回报率(ROI)

这些客户价值都是紧密联系在一起的,这里我们就只针对第1、3、7等三个方面来做一些相关的阐述。

   1.短的软件交付周期

软件交付得越早,交付周期越短,客户就能越早通过使用软件产品来创造自己的价值,特别是商业软件:这是很显然的。即使你交付的软件只包括了完整功能列表里的一部分,但因为那对客户而言是价值最高的部分,而且软件是可以运转的,客户也就得到了更多的价值。不仅仅是完整周期的缩短,更频繁的交付、增进的发布也能给客户带来商业价值。

在传统软件方法学里面,只有到软件交付期限才能交付给客户可用的软件版本。整个软件开发过程里面,客户得不到一个可运行的版本来使用。这样,就严重损害了客户的价值。

在敏捷方法学里面,项目团队通过INVEST的user story划分,保证了客户价值的周期性交付;通过简单设计来保持关注对客户而言优先级最高的user story,避免了对低价值的其他方面的付出;而通过持续集成来保证每个构建出来的版本都是客户可用的候选发布版本……这样,通过多种紧凑的实践,敏捷方法学保证了项目团队对客户价值的关注和尽早实现,并随时提供能满足一定客户价值的可用的软件版本。

   2.高的软件质量

软件质量越高,也就意味着软件的缺陷越少,提供的功能更加符合客户真正想要的要求。而且,即使商业需求上发生了变化,高质量的软件也容易进行扩展和修改。

在传统软件方法学里面,也有通过详尽的user case设计、代码评审、QA测试用例等方式来减少软件可能出现的缺陷。但因为分析、设计、开发和测试等环节的脱节,不同环节之见的理解不同,沟通不畅,很难挖掘客户真正的需求和适应需求变化,从而导致大部分最后的软件要么是缺陷多多,要么是功能不能满足客户需求,只能留到二期三期来解决。

在敏捷方法学里面,项目团队通过co-location,打破不同部门之间的隔板,让所有的项目成员坐在一起,减少了软件需求在沟通过程中的失真。如果有现场客户,就更是让这种信息失真问题减到最低。

在开发过程中,敏捷团队会使用TDD、简单设计和持续集成等实践来保证软件的简单,让可能存在的问题尽早的暴露出来;再通过小型sign off,来保证开发人员做出的软件是满足分析人员和测试人员的要求。

   3.高的投资回报率(ROI)

作为投资方,客户最关心的莫过于投资回报率(ROI)了。除了前面分析的软件质量和软件成本之外,对ROI有直接影响的因素就是软件生命周期了。软件生命周期是指软件的产生直到报废的整个生命周期。生存周期越长,说明软件能满足用户要求、能被使用的时间周期越长。而软件生命周期又与软件的可维护性和灵活性息息相关。那么,如何保证软件的可维护性和灵活性,就是影响到客户的ROI的关键因素。

在传统软件开发学里面,基本上只有靠一开始的软件设计来提供软件的灵活性。虽然开发过程上游的软件设计的设计方案可能非常优雅,但由于中间过程没有足够的验证和约束,等到软件交付的时候,软件最后实现的可维护性和灵活性完全不能保证。这样,客户就只能祈祷软件的生命周期不要那么短了。

而在敏捷方法学里面,很多的开发实践可以提升软件产品的可维护性和灵活性,比如前面分析的TDD、持续集成和增进设计等等,从而使软件的生命周期得到了极大的延长。因此,这些影响产品生命周期的实践对客户的投资回报率都是有直接或间接的影响。

从上面对三个特定的客户价值的分析可以看出,敏捷方法实践就是以“交付更多客户价值”为出发点提出来的,始终体现了敏捷方法学的价值观:“个体和交互胜过流程和工具,可工作的软件胜过面面俱到的文档,客户合作胜过合同谈判,响应变化胜过遵循计划”,和以 “尽早、持续的交付有价值的软件来使客户满意”原则为首的敏捷原则。

敏捷实施调查

2008年2月,Dr. Dobb’s发起了一次有关敏捷开发技术实施情况的调查,共收到642份数据2。调查结果表明,实施敏捷的风险很低,并且调查结果显示:实施敏捷方法之后,开发生产率得到提高的占82%,软件交付质量得到提升的占77%,而客户满意度得到提升的则占78%。

2008年6月,敏捷工具厂商Versionone也发起一次敏捷开发实施情况的调查3,共收到来自80个国家3061个参与者提交的结果。大部分参与者都是敏捷的技术带头人、教练或是咨询师,他们所处公司中,绝大部分的开发队伍少于100人。他们给出的数据是:实施敏捷方法之后,开发生产率得到提高的占84%,软件交付质量得到提升的占68%,69%的软件项目加快了上市时间,而另外有74%的人认为项目风险降低了。

从上面两次调查报告来看,在实施了敏捷方法学的软件组织里面,绝大部分的组织都认为实施敏捷方法学之后,组织的软件交付能力得到了提升,而且客户的满意度也得到了提高。也就是说,在绝大部分实施敏捷方法学的软件组织里面,敏捷方法学承诺的两个愿景得到了充分的印证。

结论

敏捷方法学承诺的“提升团队交付能力,给客户交付更多商业价值”的确能给项目团队和软件客户带来实实在在的好处。不仅有充足的理由,而且有充足的案例数据来证明这一点。面对商业价值日新月异的经济趋势,传统的软件方法学显得束手无策,既不能满足目前对软件交付商业价值的要求,也极大地扼杀了软件团队的生产力和创造力。与“重”方法学不同,敏捷方法学只是提供了很少的几种推荐实践,然后再提供了遵循的价值和原则。敏捷方法学更希望软件团体在开发过程中,根据客观的实际情况,按照敏捷价值和原则,进行裁剪出适合团队的软件方法。基于此,也是越来越多的软件组织投入的实施敏捷方法学的潮流中来。

1 http://dev.csdn.net/article/12/12726.shtm
2 http://www.ambysoft.com/downloads/surveys/AgileAdoption2008.ppt
3 http://www.versionone.com/pdf/3rdAnnualStateOfAgile_FullDataReport.pdf

一次 Refactoring to Functionality 的实践

关于重构

重构是一种改善已有代码和设计的有效手段,Martin Fowler的著作Refactoring:Improving the Design of Existing Code一书里提出了若干种重构的模式,深刻地影响了众多的开发人员。但如果认为重构只能做到小范围的代码优化,或者设计优化,并视之为无法影响更高层面工作的雕虫小技,那就大错特错了。之后 Joshua Kerievsky 的著作 Refactoring to Patterns则创造性地把重构和模式联系在一起,让人们认识到重构的巨大威力。重构从来不是程序员躲在象牙塔孤芳自赏的技术,也可以对系统的设计开发发挥巨大的作用。

如果说Martin的Refactoring只是深刻地影响了普通开发人员的程序设计和代码编写,Joshua的Refactoring to Patterns则切切实实地给架构设计人员或者tech lead的工作指出了革命性的变化。那么,对于项目的功能开发,重构又意味着什么呢?对于项目经理来讲,应用重构的技术和思想,对整个项目的功能开发是否能带来特别的好处?下面将通过一个例子给大家展示在开发新功能时,对开发的每一步都保持重构的思想,将整体功能的开发分解成若干步骤的“重构”,从而非常简易清晰地完成功能开发。

新功能的提出

某系统中存在 ReferenceFile 类,作为用户向系统上传的文件的抽象,是系统其他地方会使用到的参考文档。在最初的需求中,参考文档与用户上传的文件一一对应,并且用户能指定某些系统特定的属性,比如文档类别、文档管理部门等。于是在最初的设计中,该类的属性包含两部分:一部分是前面提到的系统属性;另一部分是描述文件信息的属性,比如文件名、文件存储路径等。请注意,这里我将“参考文档”与“上传文件”两个概念区分出来,是为了便于下文解释。总的来说,在这个阶段,“参考文档”就是系统对“上传文件”的抽象。

接到这个需求之后,我们使用TDD,很快就驱动设计出该类的业务方法;再使用Acceptance TDD,又对该类的功能的进行了全面的覆盖;最后使用hibernate的O/R Mapping,按照属性和表字段一对一的关系,把该类和数据库的表关联起来。完成UI方面的设计,并把前后台整合在一起。系统上线试运行后用户认为这块很好地契合了需求。

但是,需求总是不断在变的。上线过程中,用户的上级部门提出参考文档应该可以对应到多个上传文件,系统其他地方使用时把其下所有上传的文件作为一个“参考文档”整体来对待。也就是说,对 ReferenceFile 类而言,其中的系统属性仍然是保持一份,但是上传文件的属性则变成多份。概括下来,客户提出的新需求如下:

1. 参考文档可以管理多个上传文件
2. 用户创建或者修改参考文档时,可以同时上传多个文件,并能对已上传的文件进行删除修改
3. 系统在其他地方仍然是针对参考文档来参考引用用户上传的文件
4. 参考文档的预览和展示需要调整成支持多个上传文件

实现过程

该系统是标准的j2ee web 分层系统,包括web UI、controller、service、domain model、dao这几层。本文的重点是如何应用重构开发功能,本文将着重关注于domain层的改动,会包括domain model API的改动,以及domain model 持久化机制的改动。其他层次,比如controller、service等,因为主要是作为domain model的消费者,主要是使用domain model 的public API,故放在一起作为整体来对待,下文将统一称为client 代码。至于最外层的 web UI层,则因为主要是根据系统功能提供交互上的操作和内容展现,而且大部分情况下也会有专门UI设计开发,本文就不涉及了。

另外,系统还包括大量不同层次的测试代码,比如unit test、functional test、integration test和regression test等等。从另外一个角度,测试代码又可以分成2部分:text fixture和test case。test fixture主要是负责测试数据的准备,test case才是测试用例的实现代码。前面提到的测试,除了unit test之外都主要是基于 web UI 模拟用户使用系统功能,test case 主要是针对 web UI 来写,故对于这部分的测试而言,domain model 的修改主要会影响到测试数据的准备。而对于 unit test,又可以根据SUT的不同,分为几个部分:针对model的unit test、针对client(包括controller和service)的unit test。其中,针对model的unit test也只是model API的消费者,也可以视为domain model的client。针对controller和service的unit test,理论上也只针对于SUT的API,对model的API依赖也只是在test fixture那块。所以,根据我们的分析,我们知道测试代码可以简化成两部分,一部分是与controller/service类似的domain model的client,另一部分是使用domain model生成一组aggregation的test fixture。

综上所述,我们把整个功能实现过程中涉及的工作主要归类为:domain model API的改动、domain model持久化机制的修改、domain model client的修改,以及test fixture的修改。现在对于需要做什么事情,就变得清晰了。我们接下来对前面三项工作来分析。

面临的现状

仔细分析我们面临的情况:

1. 文件的相关信息在原始的 ReferenceFile 类里面是作为一对一的属性组存在
2. ReferenceFile 类使用 Hibernate 进行属性字段一对一的持久化
3. ReferenceFile 类以及原功能有 unit test、dao test,以及functional test 覆盖

此时的 ReferenceFile 类是这样的:

public class ReferenceFile {
private String category;
private String fileName;
//相应的 getter/setter,以及业务方法
}

ReferenceFile 类的hibernate映射文件是这样的:

<class name=”ReferenceFile” table=”referenceFiles”>
<id/>
<property name=”category”/>
<property name=”fileName”/>
//
</class>

回头看看在这次功能调整中,我们需要做哪几项任务?其中会涉及哪些方面?

  • domain model 的修改
  • domain model 持久化机制的修改
  • domain model 增加一对多的关系
抽取新类
domain model 的修改

很明显,随着需求的变化,作为一组时时刻刻同时出现而且内聚性非常强的属性,原来记录文件相关信息的属性组,比如文件名、上传路径以及类型等等,以及操作这些属性的方法需要抽取到一个单独的类里面。Martin Fowler 在Refactoring:Improving the Design of Existing Code里面写到“…consider where it can be split…A good sign is that a subste of the data and a subset of the methods seem to go together.”因此,我们决定把这些属性组和方法抽取到一个新类。新的类的职责变成维护上传文件的相关信息,而 ReferenceFile 则化身为一组上传文件的集合,不用操心文件的存储和具体细节,更利于系统其他地方进行引用。

那我们该如何进行演化呢?这里我们可以使用 Martin Fowler在Refactoring书中的“Extract Class”技巧。请大家自行参阅,就不具体讲了。经过这一步,我们现在可以得到这样一个结构:ReferenceFile has an Attachment。 这两个类的代码大概如下:

public class ReferenceFile {
private String category;
private Attachment attachment;
//相应的 getter/setter,以及业务方法
}

public

class Attachnment {
private String fileName;
//相应的 getter/setter,以及业务方法
}

domain model 持久化机制的修改

接下来,我们需要修改 ReferenceFile 的持久化机制。在原始的设计里面,ReferenceFile类的属性一一对应到数据库表中的字段。现在属性被分到了两个对象里面,为了 Hibernate依旧能把这些属性都持久化到一张数据库表里面,我们使用了 Hibernate 提供的 component配置。下面是改动后的配置:

<class name=”ReferenceFile” table=”referenceFiles”>
<id/>

<property name=”category”/>
<component class=”Attachment”>
<property name=”fileName”/>
//
</component>
</class>

运行测试,OK,所有的测试都pass了。至此,我们抽取新类的步骤就完成了。接下来,我们需要完成“一对多”的演化。

公开新类
domain model 的修改

在这里面,我们需要将 ReferenceFile 类里面的 Attachment 类公布出来,直接在client code里面使用这个类。这样,原本属于 Attachment 类的方法就能彻底地从 ReferenceFile 类里面移走,ReferenceFile类只留下必要的业务方法和 Attachment 对象的getter/setter。Martin Fowler在Refactoring:Improving the Design of Existing Code里提到“move methods”,我们采用这种技巧,很容易地把原来与Attachment类相关的业务方法都移到Attachment类里面,ReferenceFile类里面只保留对attachment属性的getter/setter方法。公布Attachment对象之后的结构:

public class ReferenceFile {
private String category;
private Attachment attachment;
//相应的 getter/setter,以及业务方法
}

public

class Attachnment {
private Long id;
private String fileName;
//相应的 getter/setter,以及业务方法
}

domain model 持久化机制的修改

这里,我们就考虑把Attachment单独持久化到自己的数据库表里面了。原来的component就变成了现在一对一关联。改动后的配置如下:

<class name=”ReferenceFile” table=”referenceFiles”>
<
id/>
<property name=”category”/>
<one-to-one name=”attachment” class=”Attachment”/>
//
</class>
<class name=”Attachment” table=”attachments”>
<id/>
<property name=”fileName”/>
</class>
实现类之间的一对多联系
domain model 的修改

到这里,读者就能发现这是一种Kent Beck曾经总结过的“First One, Then Many”情况。关于“First One,Then Many”,Kent Beck曾写了一篇文章介绍如何可靠地拥抱变化,原文链接如下http://www.threeriversinstitute.org /FirstOneThenMany.html。在那篇文章中,Kent的问题是面对未来可能的需求变化,如何使用 Succession 的方式帮助系统架构平滑演化。下面是 Kent 的观点:

Applied to software design, succession refers to creating a design in stages. The first stage is not where you expect to end up, but it provides value. You have to pay the price to progress from stage to stage, but if jumping to the final stage is prohibitively expensive in time or money, or if you don’t know enough to design the “final” stage, or if (as postulated above) the customers don’t know enough to specify the system that would use the final stage, then succession provides a disciplined, reasonably efficient way forward.

那么,在本文的功能开发之中,我们是如何做到的?

  1. 增加字段attachments,以及getter/setter
  2. 修改原来单个Attachment的getter/setter,改成从attachments里面得到首元素或者往里面添加新元素,如getAttachments().get(0)
  3. 运行测试,确保所有测试都通过
  4. inline ReferenceFile类里面的对单个Attachment的getter/setter方法。这里要注意test fixture里面对domain model的aggregation的创建,而且因为涉及对List的操作,所以可能需要修改原来的测试代码和test fixture
  5. 运行测试,确保所有测试都通过

到这里,“一对多”的工作完成之后,ReferenceFile 和 Attachment 类就变成了下文的样子:

public class ReferenceFile {
private String category;
private List<Attachment> attachments;
//相应的 getter/setter,以及业务方法
}

public

class Attachnment {
private Long id;
private String fileName;
//相应的 getter/setter,以及业务方法
}

domain model 持久化机制的修改

为了能实现一对多的实体关系,我们需要引入新的表作为“多”方,并保持“一”方的主键。使用Hibernate提供的one-to-many很容易做到这点,接下来是简单的配置文件:

<class name=”ReferenceFile” table=”referenceFiles”>
<property name=”category”/>
<set name=”attachments” cascade=”all” >
<key column=”id”/>
<one-to-many class=”Attachment”/>
</set>
//
</class>
<class name=”Attachment” table=”attachments”>
<property name=”fileName”/>
</class>

结论

至此,我们就完成了新功能的开发,可以看出整个过程的思路非常明显,而且因为主要是沿着重构的思想一路下来,思路非常清晰。另外,因为重构已经有成熟的IDE支持,我们可以利用到IDE的很多便利,这从另一方面也给我们带来了非常的效率。

从整个过程来看,重构的一些方法和思想,不仅可以让我们对遗留代码进行优化,使之能有利于新功能的开发(比如本文中的抽取新类和公开新类,都是为了下文的“由一到多”的功能开发),而且可以让我们在开发功能的时候能从一个更高的角度来分解功能的开发工作,从而把原本复杂无序的过程简化抽象成一段明确的重构链。那么,重构是否就是开发人员开发软件的领域专属语言呢(refactoring as DSLs to developers’ development)?敬请期待本博关于这点的其他博文。

Beyond OSworkflow

最近结束了一个企业OA系统的项目,客户是一家海洋航运行业的企业,散运业务全球第一。该系统以工作流系统为基础平台,对员工工作进行电子化和规范化,由系统来驱动员工自动、快捷、可管地完成日常工作。在开发过程中,在综合评比多种工作流产品之后,我们选择了开源工作流产品——OSworkflow作为底层工作流引擎支撑,通过扩展OSworkflow的接口把业务系统和工作流引擎完美无缝地集成在一起。本文就是跟大家一起分享osworkflow扩展过程中的经验心得,希望能对其他朋友有帮助。

在我们开发团队介入系统开发过程之前,这个项目已经完成了 quickstart 阶段,留给我们的阶段产品有:系统实现的proposal、一个能体现用户page flow的lo-fi,以及在mingle上的 story list和iteration原始计划。当然,还有项目团队跟客户形成的一个良好的互信基础和沟通渠道。于是,左 proposal,右 lo-fi,客户沟通在中间,我们就开始了系统设计的 inception。inception阶段的目标根据不同项目会有不同。在这个系统里面,我们完成的目标有:确定系统实现使用的技术、针对技术进行spike探讨可行性、结合master story以及比较粗略的story list实现一个系统prototype。好,工作开始了!

为什么选择OSworkflow?
工作流系统其实在理论界已经是研究得非常成熟了,有 WfMC 组织来规范工作流定义语言,而且各种工作流引擎,不仅商业产品,像 JBPM、OSworkflow、ofbiz等开源产品也都很多。这些产品都各有特点,应该如何选择呢?从使用率和文档完整度来看,JBPM 和 OSworkflow 占了上风,我们就从这两者里面选了。JBPM基于UML的状态图和活动图来定义流程,已经加入JBOSS大家庭,但是相对比较重。osworkflow是个非常轻量级的工作流产品,但是自从2006年就停止活动了。不管如何,选择的技术都是要为解决未来系统开发会面临的问题,才算是一个好的可行的选择。

通过分析quickstart阶段的proposal和lo-fi prototype,然后再反复跟客户确认,我们知道了客户在现阶段对系统的要求是:

  1. 只需要处理线性的工作流(未来可能需要增加对分支/合并的支持)
  2. 不需要功能复杂强大的工作流编辑器
  3. 节点流转主要是手工触发,不需要基于规则
  4. 需要能和遗留系统已有的工作流定义集成,而已有工作流定义用 osworkflow 定义

根据客户对系统的要求,我们选择了osworkflow作为最终的候选工作流引擎。因为我们对这几种工作流产品都不是很熟,对它们是否能满足系统要求答案不确定。而从另外一个方面来讲,如果系统足够简单,我们也可以自己实现一个状态机,自己实现对流程节点流转的维护。

为了做出选择,我们又分头对 osworkflow 的配置文件以及技术架构都进行了细致的研究。研究过程和结果就不细说了,网上有很多 OSworkflow 的介绍文章,大家可以参照。OSworkflow 会把一个简单的xml配置文件,转换成它定义的流程描述类(XXXDescriptor,我们也把它们称为osworkflow的models),然后在流转的过程中,将其转换成工作流实例和工作流节点,并通过相应的持久化接口持久化工作流的状态。整个过程用一个简图来表示,就会是:

图1 OSworkflow model

考虑到 OSworkflow 的实现也是非常轻量级,并且结构非常清晰,而且为了提供以后对分支/合并、以及自定义规则等流转形式的支持,我们还是选择了 OSworkflow,而不是自己去实现一个基于状态机的工作流引擎。但是,我们还不能直接把 OSworkflow 放进项目里面,然后直接引用:一方面我们不想让开发团队都纠缠于底层 OSworkflow 的技术细节,另外一方面也是因为 OSworkflow 并不能完全满足客户的系统要求。我们需要扩展!

为什么去扩展?
依然是问答模式,OSworkflow 有哪些方面不能满足我们系统的要求?

1. 系统对各个流程的定义都能有版本跟踪

客户会对流程定义进行修改,但是流程修改的过程中,流程应该继续按照原来的定义进行流转。这样,对于同一份流程,系统会同时存在两个不同的定义,并且都能正常流转。OSworkflow 基于XML文件的流程配置方式在这种情况下就不能满足。

2. 系统对流程定义的修改能实现“hot deploy”

客户对流程定义进行修改后,系统能自动提供给用户新的流程定义,而不需要重启服务器。因为 OSworkflow 是在系统启动的时候,将所有的流程定义都载入内存,在系统运行期不会去检测流程定义的修改。这样,OSworkflow 也不能满足。

3. 相对于系统的要求,OSworkflow 提供的功能还是太复杂了

因为 OSworkflow 是提供一个通用的工作流引擎,所以考虑了很多复杂的情况。这些复杂性对我们系统来讲,都是额外的不能对客户产生价值的特性,而且那些复杂性的存在,也会让我们开发的时候必须考虑到那些方面,从而影响我们的模型设计和开发。

除了上文提到的几个原因,我们考虑的因素还有很多方面,最终我们决定是在OSworkflow基础上进行扩展,来满足我们的要求。

如何进行扩展?

如何进行扩展?我们的原则是什么?没有一个清晰的目标,很可能最终扩展的方向和最终实现就会跟我们想要的目标离得比较远了。

原则一. DDD,MDA

我们希望开发团队在设计开发的过程中只需要考虑系统领域的模型,而不会 involve 到底层的流程流转细节。

“find core domain models”,我们重新研究了系统的 proposal 和 lo-fi prototype,找出系统里面的领域对象,它们之间存在这样的一个关系:

图2 系统model
从图1 OSworkflow model 关系图和图2 系统 model 关系图可以看出,它们两者还是存在着一定的相似点和不同点的。

相同点:
1. 相应 model 完成的功能和职责是非常类似的
2. 不同 model 之见的关系也是非常类似的

不同点:
1. model 的属性
2. model 的一些行为

原则二. decoupling

我们希望项目实现不依赖于某个工作流产品,而是可以很方便地迁移到其他的工作流产品。第一想法就是——“laying, isolate by interface”。

其实工作流产品除了工作流引擎负责流程节点流转之外,都另外包括这三部分:流程定义、定义解析和状态持久化。而成熟的工作流产品,不管是JBPM,还是OSworkflow,都会提供接口进行隔离。在OSworkflow里面,接口WorkflowFactory会负责流程定义的解析和OSworkflow models的初始化,接口WorkflowStore则负责将Workflow和WorkflowEntry的状态持久化。这样,我们就可以定制实现接口,将domain models转换成 OSworkflow models,在流程流转需要持久化流程状态的时候,则会去持久化domain models。

经过这样几个方面的论证和分析,得到的最终方案如下:

  1. 使用domain model,而不是工作流model
  2. 扩展接口将 domain model 转换成工作流 model
  3. 扩展接口提供自定义的工作流状态持久化
  4. 不改动 osworkflow 内核

最终实现

图3 系统实现图

我们得到了什么

  1. 使用 domain model 进行开发
  2. 底层工作流引擎对开发团队透明
  3. 设计、优化工作在应用层次来进行

(本博未来会以同一个项目为例对敏捷项目各个阶段过程进行简单的介绍,敬请期待)

看上去很美 –OOafarian对play!框架的几点看法

 play! framework 是一个面向小型网站开发的 rails-like 的 Java 框架,不仅在目录结构上,在系统 skeleton 生成上,也把 rails 学了个七七八八。最近和同事也在做一个 rails style 的 Java Web 应用开发框架,参考了 rails 的很多 feature,但更多的是按照自己的开发理念和哲学思想“拿来” rails 里面有借鉴意义的思想。面对号称 rails-like 的 play! framework,这几天身在海滩上,于是花了一些时间好好研究了 play! 的源代码一番。“看上去很美”,是读完代码后脑海中第一下涌现出来的想法,我承认我是OOafarian。
 
 play! framework 5个“酷”的东西,看上去很美,但这些大都是建立在对 Java class 文件 hack 的基础上的。以 JPAModel 为例,任何它的子类 model 都是可以使用 rails style 的语法:User.findAll()。表面上看和 activerecord 的语法一样的,但实际上 play! 是通过在 JPAModel 里面定义这些静态方法,默认实现是抛出 RuntimeException,然后在 classloader 载入子类 class 文件的时候,通过增强每个 model class,使之具有这样类型安全的类方法。且不说这样实现是多么暴力和对开发人员不可理解,这与 rails 的实现是完全不一样的。ruby 因为是提供了 class 的继承体系,使得类方法也成为可以 override 的 class 类的实例方法,但是 Java 不提供 class 的继承和覆盖:这是 Java 的优势也是劣势。有人说这是语言的天性,但其实语言也是人们对世界进行抽象建模的一种折射,也反映的是人们的世界观和哲学思想。如果是我,如果依旧选择 Java 来做,我更倾向于保留 Java 的静态方法的优势和优雅,通过模式或者设计方法使之具有类似于动态语言的灵活性。这也是我和几位同事一致的看法。
 
 那么,从一位受 OO 和 pattern 熏陶很深的 Java 程序员看来,看上去很美的 play! framework 华丽的袍子下有多少虱子呢?待我细细数来:
 1. controller 里面充斥的 static void 方法,不 OO,不 testable,完全是过程化的代码。作者也是说了:
 
 The Java code from the Controller class isn’t really Object Oriented : it’s mainly some procedural code.
 
 当然,过程化还是面向对象,这个争论不是今天才有,不同的人在不同的上下文情景下都会选择最适合自己的开发方式。但是,作为受 OO 熏陶很深的我,这是不可接受的。
 
 2. view 的 render 实现是继承自 RuntimeException。记得很早之前,还有人争论 Exception 作为程序出口和条件判断分支的使用,但自《Effective Java》一书以及其他编码风格介绍的书籍出来之后,这样的争论早已不知何处。Exception 和 Error 会有自己适合的场所,也有它们自己的正确含义,被滥用可不是它们的错。我想,即使是 JVM 被优化得性能完全不用考虑了,这样的处理思路也是不可以被团队成员理解、认同和接受的吧?
 
 3. 暗地里修改代码,使用自己的classloader来大量增强或者修改class,甚至反编译 class。这样就导致很多增强的东西都是框架来强制性加入的,开发人员没有办法进行测试,也没有办法修改默认的实现。也许有人会觉得这样也挺好,觉得这样也够用,那也 OK。但是如果这些都建立在抛弃 Java 强大的静态类型安全,强大的 IDE 支持,强大的社区支持的基础之上,我是不能接受。
 
 当然,play! framework 也是体现了一些很新颖的创新。比如使用 Http Server 来取代 J2EE Server,甚至是 Application Server。又比如通过动态编译代码来实现 hot swap。还比如使用 java.lang.instruments API 来定位 Exception 的位置。这些创新在一定程度上减轻了 Java web 应用开发过程中的“重”,给开发人员带来“轻”的感觉。但是,如果这些需要付出 Java 社区积累到几天的成果,完全重头再来,我想我会宁愿选择 ruby on rails。
 
 当我们选择了 Java,我们看重的是强大的静态安全,看重的是强大的 IDE 支持和社区支持。虽然这些会给开发过程带来一定的“重”,但是米兰昆德拉说过,
 
    可是,沉重便真的悲惨,而轻松便真的辉煌吗?
    最沉重的负担压得我们崩塌了,沉没了,将我们钉在地上。可是在每一个时代的爱情诗篇里,女人总渴望压在男人的身躯之下。也许最沉重的负担同时也是一种生活最为充实的象征,负担越沉,我们的生活也就越贴近大地,越趋近真切和实在。
    相反,完全没有负担,人变得比大气还轻,会高高地飞起,离别大地亦即离别真实的生活。他将变得似真非真,运动自由而毫无意义。

 rails 之所以能轻,是因为它有 runit,有 rspec 来作为它的安全网,使之不会偏离方向。而 Java 也可以变得很轻,但这个轻却绝不是以抛弃 Java 的优势作为代价的。这也是我和几位同事在做 rails style 的 Java web 应用开发框架的过程中一直坚持的,希望到时能展现给大家一个高效率而有符合编程哲学的好框架。

XP 与马克思主义趣谈 -读《Extreme Programming Refactored: The Case Against XP》

    前一段时间读了Matt Stephens 与 Doug Rosenberg 合著的《Extreme Programming Refactored: The Case Against XP》(以下简称《Refactored》)。该书虽然是针对 Kent Beck 的《Extreme Programming Explained: Embracing Changes》(以下简称《Explained》)第一版进行阐发,然后 Kent Beck 在《Explained》第二版里面也修正了一些 XP 的理念和态度,但是《Refactored》书中提到的一些见解和看法现在读来还是挺有意思的。
   
    比如作者一开始就讲了个“皇帝的代码”的故事新编,把皇帝比喻成迷信开发方法的客户,然后 XP 者就是两个骗子,利用不具体的过程标准来欺骗现场客户以及审查的其他人,并赢得最后的release,却最终被小孩子揭露出来的故事。不可否认的是作者在其中做了过多的假设和有失偏颇的类比,最终使得故事显得过于荒诞和不切实际,但 XP 在那个急于推广自己的年代用一些过激的宣传词,从而引起这样那样的误解,也是正常的。这也是 Kent Beck 在《Explained》第二版里面修正的主要方面,使得 XP 更具可操作性,概念不显得那么的突兀而容易误解了。
   
    撇开这些不谈,觉得特别有意思的是作者在书中把 XP 和马克思主义来做对比,得出一些共同点,也颇让人若有所思。这里闲话不多讲,直接贴上书中几处 XP 和马克思主义进行对比的地方。
   
    1. XP 里面的代码共有与马克思主义提倡的集体所有制
    Just for fun, we did a little research on the subject of collectivism, outside of an XP context. It came from Marxist “power to the proletariat” sociopolitical theory (more on Marxism later). Here are a couple of interesting quotes that we found:
    Karl Marx — “Since the supreme aim of collectivism is the abolition of that capitalistic regime which enables one man or one corporation arbitrarily to exploit the labour and the necessities of many men, it obviously does not—in theory at least—imply equal compensation for all individuals, nor the destruction of individual initiative, nor the establishment of a bureaucratic despotism.”

    然后引用 Robert C. Martin 的话就是:
    “The only constraint that XP puts on you is that any production code has be [sic] written by a pair. Your preferences and comfort do not supercede the delivery of quality to the project, or your parcitipation [sic] in the team.”
   
    2. XP 里面自组织团队与马克思主义提倡的人民专政
    Karl Mars — Power to the Peeps
   
I was recently interviewing a programmer for a potential contract, and he happened to mention that he had worked on a project in which his team had attempted XP (but found it too difficult for various-reasons—in particular, that management wouldn’t buy in to the new way of working. Eventually they abandoned the “experiment” [which it quickly became known as], keeping unit tests but not much else).

I asked him what he most liked about XP, and he immediately perked up with, “It empowers the programmers! Puts us on an equal footing with the management. . . .”

    可以看到的是,这里面有些确实是 XP 没有解决好的问题,或者 XP 没有明示的东西。万幸的是,XP 不是一个固步自封的软件开发方法学,而是更注重它所强调的价值和原则,比如简单、沟通、勇气和反馈。所以,在实际开发过程中,不同的项目组在不同的项目环境下会裁剪出合适项目、合适组织的方法学。所以,前面两个问题其实在 XP 的框架内也都是可以解决的。
   
    我们来看第一个问题:代码共有导致个人失去成就感和幸福感。其实这个问题的更深层次是:
    1. 敏捷团队如何评价个人performance?
    2. 敏捷团队如何激励个人创造?
   
    其实这个问题也是我们在做培训的时候,学员们问得最多的问题。在传统的文档式开发过程中,开发人员的工作量被量化成代码行数或者 bug 修正数,虽然大家都知道这其实并不能反映真正的工作量,但毕竟可以作为他们衡量开发团队成员的 performance 的一个参考值。而 XP 一方面宣扬 pair programming 以及代码共有,另一方面又宣扬增进设计和重构,这样统计个人的代码行数或者修改的 bug 数对于绩效考核就没有任何帮助。那么,在敏捷里面是怎么解决这个问题的呢?
   
    我想,在检查个人performance在项目组内部通常都会有自己的办法,符合公司文化和价值的评审方法。以我们公司为例,通常是由每个人找4-5个项目同事来给他写 feedback,提供对这个人在项目里面表现的反馈。因为 XP 提倡勇气和反馈,所以,每个人也是会基于一个比较客观的态度来对同事进行评价。这样,从项目同事的反馈里面,就能得到比较全面的个人 performance 信息了。
   
    如何激励个人创造?对,因为 XP 提倡简单设计,通过重构来增进设计,所以在设计里面考虑更多的是设计是不是满足当前的业务需求,是不是容易让项目组其他成员所理解。但是,这并不意味着敏捷就不允许引入新技术,不允许进行大范围设计。这里不举我们公司的实践,在《透析敏捷编程》一书中提到了在项目组中使用“金卡”实践来激励个人创造,就是允许项目成员申请金卡来进行新技术研究或者大范围设计。因为 XP 提倡沟通和勇气,所以只要开发人员有具体理由,并且项目组同意个人在某方面花费时间精力,那就是应该尊重和允许的。

    所以,从上面来看,XP 并不会导致个人失去成就感,重点在于项目组是否足够 XP,足够敏捷来让每个人都达到自己的幸福感和成就感。如果没有,建议通过团队 retropective 来找出有效的改进方法。
   
    我们再来看第二个问题:在项目组内,程序人员地位不比项目管理人员差。其实这个问题的更深层次问题是:
    1. 程序人员应该听从管理人员的指挥,怎么能提出异议呢?
    2. 如果程序人员有自己的想法,那管理还能进行下去么?
   
    那么,我想大部分拥有这个疑虑的人应该是传统的项目经理。但是自从在 javaeye 里面看了一篇把开发团队比喻成正规军的比喻之后,我发现有些开发人员也倾向于接收管理人员的命令,而不是更积极主动地提供自己的建议,去和管理人员进行更有效的沟通。其实,我们在做培训的时候,和一些项目经理也谈过这方面的问题。开发人员不善于与他们沟通,导致他们对开发团队的真实状态不清楚,他们觉得非常不放心,于是反过来进一步要求开发团队提交更多的文档来反映他们的状态。这样,开发团队文档任务增重了,怨声载道,项目经理就更得不到项目团队的真实状态了。于是,恶性循环就形成了。

    在开发过程中,随时都有可能会发生影响项目交付的问题,比如原来估算的工作量有偏差、客户对某一处需求提出了修改等等。很多事情项目经理是不可能事无巨细都会察觉的。这些问题在传统软件方法学里面都是作为风险来管理的,于是项目经理在预防各种各样的风险中忙得焦头烂额,“兵马未动,粮草先行”,不到最后一刻不敢让开发人员进行开发。因为一旦进行开发,而开发人员不积极主动反馈项目状态的话,项目经理对于项目就是完全失控了。这是项目经理无论如何不敢面对的。可是,“防民之口,甚于防川”,项目开发过程的风险,是能完全堵死的么?如果开发人员可以有机会,也无风险地发表自己的看法,项目管理就可以是更轻松了的。而且,一个人的智慧总是有限的,当客户逼着你做决定的时候,你不希望项目团队的人能站出来,助你一臂之力么?把全部责任都一肩扛,只会对项目不利,而谈不上有益。从另一个方面讲,每个人都是希望有一个上升的职业生涯,希望能不断提升自己。软件组织也是希望能不断涌现表现优秀的员工,从而有利于组织的成长。那为什么要打压开发人员积极参与管理的兴趣和付出,最终影响组织的成长呢?
   
    在培训中,大多数项目经理也能认识到项目团队沟通的重要性,但对怎么建设这样的团队仍然显得方法不多。XP 作为充分照顾了人性的方法学,也是提出很多原则允许或者鼓励开发人员积极沟通,表达自己的想法。所以,运用 XP,建设好简单、勇气、沟通和反馈的良好氛围的团队,不仅对于每个团队成员,对于项目经理,对于其他管理人员都是有百益而无一害的。为什么不尝试一下呢?
   
    马克思主义说过“集体所有制”、“人民专政”,毛泽东思想也说过“实事求是”,邓小平理论更是说“不管白猫黑猫,抓到耗子就是好猫”。在《Refactored》提到的一些误解,其实都不能作为反对实施敏捷的理由。我想,如果果真是阻力重重,或者不知道如何开头,或许可以考虑请求于一些专门提供敏捷咨询的第三方组织了。

play! framework hot swap 浅析

play! 最大的卖点就在于 hot swap,正如它自己宣称的:
reach your maximum productivity。play! 允许开发人员修改java文件,保存,然后刷新浏览器,立马可以看到效果。不需要编译,也不需要重启服务器。
Java 要想实现动态更新 class 文件,不外乎两种手段:替换 classloader、替换 JVM。因为替换 JVM 引起的开销更大,需要维护 JVM 的堆、栈等运行信息,所以 hot swap 通常是选择替换 classloader。比如 grails 里面就是选择替换 classloader,它会自己维护一个线程,定期轮询源文件是否发生修改,以替换原来的 classloader。那么 play! 宣称的 hot swap 又是怎么实现的呢?
让我们来看看play! 的内部流程:
1. play! 使用了 Apache Mina 作为底层的 http server,然后使用了自己关于 Mina IoHandler 接口的实现—— HttpHandler
2. 当浏览器发起一个 request:
2.1 Mina Server 生成一个 Mina Request,转发给 HttpHandler 的 messageReceived 方法
2.2 play! 解析 Mina Request 和 Mina Session,包装成自己的 Request 对象

Request request = parseRequest(minaRequest, session);

2.3 play! 检测 Route 文件修改情况,根据 Route 配置信息将 Route/Action 的信息赋给 Request 对象

Router.detectChanges();
Router.route(request);

2.4 play! 根据当前配置的开发模式来采用不同的策略调用 Action 来理 Request

if (Play.mode == Play.Mode.DEV) {
Invoker.invokeInThread(
new MinaInvocation(session, minaRequest, minaResponse, request, response));
}
else {
Invoker.invoke(
new MinaInvocation(session, minaRequest, minaResponse, request, response));
}

2.5 如果 play! 当前是 DEV 模式,invokeInThread方法会让 invocation 对象代理 run() 方法

public void run() {
try {
before();
execute();
after();
}
catch (Throwable e) {
onException(e);
}
finally {
_finally();
}
}

咱们来看看 before() 方法:

public static void before() {
Thread.currentThread().setContextClassLoader(Play.classloader);
if(!Play.id.equals(test)) {
Play.detectChanges();
if (!Play.started) {
Play.start();
}
}
//
}

在 Play 类的 detectChanges() 方法里面,有这么一句:


classloader.detectChanges();

哈哈,play! 修改源文件后,刷新浏览器即见效的奥秘就在这里了。再进去看看 play! 自定义 classloader 的 detectChanges() 方法:

public void detectChanges() {
// Now check for file modification
List<ApplicationClass> modifieds = new ArrayList<ApplicationClass>();
for (ApplicationClass applicationClass : Play.classes.all()) {
if (applicationClass.timestamp < applicationClass.javaFile.lastModified()) {
applicationClass.refresh();
modifieds.add(applicationClass);
}
}
List
<ClassDefinition> newDefinitions = new ArrayList<ClassDefinition>();
Map
<Class, Integer> annotationsHashes = new HashMap<Class, Integer>();
for (ApplicationClass applicationClass : modifieds) {
annotationsHashes.put(applicationClass.javaClass, computeAnnotationsHash(applicationClass.javaClass));
if (applicationClass.compile() == null) {
Play.classes.classes.remove(applicationClass.name);
}
else {
applicationClass.enhance();
BytecodeCache.cacheBytecode(applicationClass.enhancedByteCode, applicationClass.name, applicationClass.javaSource);
newDefinitions.add(
new ClassDefinition(applicationClass.javaClass, applicationClass.enhancedByteCode));
}
}
try {
HotswapAgent.reload(newDefinitions.toArray(
new ClassDefinition[newDefinitions.size()]));
}
catch (ClassNotFoundException e) {
throw new UnexpectedException(e);
}
catch (UnmodifiableClassException e) {
throw new UnexpectedException(e);
}
// Check new annotations
for (Class clazz : annotationsHashes.keySet()) {
if (annotationsHashes.get(clazz) != computeAnnotationsHash(clazz)) {
throw new RuntimeException(Annotations change !);
}
}
// Now check if there is new classes or removed classes
int hash = computePathHash();
if (hash != this.pathHash) {
// Remove class for deleted files !!
for (ApplicationClass applicationClass : Play.classes.all()) {
if (!applicationClass.javaFile.exists()) {
Play.classes.classes.remove(applicationClass.name);
}
if(applicationClass.name.contains($)) {
Play.classes.classes.remove(applicationClass.name);
}
}
throw new RuntimeException(Path has changed);
}
}

HotswapAgent类的 reload 方法如下:


public static void reload(ClassDefinition definitions) throws UnmodifiableClassException, ClassNotFoundException {
instrumentation.redefineClasses(definitions);
}

读到这里,也就弄清楚了 play! 怎么实现 hot swap 的原理了,还是调用java.lang.instrument目录下的类和方法来实现的 hot swap。不存在魔法,play! 还是选择了替换 classloader,只不过这个替换动作发生在处理 http request 的时候,于是开发人员用起来就是“刷新浏览器就可以看见效果了”。