看板和利特尔法则

利特尔法则(Little’s Law)作为一个非常朴素的原理,为看板方法奠定了一个理论基础,看似简单的公式背后却有其复杂的一面。

一、利特尔法则

利特尔法则的公式是这样的:

平均吞吐率=在制品数量/平均前置时间

举个例子,假设你正在排队买快餐,在你前面有19个人在排队,你是第20个,已知收银窗口每分钟能处理一个人的点餐需求,求解你的等待时间。

如果你已经决定要排队,并且站到了队尾,那么在制品数量就是20(个),平均吞吐率是1(人/分钟)。
从你站到队尾的时候开始,一直到你点完餐,这个时间就是你的“前置时间”。
即使我们没有学习过利特尔法则,也可以轻易地算出来:

    1 = 20 / x
    x = 20(分钟)

因为在一段时间之内,保持工作量饱满的话,我们每天能做多少工作基本是一定的,所以吞吐率基本上不会发生太大变化。
如果这个时候我们想缩短平均前置时间,也就是等待的时间,利特尔法则告诉我们:可以通过减少在制品数量来达成这个目标。
在这个例子中,就是减少排队者的数量。

这也很好理解,10个人的队列和20个人的队列,前者需要等待的时间会更短。 [1]

二、限制在制品的意义

如上面所说,在制品数量和前置时间是成正比的,缩短前置时间的最有效手段就是减少在制品数量。

前置时间的增长会导致交付周期变长,这一点基本毋庸置疑。

前置时间的增长会导致交付的可预测性下降,俗话说“夜长梦多”,长时间停留在某一个阶段会带来一些额外的风险。

如果我们的交付周期比需求变化周期更长,那么会有更多的紧急任务,所以交付周期变长会导致更多的紧急任务。

如果我们管理不好紧急任务的插入,会增大我们的在制品数量。

如果交付团队的可预测性很低的话,那么会影响到IT研发组织和业务部门的信任关系,当业务部门无法预测一个需求提交给研发部门什么时候能交付的时候,那么唯一可行的手段就是一次性把要做的事情全部都压给研发部门,直接增大了研发部门的在制品数量。

同时在制品数量的增长会带来的另外一个后果就是故障发现得很晚,这一点在过去三四十年的软件工程方法论中都得到了验证。

发现的故障需要资源和时间来进行修复,带来的就是在制品数量的上升和前置时间的增长。

以上所有的事情我们放到同一张图中,可以看到下面的情况,实线表示两者之间存在因果关系,同时还是正比的,因增大,果也会增大。虚线表示两者之间存在的因果关系是反比的,因增大,果会减小。

22484-4c2d36ae3b6f766f

因果回路图

而在这众多因素之中,只有在制品数量是我们能够最有效的直接加以干预的。
而只有前置时间我们是可以直接观测的。

就像我们正在开车一样,我们踩油门的时候,速度表会发生变化,从60迈到100迈,但我们真正关心的并不是仪表盘的变化,而是真正汽车行驶的速度。

所以我们采用控制在制品数量的手段,通过观测前置时间的变化来观察我们的改进是否有效,但更重要的是整个系统是否正在向着更好的方向迈进。

三、在制品数量是不是越低越好

我们用直觉感受一下,在制品数量如果越低越好,那限制到1怎么样?限制到0怎么样呢?

很显然,在制品数量如果过低的话,团队成员可能会产生空闲的现象,很大一部分产能会被浪费掉,那在制品数量限制到多少是最合适的呢?

我们都知道,一个任务如果2周能完成,两件任务串行,需要的时间是4周,但两件任务并行,绝不是2周能完成,有可能5周的时间都完成不了,所以直觉上在制品数量过高也不能带来产能的上升。

所以一个朴素的原则就是:
团队中每个人在任意时刻,手中只有一件事的时候,效率是最高的。团队的在制品数量低于这个值,会造成产能的浪费,如果高于这个值,会造成前置时间的变长。

那我们再用定量的方式模拟一下吧。

假设我们有一个三阶段的开发流程:分析、开发、测试,平均每张卡片需要4天时间分析、5天时间开发、6天时间测试。

为了简化计算,我们把分析、开发、测试三个阶段设一个总的在制品数量限制。 [2]

22484-5996f0d58c826265

模拟看板

当我们有1个分析人员、1个开发人员、1个测试人员的时候,会得到下面这个结果:

WIP平均前置时间平均产能1150.072150.133180.164240.165290.166350.167410.168470.169520.1610580.16

22484-b95912514fb62b9e

折线图这个实验可以重复,有兴趣的同学可以自己写代码重复一下。

结论

  1. 减少在制品数量可以缩短前置时间,但前置时间的缩短是有极限的,就像我们不可能让10个妈妈在1个月之内完成怀孕的全过程一样。
  2. 增加在制品数量可以提升平均产能,但平均产能的提升是有极限的,1个人每天8小时的产能再想提升只有加班加点。
  3. 最短的前置时间和最大的平均产能不可并存,在“平均每人手头有一件事”的时候,在制品数量稍微小一点,可以达到最短的前置时间,在制品数量稍微大一点,可以达到最大的产能。至于各个组织如何选择,看自己的需求了。

[1]: 道理看似很简单,但放在软件研发领域就变得非常复杂,我们需要平衡需求和产能之间的关系,控制队列长度实际上就是在控制期望和承诺。在尊重事实的前提下,我们尽量让队列的长度变短,不去承诺不切实际的东西。

[2]: 所谓“排队”,实际上软件开发真正的“收银窗口”就是最终交付的环节,在这个环节之前的所有需求其实都是一个队列,队列的末尾就是我们最近承诺的一个需求。所以一旦我们承诺了无法立刻着手的需求,那么就会产生极大的浪费。

Share

实战:持续交付中的业务分析

在需要频繁交付、不断收集用户反馈、拥抱变化、追求业务敏捷的项目中,软件的开发和交付是迭代式进行的。在这样的项目团队中,BA(业务分析师)通常需要在一个开发迭代开始之前完成该迭代开发任务的分析。但在特殊情况下,从收集客户需求到将功能细节传达给开发团队的周期会缩短到一至两天。BA可以用于思考和分析的时间远远少于可以预先做出所有设计的瀑布式项目。

那么在这样的敏捷项目中,BA如何能够适应这种交付模式,完成高质量的业务分析,协同团队为客户交付高价值的软件呢?

项目背景

ABC公司是一家知名的国际性会计师事务所,业务规模庞大,分支机构遍布全球170多个国家。

ThoughtWorks受邀对其“全球派遣服务(International Assignment Service)”业务部门提供IT解决方案,以及软件系统的开发。该系统包括收集其客户的全球派遣雇员的报税数据,以及管理ABC公司税务咨询师对这些数据的进行审核、汇算和出具报告的业务流程;逐步替换其目前已远远不能满足业务和性能需求的遗留系统。

该系统主要有两类用户,一类是ABC公司客户方被派往不同国家工作的雇员(以下简称Mary),这些雇员使用该系统填入报税需要的数据。另一类用户是ABC公司的税务咨询师(以下简称Kim),负责审核、处理Mary提交的数据。

BA在该项目中面临的主要挑战

  • 该项目为分布式开发,ABC公司的决策方在美国,而ThoughtWorks的开发团队在中国,沟通反馈周期有时较长。
  • 由于ABC公司对用户体验的重视,需要频繁交付软件,以便收集用户反馈并及时调整解决方案和后续开发计划。这大大缩短了从收集需求、开始分析到进入开发的周期,增加了分析中出现缺陷的风险。
  • 当开发过程中发现问题时,无法马上与客户取得沟通,开发进度可能会受到影响。

识别业务价值

业务分析的重要性在于首先做正确的事情。理解客户的业务,关注需求背后的价值可以帮助项目团队在软件的设计方面做出正确的选择。

而我们面临的困难是,客户提出的需求,往往都是直接的软件功能,而不是需要解决的业务问题。如果BA只专注于针对客户需要的功能进行系统分析,就丧失了帮助客户优化解决方案以及改进业务流程的机会。

如何寻找业务价值?

以敏捷开发方法中的用户故事为例,找出客户要解决的业务问题的一个简单办法是,用以下方式概括每个用户故事的内容:

As…(角色),I want to…(完成什么样的功能),So that…(解决什么问题,带来什么价值)

“So that…”说明了该故事的业务价值,即要解决的业务问题。准确的寻找业务价值将有利于我们设计出最适合的“I want to”,这很可能优于客户直接提出的功能要求。

需要注意的是,不要把解决方案或功能当成该用户故事的价值。以ABC公司业务系统中的一个用户故事为例,BA对该需求业务价值的了解程度将直接影响到解决方案的优劣。

作为(As…) 我想要(I want to…) 以便(So that…) 是否阐明了价值?
Mary 即时浏览我的行程统计数据 了解我在各个国家或地区停留的时间以及从事的活动
Mary 即时浏览我的行程统计数据 我可以迅速地检查我所输入的在各国家或地区停留时间及从事活动的数据是否正确(以保证我可以依照法律要求提交准确的报税数据)

在该用户故事的两种不同表述中,由于第一种表述只说明了需要的功能,没有说明业务价值,在功能设计时,我们可能会将“行程统计数据”的内容设计的过于详细而造成浪费,使用户不明白此功能的意图。而第二种表述的业务目标就非常明确,可以帮助我们更加容易地设计出适合的解决方案。

此外,BA在了解客户的业务问题时,最好请客户提供一些真实案例/场景来证实其观点并加深自己的理解。

避免分析错误

在实际工作中,我们发现有以下两个方面的分析工作容易被BA忽略,而做出错误的决定。

    1. 客户要求实现某些现有业务流程或遗留系统的功能

例如,客户需求的功能,是当前遗留系统中已经使用多年、且未收到过任何抱怨的功能。所以客户和BA往往认为这个功能是合理的,忽略了深入的分析和思考。而这种思考不全面而做出的决定可能会与可以预见的新功能产生冲突。

在ABC公司的遗留系统中,用来收集报税数据的问卷内容是通过excel表来维护的,而Mary在前台也是通过下载excel问卷,填写完毕后再上传。

在新开发的系统中,问卷被改为在线方式,并辅助以其他必要功能提升Mary的用户体验和满意度。但由于客户方的员工都是财务背景出身,非常喜欢使用excel表,而之前用excel表维护问卷内容也被证明是非常有效的,所以客户坚持在新系统中延用这种方式。经过仔细的分析,我们发现在针对提高Mary用户体验的新功能上线后,使用excel表维护问卷内容将大大增加维护的工作量及错误率,而这与项目的相关目标背道而驰。ThoughtWorks在列举了问题的细节后,说服客户采用了新的解决方案。

    1. 客户要求利用新的IT系统改变当前的业务流程

客户发现目前的业务流程有不合理的地方,希望在新的IT系统里直接改变这些流程。如果不经过仔细的分析,这种做法可能会很危险,业务流程的盲目改变可能会对一部分用户造成麻烦,为客户实施该软件形成强大阻力。那么了解清楚目前这些流程存在的价值和原因事关重要,从而可以帮助我们为客户提供科学的、逐步优化其流程的IT解决方案。

在ABC公司的业务流程中,Kim和Mary之间的一些交流是通过邮件来完成的。这里存在两个业务风险:1)Kim和Mary交流的重要信息被散落在各自的邮件里,系统无法记录,在遇到法律问题时,难以划分责任;2)Kim和Mary可能会使用邮件发送一些保密性较强的内容,如果发错,后果不堪设想。

在开发新系统时,客户要求我们增加了一个消息功能,使Kim和Mary之间的交流可以方便地在系统内部完成。该功能上线后,很好地化解了这两个业务风险,同时收到了Mary这类用户的良好反馈。然而这对该会计师事务所在某些国家分支机构里的Kim这类用户的工作却带来了不小的影响。由于之前使用邮件系统,Kim可以将Mary的邮件转发给相关的同事,并利用邮件丰富的功能进行结果的跟踪。而新上线的消息功能达不到邮件的所有要求,所以增加了他们的工作难度。此外,由于Mary对这个功能的青睐,发送消息的数量远远超过了在使用遗留系统时发送邮件的数量,超过了客户想提高Mary的满意度而在短期内所能承受的代价。

在遇到以上问题时,我们与客户一同分析,提出了折中的解决方案,花费了较少的代价将消息系统和客户的邮件进行集成,同时帮助客户制定了对此项业务流程改进和配套IT解决方案的蓝图。

理清需求优先级

在频繁上线的项目中,其中一个重要的实践是确定需求的优先级,使得重要的功能能够先被开发出来投入使用以便及时收集用户反馈。一般的做法是要求客户排好需求优先级,然后与项目相关成员一同制订迭代开发和上线计划。但由于客户决策方所处角色以及思维角度的局限性,对优先级的评定可能存在盲目。建议BA参照以下价值维度帮助客户对优先级进行评定。

从客户价值维度分析需求优先级

需求价值维度分析图

价值维度 说明
愿景目标 该功能点是否契合项目的愿景和业务目标?与项目目标的契合程度越高者优先级越高
时间限制 客户的业务是否有一定的时间表?如果该功能点必须在某时间点前投入使用,则该需求必须被排入相应时间的发布计划中
市场卖点 该功能点是否是吸引特定目标用户的卖点?如果客户的资金存在问题或者需要市场的快速认可,则可以考虑将该需求列为高优先级
有无替代方案 该功能点有无方便的替代方案?如果有简单易行的替代方案,则该需求的优先级较低
客户内部政治因素 该功能点是否存在客户内部的政治因素?例如某功能只对小部分用户提供价值,但会决定客户内部某个重要组织对这个项目的投资和评价,则可以考虑将该需求列为高优先级
投资收益 该功能点的开发成本和客户所能获得的收益是否匹配?例如客户某工作流程浪费了一个小组人员大量时间,但对其他部门或工作环节无影响。如果开发相应的软件功能造成的投入大于客户在一定时期内可以节省的资金,则该需求的优先级较低
技术依赖性 其他需求是否依赖于该功能点?如果依赖于这个功能点的需求优先级高,那么该功能的优先级应更高

技术风险对优先级的影响

除了来自客户方面的决定因素,我们还应考虑技术实现方面的影响。如果一些技术风险较高的功能可以先进入开发阶段,则问题会尽早地被暴露。开发人员在项目早期解决这些问题会有利于开发成本的节约。所以除以上客户价值维度外,应再参考以下矩阵来权衡需求的优先级。

需求优先级矩阵

客户价值维度和需求优先级矩阵并不是优先级高低的计算器,而是与客户以及团队沟通交流的工具。不同项目的影响维度也会有所不同。由于各项因素的复杂性,客户价值维度和技术风险因素需综合考虑,不可以权重来计算。BA可以与客户对以上因素的内容达成一致,使得客户在评定需求优先级时可以快速、准确地做出判断。同时,通过对价值维度的分析,我们将有机会清晰地了解到功能优先级高或低的原因,以便我们能够准确地编制项目开发和上线计划,并合理地划分用户故事范围。

借助价值维度分析,管理客户期望值

有些客户的决策人可能会依据自己的喜好划分优先级,这对于项目能够按目标成功交付造成一定的风险。此外,客户在功能的设计和验收阶段也容易对单个功能追求完美,造成额外工作量,增加项目范围。而这部分额外工作可能并不合理或者价值较低。长期如此,团队在开发过程中将逐渐偏离项目目标。如果能借助优先级维度对这些额外需求进行分析,则可以提供更有说服力的依据,帮助客户做出正确决定,达成BA和项目经理对客户期望值的有效管理,从而降低交付风险。

发挥团队其他成员在业务分析中的作用

在频繁交付的项目中,如果BA独自承担业务分析工作,难免会出现疏漏。ThoughtWorks曾与ABC公司的IT部门合作完成其业务系统的一些集成工作。在合作过程中发现,ABC公司IT部门的开发人员在业务分析中参与度很低,由此造成了如下问题:

  1. BA需要写大量需求文档,故从需求分析到软件交付的周期较长
  2. 设计缺陷的发现滞后
  3. 在需要频繁交付的情况下,解决方案质量较差,方案优化能力较弱

而ThoughtWorks的开发人员由于在业务分析中的参与度较高,则有效地避免了以上问题。

开发人员如何参与分析

开发人员是软件功能的实现人员,对方案的实现工作量有较准确的估计。在明确项目目标或业务问题后,BA如果能够和开发人员一同分析解决方案,将更有效地为客户找到兼顾成本和效果的方案。

在收集到客户需求后,BA可根据业务价值对需求进行分析,判断客户提出的功能或解决方案是否能很好地满足该业务价值或要解决的业务问题;或者按照自己的理解设计出满足该业务价值的功能或解决方案。

完成上述工作之后,BA应与开发人员就需求和业务价值进行充分沟通,验证功能实现的可行性,同时积极探寻更优方法。如果开发人员提出符合业务价值的不同方案,BA则可以要求开发人员提供一些关于开发工作量、方案优劣、技术风险方面的比较数据,从而帮助自己有效地与客户沟通并挑选最佳方案。甚至可以根据分析结果帮助客户调整该需求的优先级。对于技术难度和风险较高的功能点,建议邀请资深开发人员参与讨论。

与开发人员沟通中遇到的挑战与解决方法

由于上述方法需要与开发人员大量沟通,有些BA在应用以上实践时也遇到了以下挑战。

    1. 开发人员缺少参与业务分析的热情

在ThoughtWorks,大多数开发人员都喜欢积极思考、主动为业务分析提供帮助,大大减少了需求分析上的漏洞。然而在ABC公司的IT部门中,开发人员很少主动为业务分析出谋划策,尤其是团队中资历较浅的成员,甚至不愿意参与解决方案的讨论。团队成员的优势没有得到充分发挥,开发人员只管按需求埋头苦干,结果功能和解决方案的问题往往在测试或者验收阶段才暴露出来,不可避免地造成了浪费。

站在开发人员成长的角度,从ThoughtWorks实践来看,积极地理解业务、思考解决方案能够更快地提高技术能力。故此BA可以找出一些实际案例,协同项目经理与团队各成员进行沟通,鼓励大家积极参与业务分析,逐步形成开发人员与BA协作的良好氛围。

    1. 开发人员容易就客户需求或解决方案产生争论

开发人员在积极参于分析的过程中,有时会对软件功能的价值吹毛求疵,在细节上与BA产生较多争论,使BA在应付开发人员的问题以及与客户求证答案之间疲于奔命。

解决此类问题,可采取以下方法:

  • BA在收集需求时,尽可能充分地了解客户要解决的业务问题,以便能够快速回答开发人员的问题
  • 面对开发人员对解决方案的质疑时,应保持良好的心态,清楚地了解开发人员顾虑的问题和原因
  • 如果自己掌握的信息确实不能证明现行方案的合理性时,协同开发人员,找到更优方案并与现行方案进行优缺点比较
  • 将新旧方案与客户沟通,则可快速帮助客户做出判断

不要忽略测试人员在业务分析中的贡献

由于测试人员所处角度和对细节的关注,往往可以发现一些功能细节的设计漏洞。所以在用户故事进入开发前,BA与质量保证人员对相关业务价值进行充分沟通,会在功能进入开发之前为BA创造更正设计缺陷的机会。

做为质量保证人员,如果充分了解功能背后的业务价值,相对于只了解功能需求,将可以写出更加完善的测试用例,提高测试覆盖率。这会为交付高质量的软件把好最后一道关。

结语

业务分析是困难的,特别是我们面对未知领域的时候。如果只是简单地按照客户的具体需求进行软件开发,那么我们交付给客户的价值将非常有限。然而识别业务价值、帮助客户分析需求优先级、保障团队协作,将有效提升团队对软件的设计能力,解决客户真正的业务问题,交付更大价值。

作为一名业务分析人员,当您在尝试以上实践时,可能会发现自己对客户业务的理解变得更加深刻。在与客户的沟通中,也能够更加容易地提出有价值的问题以及建议,从而提升客户对项目团队的信任,为成功交付项目打下良好基础。

*注:“客户价值维度”的概念由ThoughtWorks咨询师李光磊提出。在此对李光磊表示感谢。


Share

软件系统的稳定性

软件系统的稳定性,主要决定于整体的系统架构设计,然而也不可忽略编程的细节,正所谓“千里之堤,溃于蚁穴”,一旦考虑不周,看似无关紧要的代码片段可能会带来整体软件系统的崩溃。这正是我阅读Release It!的直接感受。究其原因,一方面是程序员对代码质量的追求不够,在项目进度的压力下,只考虑了功能实现,而不用过多的追求质量属性;第二则是对编程语言的正确编码方式不够了解,不知如何有效而正确的编码;第三则是知识量的不足,在编程时没有意识到实现会对哪些因素造成影响。

例如在Release It!一书中,给出了如下的Java代码片段:

package com.example.cf.flightsearch; 
//... 
public class FlightSearch implements SessionBean {
    private MonitoredDataSource connectionPool;
    public List lookupByCity(. . .) throws SQLException, RemoteException { 
        Connection conn = null; 
        Statement stmt = null;
        try { 
            conn = connectionPool.getConnection(); 
            stmt = conn.createStatement();

            // Do the lookup logic
            // return a list of results
        } finally { 
            if (stmt != null) {
                stmt.close();
            }
            if (conn != null) { 
                conn.close();
            }
        }
    }
}

正是这一小段代码,是造成Airline系统崩溃的罪魁祸首。程序员充分地考虑了资源的释放,但在这段代码中他却没有对多个资源的释放给予足够的重视,而是以释放单资源的做法去处理多资源。在finally语句块中,如果释放Statement资源的操作失败了,就可能抛出异常,因为在finally中并没有捕获这种异常,就会导致后面的conn.close()语句没有执行,从而导致Connection资源未能及时释放。最终导致连接池中存放了大量未能及时释放的Connection资源,却不能得到使用,直到连接池满。当后续请求lookupByCity()时,就会在调用connectionPool.getConnection()方法时被阻塞。这些被阻塞的请求会越来越多,最后导致资源耗尽,整个系统崩溃。

Release It!的作者对Java中同步方法的使用也提出了警告。同步方法虽然可以较好地解决并发问题,在一定程度上可以避免出现资源抢占、竟态条件和死锁的情况。但它的一个副作用同步锁可能导致线程阻塞。这就要求同步方法的执行时间不能太长。此外,Java的接口方法是不能标记synchronized关键字。当我们在调用封装好的第三方API时,基于“面向接口设计”的原理,可能调用者只知道公开的接口方法,却不知道实现类事实上将其实现为同步方法,这种未知性就可能存在隐患。

假设有这样的一个接口:

public interface GlobalObjectCache {
    public Object get(String id);
}

如果接口方法get()的实现如下:

public synchronized Object get(String id){
    Object obj = items.get(id); 
    if(obj == null) {
        obj = create(id); 
        items.put(id, obj);
    } 
    return obj;
}

protected Object create(String id) {
    //...
}

这段代码很简单,当调用者试图根据id获得目标对象时,首先会在Cache中寻找,如果有就直接返回;否则通过create()方法获得目标对象,然后再将它存储到Cache中。create()方法是该类定义的一个非final方法,它执行了DB的查询功能。现在,假设使用该类的用户对它进行了扩展,例如定义RemoteAvailabilityCache类派生该类,并重写create()方法,将原来的本地调用改为远程调用。问题出现了。由于采用create()方法是远程调用,当服务端比较繁忙时,发出的远程调用请求可能会被阻塞。由于get()方法是同步方法,在方法体内,每次只能有一个线程访问它,直到方法执行完毕释放锁。现在create()方法被阻塞,就会导致其他试图调用RemoteAvailabilityCache对象的get()方法的线程随之而被阻塞。进而可能导致系统崩溃。

当然,我们可以认为这种扩展本身是不合理的。但从设计的角度来看,它并没有违背Liskove替换原则。从接口的角度看,它的行为也没有发生任何改变,仅仅是实现发生了变化。如果不是同步方法,则一个调用线程的阻塞并不会影响到其他调用线程,问题就可以避免了。当然,这里的同步方法本身是合理的,因为只有采取同步的方式才能保证对Cache的读取是支持并发的。书中给出这个例子,无非是要说明同步方法潜在的危险,提示我们在编写代码时,需要考虑周全。

本文原文出自:http://zhangyi.farbox.com/post/stable-software-system

Share

架构腐化之谜

前言

新技术层出不穷。过去十年时间里,我们经历了许多激动人心的新技术,包括那些新的框架、语言、平台、编程模型等等。这些新技术极大地改善了开发人员的工作环境,缩短了产品和项目的面世时间。然而作为在软件行业第一线工作多年的从业者,我们却不得不面对一个现实,那就是当初采用新技术的乐趣随着项目周期的增长而迅速减少。无论当初的选择多么光鲜,半年、一年之后,只要这个项目依然活跃,业务在扩张——越来越多的功能需要加入,一些公共的问题就会逐渐显露出来。构建过慢,完成新功能让你痛不欲生,团队成员无法很快融入,文档无法及时更新等等。

在长期运转的项目中,架构的腐化是怎么产生的?为什么常见的面向对象技术无法解决这类问题?如何延缓架构的腐化?

本文将尝试解释这一切,并提出相应的解决方案。读者需要具备相当的开发经验——至少在同一个项目的开发上一年以上;公司负责架构演进、产品演进的角色会从本文找到灵感。

架构

架构这个词在各种场合不断地以各种面目表现出来。从维基百科的词条看来,我们经常听到的有插件架构(Plugin),以数据库为中心的架构(Database Centric),模型-视图-控制器架构(MVC),面向服务的架构(SOA),三层模型(Three-Tier model),模型驱动架构(MDA)等等等等。奇妙的是,这些词越大,实际的开发者就越痛苦。SOA很好——但在它提出的那个年代,带给开发者的只是面向厂商虚无缥缈的“公共数据类型”;MDA甚至都没有机会沦为新一轮令人笑话的CASE工具。

在继续阅读之前,读者不妨问自己一个问题:在长期的项目中,这些大词是否真的切实给你带来过好处?更为功利的问题是:你,作为战斗在一线的开发者,在长期项目中可曾有过美好的体验?

技术的演变与挥之不去的痛

企业应用的发展似乎从十年前开始腾飞。从Microsoft ASP/LAMP(Linux、Apache、MySQL、PHP)年代开始,各种企业应用纷纷向浏览器迁移。经过十年的发展,目前阵营已经百花齐放。与过去不同,现在的技术不仅仅在编程语言方面,常见的编程套路、最佳实践、方法学、社区,都是各种技术独特拥有的。目前占据主流的阵营有:

 

  • Rails
  • Java EE平台。值得一提的是Java VM已经成为一种新的宿主平台,Scala、JRuby更为活跃并引人瞩目
  • LAMP平台。Linux/MySQL/Apache并没有多少变化,PHP社区从Rails社区获得了不少养分,出现了许多更加优秀的开发框架
  • Microsoft .NET平台
  • Django

没有理由对这些新技术不感到振奋。它们解决了许多它们出现之前的问题。在它们的网站上都宣称各种生产效率如何之高的广告语,类似于15分钟创建一个博客应用;2分钟快速教程等等。比起过去21天才能学会XXX,现在它们在上手难度上早已大幅度降低。

需要泼冷水的是,本文开篇提出的问题,在上述任何一种技术下,都如幽灵般挥之不去。采用Ruby on Rails的某高效团队在10人团队工作半年之后,构建时间从当初的2分钟变成2小时;我们之前采用Microsoft .NET 3.5 (C# 3.0)的一个项目,在产生2万行代码的时候,构建时间已经超过半小时;我们的一些客户,工作在10年的Java代码库上——他们竭尽全力,保持技术栈与时俱进:Spring、Hibernate、Struts等,面对的困境是他们需要同时打开72个项目才能在Eclipse中获得编译;由于编译打包时间过长,他们去掉了大部分的单元测试——带来巨大的质量风险。

如果你真的在一个长期的项目工作过,你应该清楚地了解到,这种痛苦,似乎不是任何一种框架能够根本性解决的。这些新时代的框架解决了大部分显而易见的问题,然而在一个长期项目中所面对的问题,它们无能为力。

一步一步:架构是如何腐化的

无论架构师在任何时代以何种绚丽的方式描述架构,开发中的项目不会超出下图所示:

architecture1

基本架构示意

一些基本的准则:

  • 为了降低耦合,系统应当以恰当的方式进行分层。目前最经考验的分层是MVC+Service。
  • 为了提供基础的访问,一些基本的、平台级别的API应该被引入。用Spring之类的框架来做这件事情。
  • 用AOP进行横向切分业务层面共性的操作,例如日志、权限等。
  • 为了保证项目正常构建,你还需要数据库、持续集成服务器,以及对应的与环境无关的构建脚本和数据库迁移脚本。

阶段1

满足这个条件的架构在初期是非常令人愉悦的。上一部分我们描述的框架都符合这种架构。这个阶段开发非常快:IDE打开很快,开发功能完成很快,团队这个时候往往规模较小,交流也没有问题。所有人都很高兴——因为用了新技术,因为这个架构是如此的简单、清晰、有效。

阶段2

好日子不算太长。

很快你的老板(或者客户,随便什么)有一揽子的想法要在这个团队实现。工作有条不紊的展开。更多的功能加入进来,更多的团队成员也加入了进来。新加入的功能也按照之前的架构方式开发着;新加入的团队成员也对清晰的架构表示欣喜,也一丝不苟的遵循着。用不了多久——也许是三个月,或者更短,你会发现代码库变成下面的样子:

architecture2

正常开发之后

你也许很快会意识到这其中有什么问题。但你很难意识到这到底意味着什么。常见的动作往往围绕着重构——将纵向相关的抽取出来,形成一个新的项目;横向相关的抽取出来,形成一个名叫common或者base的项目。

无论你做什么类型的重构,一些变化在悄悄产生(也许只是快慢的不同)。构建过程不可避免的变长。从刚开始的一两分钟变成好几分钟,到十几分钟。通过重构构建脚本,去掉那些不需要的部分,构建时间会降到几分钟,你满意了,于是继续。

阶段3

更多的功能、更多的成员加入了。构建时间又变长了。随着加载代码的增多,IDE也慢了下来;交流也多了起来——不是所有人能够了解所有代码了。在某些时候,一个很有道德的程序员尝试重构一部分重复逻辑,发现牵涉的代码太多了,好多都是他看不懂的业务,于是他放弃了。更多的人这么做了,代码库越来越臃肿,最终没有一个人能够搞清楚系统具体是怎么工作的了。

系统在混乱的状态下继续缓慢地混乱——这个过程远比本文写作的时间要长很多,之间会有反复,但据我观察,在不超过1年的时间内,无论采用何种技术框架,应用何种架构,这个过程似乎是不可抗拒的宿命。

常见的解决方案

我们并非是坐以待毙的。身边优秀的同事们在问题发现之前采取了各种解决方案。常见的解决方案如下:

升级工作环境

没有什么比一台与时俱进的电脑更能激励开发人员了。最多每隔三年,升级一次开发人员的电脑——升级到当时最好的配置,能够大幅度的提升生产效率,激励开发人员。反过来,利用过时的电脑,在慢速的机器上进行开发,带来的不仅仅是客观上开发效率的降低,更大程度上带来的是开发人员心理上的懈怠。

升级的工作环境不仅仅是电脑,还包括工作的空间。良好的,促进沟通的空间(以及工作方式)能够促进问题的发现从而减少问题的产生。隔断不适合开发。

分阶段的构建

一般而言,构建的顺序是:本地构建确保所有的功能运行正常,然后提交等待持续集成工作正常。本地构建超过5分钟的时候就变得难以忍受;大多数情况下你希望这个反馈时间越短越好。项目的初期往往会运行所有的步骤:编译所有代码,运行所有测试。随着项目周期的变长,代码的增多,时间会越来越长。在尝试若干次重构构建脚本再也没办法优化之后,“分阶段构建”成为绝大多数的选择。通过合理的拆分、分层,每次运行特定的步骤,例如只运行特定的测试、只构建必要的部分;然后提交,让持续集成服务器运行所有的步骤。这样开发者能够继续进行后续的工作。

分布式构建

即便本地快了起来,采用分阶段构建的团队很快发现,CI服务器的构建时间也越来越让人不满意。每次提交半小时之后才能得到构建结果太不可接受了。各种各样的分布式技术被创建出来。除了常见的CI服务器本身提供的能力,许多团队也发明了自己的分布式技术,他们往往能够将代码分布到多台机器进行编译和运行测试。这种解决方案能够在比较长的一段时间内生效——当构建变慢的时候,只需要调整分布策略,让构建过程运行在更多的集群机器上,就可以显著的减少构建时间。

采用JRebel或者Spork

一些新的工具能够显著地提速开发人员的工作。JRebel能够将需要编译的Java语言变成修改、保存立即生效,减少了大量的修改、保存、重新编译、部署的时间;Spork能够启动一个Server,将RSpec测试相关的代码缓存于其中,这样在运行RSpec测试的时候就不用重新进行加载,极大提升了效率。

到底是什么问题?

上述的解决方案在特定的时间域内很好地解决了一部分问题。然而,在项目运转一年,两年或者更久,它们最终依然无法避免构建时间变长、开发变慢、代码变得混乱、架构晦涩难懂、新人难以上手等问题。到底问题的症结是什么?

人们喜欢简洁。但这更多的看起来是一个谎言——没有多少团队能够自始至终保持简洁。人们喜欢简洁只是因为这个难以做到。并不是说人们不愿意如此。很多人都知道软件开发不比其他的劳动力密集型的行业——人越多,产量越大。《人月神话》中已经提到,项目增加更多的人,在提升工作产出的同时,也产生了混乱。短期内,这些混乱能够被团队通过各种形式消化;但从长期看来,随着团队人员的变动(新人加入,老人离开),以及人正常自然的遗忘曲线,代码库会逐渐失控,混乱无法被消化,而项目并不会停止,新功能不断的加入,架构就在一天天的过程中被腐蚀。

人的理解总有一个边界,而需求和功能不会——今天的功能总比昨天的多;这个版本的功能总比上个版本的多。而在长时间的开发中,忘记之前的代码是正常的;忘记某些约定也是正常的。形成某些小而不经意的错误是正常的,在巨大的代码库中,这些小错误被忽视也是正常的。这些不断积攒的小小的不一致、错误,随着时间的积累,最终变得难以控制。

很少有人注意到,规模的变大才是导致架构腐化的根源——因果关系在时空上的不连续,使得人们并不能从其中获得经验,只是一再重复这个悲剧的循环。

解决方案

解决方案的终极目标是:在混乱发生之前,在我们的认知出现障碍之前,就将项目的规模控制在一定范围之内。这并不容易。大多数团队都有相当的交付压力。大多数的业务用户并没有意识到,往一个项目/产品毫无节制地增加需求只会导致产品的崩溃。看看Lotus Notes,你就知道产品最终会多么令人费解、难以使用。我们这里主要讨论的是技术方案。业务上你也需要始终对需求的增长保持警惕。

0. 采用新技术

这可能是最廉价的、最容易采用的方案。新技术的产生往往为了解决某些特定的问题,它们往往是经验的集合。学习,理解这些新技术能够极大程度减少过去为了完成某些技术目标而进行的必要的经验积累过程。就像武侠小说中经常有离奇遭遇的主人公突然获得某个世外高人多年的内力一样,这些新技术能够迅速帮助团队从某些特定的痛点中解脱出来。

已经有足够多的例子来证明这一观点。在Spring出现之前,开发者的基本上只能遵循J2EE模式文档中的各种实践,来构建自己的系统。有一些简单的框架能够帮助这一过程,但总体来说,在处理今天看起来很基础的如数据库连接,异常管理,系统分层等等方面,还有很多手工的工作要做。Spring出现之后,你不需要花费很多精力,很快就能得到一个系统分层良好、大部分设施已经准备就绪的基础。这为减少代码库容量以及解决可能出现的低级Bug提供了帮助。

Rails则是另外一个极端的例子。Rails带来的不仅仅是开发的便利,还带来了人们在Linux世界多年的部署经验。数据库Migration, Apache + FastCGI或者nginx+passenger,这些过去看起来复杂异常的技术在Rails中变得无足轻重——稍懂命令行的人即可进行部署。

任何一个组织都无法全部拥有这些新技术。因此作为软件从业者,需要不断地保持对技术社区的关注。闭门造车只能加速架构的腐化——特别是这些自己的发明在开源社区早已有成熟的方案的时候。在那些貌似光鲜的产品背后,实际上有着无数的失败的案例成功的经验在支撑。

我们曾经有一个项目。在意识到需求可能转向类似于key-value的文档数据库之后,团队大胆的尝试采用SQLServer 2008的XML能力,在SQL Server内部实现了类似于No-SQL的数据库。这是一个新的发明,创造者初期很兴奋,终于有机会做不同的事情了。然而随着项目的进行,越来越多的需求出现了:Migration的支持、监控、管理工具的支持、文档、性能等等。随着项目的进展,最终发现这些能力与时下流行的MongoDB是如此的相似 ——MongoDB已经解决了大多数的问题。这个时候,代码库已经有相当的规模了——而这部分的代码,让许多团队成员费解;在一年之后,大约只有2个人能够了解其实现过程。如果在早期采用MongoDB,团队本有机会摒弃大部分相关的工作。

值得一提的是,高傲的开发者往往对新技术不够耐心;或者说对新技术的能力或局限缺乏足够耐心去了解。每一个产品都有其针对的问题域,对于问题域之外,新技术往往没有成熟到能够应对的地步。开发者需要不断地阅读、思考、参与,来验证自己的问题域是否与其匹配。浅尝辄止不是好的态度,也阻碍了新技术在团队内的推广。

新技术的选型往往发生在项目/产品特定的时期,如开始阶段,某个特定的痛点时期。日常阶段,开发者仍然需要保持对代码库的关注。下一条,重构到物理隔离的组件则是对不断增大的代码库另一种解决方案。

1. 重构到物理隔离的组件

显而易见的趋势是,对于同一个产品而言,需求总是不断增多的。去年有100个功能,今年就有200个。去年有10万行代码,今年也许就有20万行。去年2G 内存的机器能够正常开发,今年似乎得加倍才行。去年有15个开发人员,今年就到30个了。去年构建一次最多15–20分钟,今年就得1个小时了,还得整个分布式的。

有人会注意到代码的设计问题,孜孜不倦地进行着重构;有人会注意到构建变慢的问题,不懈地改进着构建时间。然而很少有人注意到代码库的变大才是问题的根源。很多常规的策略往往是针对组织的:例如将代码库按照功能模块划分(例如ABC功能之类)或者按层次划分(例如持久层、表现层),但这些拆分之后的项目依然存在于开发人员的工作空间中。无论项目如何组织,开发者都需要打开所有的项目才能完成编译和运行过程。我曾经见到一个团队需要在Visual Studio中打开120个项目;我自己也经历过需要在Eclipse中打开72个项目才能完成编译。

解决方案是物理隔离这些组件。就像团队在使用Spring/Hibernate/Asp.NET MVC/ActiveRecord这些库的时候,不用将它们对应的源代码放到工作空间进行编译一样,团队也可以将稳定工作的代码单元整理出来形成对应的库,标记版本然后直接引用二进制文件。

在不同的技术平台上有着不同的方案。Java世界有历史悠久的Maven库,能够良好的将不同版本的 JAR以及他们的以来进行管理;.NET比较遗憾,这方面真正成熟的什么也没有——但参考Maven的实现,团队自己造一个也不是难事(可能比较困难的是与MSBuild的集成);Ruby/Rails世界则有著名的gem/bundler系统。将自己整理出来的比较独立的模块不要放到rails/lib /中,整理出来,形成一个新的gem,对其进行依赖引用(团队内需要搭建自己的gems库)。

同时,代码库也需要进行大刀阔斧的整改。之前的代码结构可能如下,(这里以SVN为例,因为SVN有明确的trunk/branches/tags目录结构。git/hg类似)

svn-old1

原来的库结构

改进之后,将会如下图所示:

svn-new1

改进的库结构

每个模块都有属于自己的代码库,拥有自己的独立的升级和发布周期,甚至有自己的文档。

这一方案看起来很容易理解,但在实际操作过程中则困难重重。团队运转很长一段时间之后,很少有人去关心模块之间的依赖。一旦要拆分出来,去分析几十上百个现存项目之间的依赖相当费劲。最简单的处理办法是,检查代码库的提交记录,例如最近3个月之内某个模块就没有人提交过,那么这个模块基本上就可以拿出来形成二进制依赖了。

很多开源产品都是通过这个过程形成的,例如Spring(请参考阅读《J2EE设计开发编程指南》,Rod Johnson基本上阐述了整个Spring的设计思路来源)。一旦团队开始这样去思考,每隔一段时间重新审视代码库,你会发现核心代码库不可能失控,同时也获得了一组设计良好、工作稳定的组件。

2. 将独立的模块放入独立的进程

上面的解决方案核心原则只有一条:始终将核心代码库控制在团队可以理解的范围内。如果运转良好,能够很大程度上解决架构因为代码规模变大而腐化的问题。然而该解决方案只解决了在系统在静态层面的隔离。当隔离出的模块越来越多,系统也因此也需要越来越多的依赖来运行。这部分依赖在运行期分为两类:一类是类似于 Spring/Hibernate/Apache Commons之类的,系统运行的基础,运行期这些必须存在;另外一类是相对独立的业务功能,例如缓存的读取,电子商城的支付模块等。

第二类依赖则可以更进一步:将其放到独立的进程中。现在稍具规模的系统,登录、注销功能已经从应用中脱离而出,要么采用SSO的方案来进行登陆,要么则干脆代理给别的登陆系统。LiveJournal团队在开发过程中,发现缓存的读写实际上可以放到独立的进程中进行(而不是类似EhCache的方案,直接运行于所在的运行环境中),于是发明了现在鼎鼎有名的memcached. 我们之前进行的一个项目中,发现支付模块完全能够独立出来,于是将其进行隔离,形成了一个新的、没有界面的、永远在运行的系统,通过REST处理支付请求。在另外一个出版项目中,我们发现编辑编写报告的过程实际上与报告发行过程虽然存在类级别的重用,但在业务层面是独立的。最终我们将报告发行过程做成了一个常驻服务,系统其他的模块通过MQ消息与其进行交互。

这一解决方案应该不难理解。与解决方案1不同的是,这一方案更多的是要对系统进行面向业务层面的思考。由于系统将会以独立的进程来运行这一模块,在不同的进程中可能存在一定的代码重复。例如Spring同时存在两个不相关的项目中大家觉得没什么大不了的;但如果是自己的某个业务组件同时在同一个项目的两个进程中重复,许多人就有些洁癖不可接受了。(题外话:这种洁癖在OSGi环境中也存在)这里需要提醒的是:当处于不同的进程时,它们在物理上、运行时上已经彻底隔离了。必须以进程的观点去思考整个架构,而不是简单的物理结构。

从单进程模型到多进程模型的架构思维转变也不太容易——需要架构师有意识的加强这方面的练习。流行的.NET和Java世界倾向于把什么都放到一起。而 Linux世界Rails/Django则能更好的平衡优秀产品之间的进程协调。例如memcached的使用。另外,现在多核环境越来越多,与其费尽心思在编程语言层面上不如享受多核的好处,多进程能够简单并且显著地利用多核能力。

3. 形成高度松散耦合的平台+应用

现在将眼光看更远一些。想象一下我们在做一个类似于开心网、Facebook、人人网的系统。它们的共同特点是能够接入几乎无限的第三方应用,无论是买卖朋友这类简单的应用,还是绚丽无比的各种社交游戏。神奇的是,实现这一点并不需要第三方应用的开发者采用跟它们一样的技术平台,也不需要服务端提供无限的运算能力——大部分的架构由开发方来控制。

在企业应用中实现这个并不难。这其中的秘诀在于:当用户通过Facebook访问某个第三方应用的时候,Facebook实际上通过后台去访问了第三方应用,将当前用户的信息(以及好友信息)通过HTTP POST送到第三方应用指定的服务网址,然后将应用的HTML结果渲染到当前页面中。某种意义上说,这种技术本质上是一种服务器端的mashup. (详情参考InfoQ 文章

facebook

Facebook App架构

这种架构的优点在于极度的分布式。从外观上看起来一致的系统,实际由若干个耦合极低、技术架构完全不同的小应用组成。它们不需要被部署在同一台机器上,可以单独地开发、升级、优化。一个应用的瘫痪不影响整个系统的运行;每个应用的自行升级对整个系统也完全没有影响。

这并非是终极的解决方案,只在某些特定的条件下有效。当系统规模上非常庞大,例如由若干个子系统组成;界面基本一致;子系统之间关联较少。针对这个前提,可以考虑采用这种架构。抽象出极少的、真正有效公用的信息,在系统之间通过HTTP POST.。其他的系统完全可以独立开发、部署,甚至针对应用访问的情况进行特定的部署优化。如果不这么做,动辄上百万千万行的代码堆在一个系统中,随着时间的推移,开发者逐渐对代码失控,架构的腐化是迟早的事情。

例如,银行的财务系统,包括了十多个个子系统,包括薪资、资产、报表等等模块,每一部分功能都相对独立并且复杂。整个系统如果按照这种方式拆分,就能够实现单点优化而无需重新启动整个应用。针对每个应用,开发者能够在更小的代码内采用自己熟悉的技术方案,从而减少架构腐化的可能。

结语

没有糟糕的架构,变化使之

我访问过很多团队。在很多项目开始的时候,他们花很多时间在选择用何种技术体系,何种架构,乃至何种IDE。就像小孩子选择自己钟爱的玩具,我相信无论过程如何,团队最终都会欣然选择他们所选择的,并且坚信他们的选择没有错误。事实也确实如此。在项目的开始阶段很难有真正的架构挑战。困难的地方在于,随着时间的增长,人们会忘记;有很多的人加入,他们需要理解旧代码的同时完成新功能;每一次代码量的突破,都会引起架构的不适应;这些不适应包括:新功能引入变得困难,新人难以迅速上手;构建时间变长等等。这些能否引起团队的警觉,并且采取结构性的解决方案而不是临时性的。

关于文档

很多人说敏捷不提倡文档。他们说文档很难写。他们说开发人员写不了文档。于是就没有文档。

奇怪的是我看到的情况却不是这样。程序写得优秀的人,写起文字来也很不错。ThoughtBlogs上绝大多数都是程序员,很多人的文字写得都很赞。

而项目中的文档往往少得可怜。新人来了总是一头雾水。令人奇怪的是,新人能够一天或者两天之内通过阅读RSpec或者JBehave迅速了解这些工具的使用,到了团队里面却没有了文档。

抛开项目持续运转并交付的特性不谈,我认为巨大的、不稳定的代码库是文档迅速失效的根源。如果我们能够按照上述的解决方案,将代码库缩小,那么独立出来的模块或者应用就有机会在更小的范围内具备更独特的价值。想象一下现在的Rails3/Spring框架,他们往往有超过20个第三方依赖,我们却没有觉得理解困难,最重要的原因是依赖隔离之后,这些模块有了独立的文档可以学习。

企业级项目也可以如此。

创建应用程序的生态环境,而非单一的项目

功能总是不断的、不断的加到同一个产品中。这毫不奇怪。然而通过我们前面的分析,我们应当重新思考这个常识。是创建一个日益庞大的、缓慢的、毫无生机的产品,还是将其有机分解,成为一个生机勃勃的具有不同依赖的生态系统?项目的各方人员(包括业务用户、架构师、开发者)应当从短视的眼光中走出来,着眼于创建可持续的应用程序生态系统。

关于作者

陈金洲,Buffalo Ajax Framework作者,ThoughtWorks中国公司首席咨询师,现居西安。目前的工作主要集中在RichClient开发,同时一直对Web可用性进行观察,并对其实现保持兴趣。


本文原文发表于InfoQ:http://www.infoq.com/cn/articles/cjz-architecture-corruption

Share

从敏捷宣言理解敏捷交互设计

本文原文发表于InfoQ:http://www.infoq.com/cn/articles/xzc-agile-interaction-design

敏捷交互设计是敏捷方法论向交互设计领域的延伸,它提倡让所有相关人参与到设计过程中,迭代演进式地进行交互设计。从2010年开始,已经有越来越的团队在不同程度上使用敏捷交互设计的方法,而放弃了流程化的传统产品设计过程。

事实上,敏捷交互设计方法在很多方面都充分体现了敏捷价值观,因此,理解敏捷交互设计实践的最好方法是从记录在敏捷宣言中的价值观开始。

个体和交互胜过流程和工具

一个传统交互设计的流程一般分成以下几个步骤进行:

  1. 任务分析:任务分析基于功能列表(一般来自于客户的功能说明书)──在功能性需求的基础上拆分出人物流程和场景;
  2. 页面流程:根据任务分析的结果,为每一个大任务下的子任务中覆盖的功能制作页面流程;
  3. 信息建模:根据页面流程的设计出一套完整的信息框架,满足用户所有功能性需求;
  4. 原型设计:基于信息建模,设计出低保真原型,交给美工进行页面美化;
  5. 视觉设计:基于原型设计,对页面进行美化,最终产出高保真原型,同时编写设计说明;

在传统流程中,我们可以看到非常细致的分工──产品经理负责功能的拆解和分类,以及页面流转;交互设计师设计信息架构和具体交互行为;视觉设计师负责美化页面;前端开发人员负责高保真原型。

你是否体看见了传统瀑布式开发的影子?弊端显而易见:

  1. 分工造成的局限性──每个人都用自己的视角进行工作,无法形成统一的产品视角(Vision);
  2. 分工造成的“不可评价性”──你没权利对产品经理的功能拆解有异义,因为你不是这方面的专家;
  3. 需求在传递中产生了失真的风险──需要靠大量文档进行记录;
  4. 客户没法说不──当客户需要到整个流程的最后看到一个或者两个大而全的设计方案时,他无法提出任何有价值的反馈,这本身就是用一个贵重的半成品绑架客户;

跟软件交付中的敏捷实践一样,敏捷交互设计倡导全功能团队,避免过于明显的分工,和基于分工特有的流程。仅有的流程是基于产品逐步清晰化的过程,而非基于人员的技能,所有人都应该参与到这个过程中来。敏捷交互设计主要分以下几个步骤:

  1. 寻找产品方向(Inspire):抛开需求列表,从目标人群的期待体验出发,寻找可能存在的产品方向;
  2. 定位产品需求(Identify):定位本产品需要提供什么样的消费者体验;
  3. 设计产品体验(Ideate):对决定的目标体验进行设计;
  4. 验证产品设计(Implement):快速制作原型,并频繁进行用户测试,迭代式改进。

图1. 从体验中寻找交付范围,把功能列表放在一边

除了流程简化和分工融合,在工具的选择上,敏捷交互设计也与传统方式有所不同──对于交互的推崇高于对特定工具的选择。

所有在敏捷交互设计中使用的工具,都应该遵循一条原则:它必须推动设计团队成员间的交互,而不是简单提升单个成员的工作效率。

基于此,敏捷交互设计中推崇各种轻量级工具,而不是大型的第三方软件,例如纸质原型Paper Protityping而非Visio或Axure此类原型工具。过于精细的结果往往会增加协作和反馈的门槛,虽然可能提升单个成员工作效率,却达不到鼓励交互的目的。

图2. 使用轻量级的工具进行交互设计

可工作的软件胜过完备的文档

敏捷软件交付过程中,每个迭代的核心产出是不足够完美,但却满足一个完整业务场景的软件──端到端流程的可完成,而不需要面面俱到的完美。

而传统开发方式中在长时间内只是各个功能模块中功能的堆砌,无法在短时间内实现端到端场景,那么文档便成为串联各个功能保证有序开发的必需品。

传统交互设计也存在这个问题。往往一个标准交互设计阶段的文档分三个方面,它们是:

  1. 内容方面(Content):内容的层次和整体信息架构设计;
  2. 视觉方面(Visual):整站风格的视觉设计文档;
  3. 交互方面(Interaction):整站高保真交互设计原型;

仔细分析这些文档的生产过程,我们不难发现以下特点:

  1. 它们都是以整站为目标,试图覆盖所有使用场景;
  2. 它们的生产过程是线性的,直到三者全部完成才能够指导开发;
  3. 正因为每个环节的过程是孤立的,无法形成统一的认识,文档传递经常发生失真;
  4. 一个完美的东西很难得到产品方向性的关键反馈;

敏捷交互设计试图在解决以上问题。

敏捷交付的核心在于尽早地交付出可进行端到端测试的代码,而非完备文档,而敏捷交互设计的核心则在于尽早地交付出可以进行端到端可测试的原型,同样亦非完备文档。

而这里说的“端到端可测试的原型”包含以下含义:

  1. 端到端:必须设计出符合合理使用场景的端到端流程,这个流程会覆盖一个典型用户最核心的使用场景,所有的交互设计应该第一时间收敛在这个端到端场景周围,而非“整站”功能的分割展示;
  2. 可测试原型:不需要完美的原型,只需要所设计端到端场景中涉及到的原型,同时,原型的完备性上必须达到内容、视觉、交互三者细致力度一致,在测试中,三者力度的不一致往往会隐藏问题,例如,如果测试的原型视觉上过于完美,就会减弱用户在交互上的关注;

假设我们把交互设计的四周拆分成每周一个迭代,每周交付一个覆盖端到端场景,且在内容、视觉和交互方面都相对完备的原型收集反馈或进行测试,和传统项目相比,我们是否可以更早地得到客户反馈,是否可以让设计过程更透明,是否毋须完备的过程文档?答案自然是肯定的。

图3 逐步细化的原型,不停进行用户测试

当然,这样的过程必然需要多模块(这里的模块指技能的差别)之间的融合,实现全团队的运作。

你会问我,这是否意味着我们的交互设计师需要学会使用IIlustrtor进行视觉设计?或者视觉设计师需要学会HTML+CSS+jQuery制作高保真原型?

是的,在很多情况下,敏捷交互设计团队中的设计师确实需要具备多种功能的T型人才──他们有专业的技能,也可以在其他方面有足够的贡献。

分工的结果只能导致能力的停滞不前、产品视角的缺失、职能不可替代的风险、协作软的低效、沟通的浪费等等,而唯一的好处在于,可以更快和“更不被抱怨(如德鲁克说过程文档的价值在于管理抱怨)”产生一堆堆相互分裂且无法让客户挑错的文档。

客户协作胜过合同谈判

让我们举例说明在传统交互设计阶段出现的场景:一份标书、一份功能列表、一份到处使用的所谓用户体验调查问卷,在交互设计的开始阶段,我们和客户在一起进行“需求调研”,其实这些不重要,我们只需要对比我们之前的产品都有哪些功能可能有重复,类似产品有哪些可以借鉴,调研往往只是形式,关键还是未来的二至四周;

然后最后一次见客户也许就是最终文档提交的那天,给客户看一套精美的PSD文档,一般我们会做出A和B方案,其中不乏我们臆想出来的需求,有时只为让那个区域看起来不那么空,客户高兴地选择了其中的一套方案后,交付正式开始。

这就是基于合同进行设计的典型场景,谁也不知道合同中的功能列表意味着什么,而对于客户来说,它确实是购物单,真正交付时已忘记为什么需要。

但这不妨碍交互设计师进行设计,大部分时候他们并没有仔细思考这个功能真正的使用场景,而是把它“画”出来,让它看起来很美,却不管它如何实现和如何被用户使用。

这是基于合同进行设计自然而然的结果,在每个功能上,设计师都希望当成对自己的挑战,他们绞尽脑汁收集类似产品类似功能,尽可能取悦客户,说服他们采用更炫更丰富的交互方式,而作为不对交付负责的人他们毋须承担责任。

而敏捷交互设计希望打破这种基于合同或者换言之,基于功能列表的设计模式。它希望在设计的全过程将客户参与进来,让客户了解某个标书上的功能也许没有意义,或者不是当下应该解决的问题──一个交付项目范围的最好确定时机就是在交互设计过程中客户的全程参与。客户参与的优势有以下几点:

  1. 过程的透明化提升客户的安全感,随时保持对设计进度的了解;
  2. 最好的反馈模式就是让客户亲身参与设计过程;
  3. 了解最终用户和业务模式的是客户,客户是不可多得的资源;
  4. 当设计结果是功能列表的重新确认,对于合同中的内容便达成了新的共识;
  5. 客户参与的过程是建立信任的最佳机会,良好的信任关系应该在最开始就建立;
  6. 在产品设计方向上和客户达成一致,而不是仅靠标书或者访谈结果的支言片语;
  7. 提升参与感有助于培养更负责的客户,在交付阶段,之前的努力将会获得巨大的回报;
  8. 有效控制设计的变化,当客户亲身参与设计决定过程时,很多盲目的设计变化会减少很多;

拥抱变化胜过遵循计划

当你的设计不能和客户一起进行,你只是个标书需求列表的简单执行者,需求的变化便是理所当然的事情。

在有些传统交互设计流程里甚至出现“需求冻结”这样的流程──如果你已经对某个环节的文档进行进行签收,就不能在“需求冻结”后改变主意。这样的结果无非两个:一尽量不要签收那个环节的文档;二在需求没有冻结的时候抓紧机会改变主意。

从敏捷宣言的角度来看,之前的三句话是最后一句话的基础,如果不能做到对个体和交互的尊重,把可用软件当作产生核心价值的东西,并且努力和客户进行协作,谈拥抱变化只是空话。在敏捷交互设计中,遵循同样的道理:

对个体和交互的尊重──当需求发生变化时,因为在新的流程中对产品视角的重视,可首先对需求变化的价值进行判断,即它是不是匹配达成一致的产品视角;真正需求变化时,因为分工的模糊以及流程的简化,可更灵活地调配资源进行处理;再者因为大量使用轻量级的工具,修改成本大大降低;

关注可工作的软件──因为我们把可进行的端到端场景测试作为敏捷交互设计过程中最重要的目标,当出现需求变化时,可通过审视变化本身“是否包含在端到端场景中?如果不产生这样的变化,用户因此有何种损失”来判断需求变化本身的价值;同时,因为能够尽早的得到端到端场景原型设计,需求本身往往来自于用户测试的结果,这样的需求变化往往是有价值的,有理可依的;

客户协作的重要性──需求变化往往来自于客户的不确定性,很多情况下这种不确定又来自于一个新产品背后业务模式的不确定,客户协作帮助在设计过程中及早发现和解决这种来自业务方面的不确定,同时,客户协作对客户责任感的培养也大大减少了来自于客户不负责任的需求摆动。

这就是敏捷交互设计可以很好的管理用户需求的根本原因。

从敏捷推行到现在,在交付领域已经走向成熟,越来越多的团队开始采用敏捷方法进入开发,但从软件交付的全流程来看,早于交付的交互设计环节目前依然以传统设计方式为主。

随着消费型软件大行其道,用户对软件使用体验的要求越来越高,同时新产品的推陈出新对快速产品设计提出新的要求,这样的背景使得传统交互设计流程不得不做出一些新的调整以拥抱更加频繁的变化,这也是为什么很多交互设计团队开始努力尝试具有敏捷价值观的敏捷交互设计进行产品设计的主要原因。

最后,让我们来比较一下传统交互设计方法和敏捷交互设计方法的区别:

传统交互设计 敏捷交互设计
没有交付团队的参与;客户参与度低;设计团队中各职能分工明确; 交互设计师、用户研究者、视觉设计师、前端开发者、客户代表、以及开发团队代表都完整参与整个交互设计的过程,并只有能力区分而弱化职责分工;
客户需求文档中的功能列表是贯穿设计过程的主线; 基于终端使用者期待体验的设计过程,往往客户功能列表只作为参考;
各自有各自对产品的理解,无法达成共识; 对产品设计方向的形成共识是贯穿整个交互设计阶段;
使用大型的交互设计软件; 鼓励使用白板、海报、贴纸、手绘等轻量级的工具;
客户只在开始和结束参与项目; 客户全程参与设计活动;
主要以文档制作为主; 主要以Workshop工作坊活动为主,高互动过程;
设计师单独和封闭工作; 设计师合作式的工作,随时把工作的产出物展示,接受反馈;
设计师能力专一; 鼓励T型人才;
缺少用户测试; 在不同精细度的原型上快速进行用户测试,迭代式演进设计;
大而全的设计; 只设计足够的交互;
远离客户以避免变化; 和客户在一起鼓励变化;
不对交付负责,肆意发挥; 对交付负责,在成本接受的范围内创新;

感谢张凯峰对本文的审校。

本文原文发表于InfoQ:http://www.infoq.com/cn/articles/xzc-agile-interaction-design

Share