软件系统的稳定性

软件系统的稳定性,主要决定于整体的系统架构设计,然而也不可忽略编程的细节,正所谓“千里之堤,溃于蚁穴”,一旦考虑不周,看似无关紧要的代码片段可能会带来整体软件系统的崩溃。这正是我阅读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

持续集成理论和实践的新进展

之前雷镇同学将Martin Fowler先生的著名论文《持续集成》第二版翻译成中文并发布出来,掀起了国内对于持续集成理论和实践讨论的新高潮。笔者在本文中将全面对比持续集成论文前后两版的异同,分析并展示ThoughtWorks在持续集成领域的理论和实践方面的研究成果,以期对国内企业实施持续集成起到参考和借鉴作用。需要说明的是,本文所介绍的内容毕竟限于笔者的水平,并且主要是ThoughtWorks内部开发和对外咨询实践的总结,所以未必对读者所遇到的情况是适用的,请自行甄别。

《持续集成》第二版虽然是最近才翻译出来(本文写于2015年初),但是实际上Martin Fowler先生完成此文是在5年前的事情。这五年恰好是ThoughtWorks中国公司快速成长的五年。在这五年内ThoughtWorks中国在持续集成领域也有很多发展,这包括:著名的持续集成工具Cruise主要是由中国公司负责开发1; 中国公司帮助国内很多大中型企业完成持续集成实施和相关的流程改进;2009年中国公司的很多同事对于持续集成的度量进行了深入的讨论并且最终由胡凯将其实现为一款软件iAnalysis;2010年至2011年成功的交付了从需求提供方到多个技术服务提供商的持续集成方案,以及企业级自动化中心方案。所以,本文主要包括两部分内容,一部分是通过对比第一版与第二版的异同介绍2000年到2006年之间持续集成领域的主要发展,另一部分则是介绍第二版发表之后持续集成领域的新进展。读者如果之前没有阅读过《持续集成》论文的第二版,建议将本文第一部分一同阅读,因为本文并非对论文的重述,所以很多地方还需要参考原文中的内容。

第一部分 《持续集成》第一版与第二版

《持续集成》第一版由ThoughtWorks首席科学家Martin Fowler和Matthew Foemmel共同完成2,第二版由Martin Fowler更新。

《持续集成》的第一版中并没有给出比较正式的定义,虽然作者在文中说是借鉴了XP实践中的术语,但是目前能看到的XP实践中对持续集成的定义实际上大多数都是指向了Martin的文章。那么我们还是来看看第二版中给出的定义。

持续集成是一种软件开发实践。在持续集成中,团队成员频繁集成他们的工作成果,一般每人每天至少集成一次,也可以多次。每次集成会经过自动构建(包括自动测试)的验证,以尽快发现集成错误。许多团队发现这种方法可以显著减少集成引起的问题,并可以加快团队合作软件开发的速度。3

第二版相对于第一版增加了不少内容,其中最重要的几点包括:

  1. 详细介绍了使用持续集成进行软件开发的工作流程。
  2. 突出了配置管理在持续集成实践中的作用。
  3. 提出分阶段构建的概念。
  4. 增加了持续集成报告的内容。
  5. 增加了持续部署的内容。
  6. 给出了引入持续集成的建议。

持续集成的流程

在持续集成领域,我们经常会用到的一个术语就是“构建(Build)”。很多人认为“构建=编译+链接(Build=Compile+Link)”,Martin在第一版中指出一次成功构建包括:

  • 所有最新代码从配置管理工具中取出(check out或者update)。
  • 所有的代码从干净的状态开始编译。
  • 将编译结果链接并部署,以备执行。
  • 执行部署的应用并运行测试套。
  • 如果上述所有操作没有任何错误,没有人工干预,并通过了所有测试,我们认为这才是一次成功的构建。

实际上,目前很多团队对成功持续集成构建的定义基本上是符合上述定义的。这个定义的特点在于它是相对独立的,它是一个从干净状态的源代码最终获得可运行的通过验证的软件的过程。

Martin在第二版中则在成功构建的基础上给出了成功集成的定义。成功集成关注的不是一次“编译+链接+部署+验证”的过程,而是从开发流程的角度介绍一次完整的在持续集成约束下的代码提交过程4

  • 将已集成的源代码复制一份到本地计算机。
  • 修改产品代码和添加修改自动化测试。
  • 在自己的计算机上启动一个自动化构建。
  • 构建成功后,把别人的修改更新到我的工作拷贝中。
  • 再重新做构建。
  • 把修改提交到源码仓库。
  • 在集成计算机上并基于主线的代码再做一次构建。
  • 只有这次构建成功了,才说明改动被成功的集成了。

下图展示了Martin对成功集成的定义:

当然在第一版的“代码提交”这一节,Martin也提到了本地构建的概念,只是不如第二版这么明确。

配置管理

Martin在第一版中有两处提及配置管理,分别是:单一代码源(Single Source Point)和代码提交(Checking In)。第二版中则包括:通过持续集成构建特性(Building a Feature with Continuous Integration)、只维护一个代码仓库(Maintain a Single Source Repository)、每人每天都要向主线提交代码(Everyone Commits To The Mainline Every day)、每次提交都应在集成计算机上重新构建主线(Every Commit Should Build the Mainline on an Integration Machine)。不仅条目数量上增加明显,作者提出的很多实践都是基于配置管理来讲的。

工具

配置管理是持续集成的输入。在第一版中作者所推荐的配置管理工具是CVS,到第二版中作者推荐的配置管理工具已经换成了SVN5(参见第二部分中的配置管理工具部分)。

分支策略

实现进度与质量的平衡是配置管理的重要目的。Martin在第二版中对滥用分支给出了警告:

尽量减少分支数量。典型的情况是保持一条主线,……,每个人都从这条主线开始自己的工作(对之前发布版本进行Bug修正或者临时性的实验都是创建分支的正当理由)。

但是这里给出的建议对于大型团队来说并不十分合适。我们将在第二部分对于配置管理的分支策略进行详细描述。

内容

Martin在第一版中给出的原则是:

任何人都可以找到一台干净的机器,连上网,通过一个命令就可以取得要构建所开发的系统需要的所有源文件。

第二版中的原则增加了对构建的支持6

任何人都可以找到一台干净的机器,做一次取出(checkout)动作,然后对系统执行一次完整的构建。

分阶段构建(Staged Build)

分阶段构建是Cruise(已经更名为Go)引入的重要概念。其主要的意义在于:

  • 分离关注度不同的验证阶段,比如Commit Build和Regression Tests,团队会对不同的验证阶段采取不同的策略
  • 构建流程可视化
  • 通过分阶段并发构建来缩短反馈周期

当构建的时间过长时,我们通常会要求开发人员只运行速度较快的价值较高的构建阶段就可以继续自己的开发任务,而不必等待漫长的次级构建完成。这里作者提到ThoughtWorks不同的团队有很多有趣的实践,我们将在第二部分向读者介绍其中的一部分。

报告

作者在第二版中专门拿出一节“每个人都能看到进度(Everyone Can See What’s Happening)”来介绍有关持续集成报告的内容。因为:

持续集成的目的是为了沟通。

这是第二版相对于第一版来说一个非常明显的变化。在第一版中通知的手段还主要是电子邮件,实际上在作者撰写第二版的时候,ThoughtWorks已经不赞成将电子邮件作为主要的持续集成通知工具了。更好的沟通工具包括音乐、熔岩灯、显示器等。

对于沟通的重视从工具的角度也可以体现出来。Cruise Control最主要做的事情是任务调度,在报告部分做的相对来说非常粗糙,比较有价值的报告大部分是从Cruise移植过去的。Cruise在从一开始就非常重视这一点,通过Cruise你可以非常清晰地知道,代码发生了什么变化、正在进行的构建的状态和历史构建的状态。网页的形式对于分布式团队来说具有不可替代的优势。

正如我们前面所说的,音乐、熔岩灯等物理手段,具有更强的信息辐射能力。站起来往周围看一看就知道哪个团队的构建成功了,哪个失败了。

持续部署

持续集成实践有一个基本的思想就是:越是痛苦的事情,越要经常做。集成之后更令人心惊胆颤的事情就是——部署。部署到生产环境的流程通常要严格得多,然而所有的工作必须经历了生产环境的验证才算是成功的,所以——持续部署才是王道。Martin在第二版中建议:

你应该有一个脚本帮助你很容易地将系统部署到生产环境中去。……同时要特别考虑的是要能够自动回滚。

引入持续集成的建议

作者在第二版中特别给出了逐步引入持续集成的建议。包括:

  1. 引入版本控制。
  2. 实现自动化构建。
  3. 添加自动化测试。
  4. 加快提交构建。
  5. 寻找帮助。(比如ThoughtWorks)

第二部分 持续集成领域的新进展

正如前文所说,ThoughtWorks中国公司在过去的几年里面对于持续集成实践和帮助客户实施持续集成都积累了很多的经验,同时在理论体系方面也更加丰富完整。这也使ThoughtWorks在这个领域继续保持了行业领先的位置。

正如我们在第一部分讲到的,持续集成不应该只作为一个孤立的实践来应用。我们的经验表明如果只把持续集成作为一个孤立的实践应用很难从持续集成长期受益。持续集成往往进入“长红”或者“长绿”的不正常的状态。长红意味着系统长期无法集成;长绿则往往意味着缺少足够的验证。为了术语上的澄清,我们明确地将持续集成的定义区分为狭义的持续集成和广义的持续集成。

狭义的持续集成:基于某种或者某些变化对系统进行的经常性的构建活动。> 广义的持续集成:软件开发团队在上述活动的约束下所采用的开发流程。

狭义的持续集成

一般来说,狭义的持续集成包括如下几个方面:持续检查、持续编译(链接)、持续验证、持续部署、持续基础设施、持续报告等6个方面。

持续检查

持续检查的目的是保证代码风格一致,主要关注于代码的静态质量和内部质量,比如变量命名方式、大括号位置等等。大部分的现代集成开发环境(IDE)都具备实时检查代码质量的功能。为了保证主线上的代码质量能够达到一致的标准,应当在持续集成脚本中加入静态检查阶段。比如,Java的PMD、 FindBugs等等。

持续编译

持续编译是一个很朴素的想法,就是保证主线上的代码始终处于可编译的状态。但是这对于很多大中型团队来说却并非想当然的简单。这是因为很多团队并未采用集体代码所有权策略,导致存在依赖的团队的代码无法编译。针对这样的问题,一方面我们建议采用集体代码所有权;另一方面,对于确实因为安全原因需要隔离的代码应该边界、明确接口,很少存在大部分代码需要对大部分人保密的情况。

持续检查和持续编译是持续集成中最基本的验证手段。

持续验证

持续验证的目的是检查主线上的代码是否能够实现所要求的功能,或者已有的功能是否被破坏。在大部分的构建中,验证部分是耗时最长、成本最高的部分,但同时也是收益最大的部分。在这个阶段,我们看到的主要问题是验证不充分和验证时间过长。针对这样的问题,我们通常采用分层分级的持续集成策略。后面有详细的描述。

持续部署

对于大型软件应用来说,部署往往是一个费时费力又容易出错的步骤,因为这里面涉及到数据迁移、版本兼容等各种棘手的问题。持续部署的思想是将这些工作标准化自动化,使其能够可靠地、自动地、快速地运行。持续部署是当前DevOps运动中的热门话题之一。

持续基础设施集成

现代大型软件开发,尤其是互联网应用开发中经常依赖于一些常见的基础设施——比如Spring、Tomcat、Database等等。这些基础设施发生变化的时候,我们应当及时地触发持续集成,以确保我们的系统是能够与新的基础设施一起工作的。

持续报告

报告是将持续集成的状态以适当的形式展现给干系人的基本手段。报告是持续集成的晴雨表,所以它必须直观、易懂,而且对关注点不同的角色展现不同的内容和粒度。比如,开发人员可能更关心错误的日志;项目经理可能更关心测试覆盖率;产品经理可能更关心持续集成通过率的趋势等等。

广义的持续集成及持续集成策略

当要把持续集成实践应用到团队的时候,有很多额外的技术或者非技术因素需要考虑。

组织结构

持续集成是一个重要的沟通工具,而开发过程中两个最需要紧密沟通的角色就是开发和测试。在我们常见的组织结构中开发和测试往往隶属于不同的部门,甚至这些部门隶属于不同的高级经理。这往往会给持续集成的推广带来很大的阻力。这是因为持续集成从环境搭建到运行维护都需要两种角色的通力合作。我们的经验是这类涉及到人力资源的事情除非某一级“共同的大老板”出面,否则是很难协调的。“借调”这样的方式往往不能保证效果。

流程

放到团队的角度看待流程应当更加关注于各个成员之间的配合。每个开发人员提交代码之前应当确保是经过本地构建的;开发人员在提交之前应该确认主线上的代码是通过了持续集成的;测试人员测试的版本应该是通过了某次持续集成的,并且有相应的具体版本信息。

为了保障流程的顺利执行,我们还经常采用持续集成看板、提交令牌等辅助手段。

环境

环境是指持续集成运行时所依赖的软件和硬件的集合。我们经常遇到的一个问题是,软件在一台机器上能够通过持续集成的验证,而在另一台机器上则不能通过。这通常是因为我们对持续集成环境的定义不明确造成的。所以在搭建持续集成和在组织内推广持续集成的时候,我们需要特别注意持续集成环境的标准化,明确指出持续集成运行时依赖哪些第三方库,机器配置如何,端口和网络状况如何等等。

我们经常采用将持续集成环境加入配置管理的方式来解决环境标准化的问题。

分层

在大型团队(超过100人)中,扁平的开发组织结构是运行起来是比较困难的。常见的做法是按照特性,将团队划分为10人左右的小团队。一般来说,如果团队数量超过10个,还会再增加一层架构。这时候,配置管理的策略也应当做出调整。常见的做法是为每个团队拉出一个分支,设置一个集成分支用于将各个特性分支的内容整合在一起。需要注意的是,这里每个分支都应该具备所有代码的访问权限,也就是所有分支是同根的、等价的。

图片来自:http://damonpoole.blogspot.com/2008/01/multi-stage-continuous-integration-part_05.html

测试分级

在实施持续集成的时对于测试的类型应该有比较明确的定义。一般来说,我们经常把测试分为三级——单元测试、集成测试和系统测试。这是一个很大的话题,这里只是说明此处的单元测试并不是指针对函数的测试。虽然单元测试主要是函数基本的测试,但是每个单元测试应该针对的是特性或者对应代码在实现该特性上所发挥的作用。

单元测试的运行速度通常非常快,应该在数秒到数分钟。单元测试应该覆盖绝大部分的特性需求。集成测试单个测试的运行速度通常会比单元测试慢一个数量级,比如存在文件读写或者其他的IO和网络操作。集成测试主要用于保证系统的各个组件之间的调用是工作的。系统测试的运行速度一般会比集成测试慢,通常需要将整个系统运行起来,比如Web开发中的selenium测试。系统测试主要用于测试系统的正确路径(Happy Path)可以工作。有的团队会开发很多基于整个系统的回归测试,也属于系统测试。就场景的覆盖而言,单元测试>集成测试>系统测试,从运行时间来看则相反。

持续集成的成熟度评估

在帮助客户实施和推广持续集成的过程中我们逐渐总结出一些原则,帮助客户评估现状,分析和设计未来的目标。该评估方法借鉴了ThoughtWorks敏捷成熟度模型中有关配置管理、测试、构建等内容,并做了适当的简化。

构建:

级别 描述
3+:对外防御的 团队能够根据自己的需要,协调其他团队的持续集成,当依赖的其他团队的代码和组件或者第三方库和基础设施发生改变时自动进行构建。团队对于外部依赖的可靠性有信心。
3:对内防御的 构建是自动的。由测试和检查来保证团队内部的开发质量。持续集成的修复具有最高的优先级。团队对持续集成的结果有信心。
2:频繁的 构建是自动的,而且构建的速度较快。构建的触发条件是明确的,通常每次代码提交都会触发构建。团队中的每个人都会触发构建,并且了解构建的状态。
1:重复执行 构建是自动的,但是执行的不够频繁。构建的触发是随机的或者频率是非常低的(低于每天一次)。构建的速度通常非常慢,比如一次构建超过半个小时。
0:可重复的 主要依赖于手动的方式构建软件,但是每次构建的方式都是相同的或者相似的。通常有相关的文档的指导。经常团队指定某个人负责构建软件,虽然大部分人都能够做这件事情。
-1:手动的 主要依赖于手动的方式集成软件。每次集成的方式可能不一样。经常团队中只有个别人能够将软件集成起来。

测试:

级别 描述
3+:全面集成的 全团队对测试负责。测试驱动整个开发过程。测试与构建完全集成。
3:测试驱动的 业务分析人员和开发人员均参与测试。测试在构建过程中自动执行。开发人员实践测试驱动开发。
2:集成的 开发人员参与测试。部分测试集成在构建过程中执行。大部分测试在软件开发过程中执行。
1:共享的 开发人员参与测试。测试并未集成在构建过程中。部分测试在软件开发过程中执行,大部分测试在软件开发结束后执行。
0:审查的 测试由专门的测试人员负责。有部分测试是在软件开发过程中执行。但大部分测试在软件开发结束后执行。
-1:独立的 测试由专门的测试人员负责。仅在软件开发结束后执行。

配置管理:

级别 描述
3+:企业级的 企业有统一的配置管理策略。
3:跨项目的 配置管理在多个项目之间协调。
2:自动的 配置管理策略与持续集成策略紧密结合,团队成员有频繁提交的意识。一般采用乐观锁策略,原子提交。
1:集成的 版本管理下的内容是受控的。通常在版本管理中的代码是可以编译的。开发人员能够访问到自己工作所需要的代码。开发人员按照一定的规则访问配置管理工具和提交代码。一般采用悲观锁策略。版本管理工具和构建过程集成在一起的。
0:基本的 有基本的版本管理。但版本管理下的内容不全面,或者不足以支撑团队的开发。
-1:无配置管理 没有配置管理。或者使用方式完全错误。

常用实践和工具

持续集成看板7

问题:

我们需要让整个团队能够方便快捷的了解持续集成的状态。

上下文:

在完成了基本的构建基础设施的搭建之后,我们需要让团队成员及时获得持续集成的状态信息。传统的邮件方式可能会使人厌烦,或者错过重要的构建信息。

解决方案和实现:

安装一个显示器,将构建的状态信息以简明的方式展示在显示器上。将显示器放置在团队所有成员都能够很容易看到的地方。

个人构建中心

问题:

在某些情况下,构建环境的成本很高,而我们需要每一个开发人员提交之前完成一次个人构建。

上下文:

有些测试是依赖于设备的,而这些设备非常昂贵,并且配置复杂,所以无法给每个开发/测试人员一套构建环境。我们发现根据本地构建的理论模型,每个人的提交应该是串行的,这样我们实际上可以做到让这些人共享一套环境,但是从逻辑上就像是每个人有一套自己的环境一样。

解决方案:

在运行个人构建的时候将本地的代码同步到一台共享的机器上执行构建,构建完成后结果反馈给提交这次个人构建的人。

实现:

个人构建中心的实现有很多种方案。这些方案的区别主要在于如何将代码同步到个人构建中心服务器上。两种常见的方式:一个是使用rsync或者类似方式同步;另一个是使用分布式配置管理工具如git/hg同步。其拓扑结构如下:

第一种方式相对独立灵活;第二种方式稳定、高效,但是对于配置管理工具有依赖。

后果:

个人构建中心节省了大量的计算资源,同时也容易使得中心服务器成为单点失败的源头。一旦中心服务器出现问题,可能会导致团队的流程受到较大影响。

提交令牌

问题:

在实施本地构建的时候,向目标分支的提交应该是串行的,以避免构建被破坏后难以定位问题来源。但是团队往往缺乏一种有效的机制来保证这种串行。

上下文:

有些团队试图通过技术的手段来解决这个问题,比如通过配置管理上的锁机制,这种方式和乐观锁模式有较大冲突。有些团队通过团队内部沟通的方式解决,比如谁提 交之前都会通知别人,或者通过持续集成监视器来了解当前的构建状态,以决定自己是否可以提交。这些方式各自有各自的适用情形,较容易理解。

解决方案和实现:

使用一个实物作为令牌,准备提交的代码的人首先取得令牌,当代码提交完成(包括相应的提交构建)之后,将令牌交还。令牌要醒目,并且移动方便。小型奖杯、毛绒玩具、较大的头饰(如下图)都是不错的令牌。

分阶段构建

问题:

在某些团队中完整构建所花费的时间可能很长,如果每次提交都运行完整的构建会浪费很多时间。

上下文:

随着持续集成的日益完善,我们往往会发现验证所花费的时间越来越长,而大部分验证趋于稳定,失败的情况很少见。通过技术手段缩短构建时间是解决问题的根本办法,但是缩短构建时间是一个耗时耗力的工作,很难短期内见效。

解决方案和实现:

将构建分为几个阶段执行,在本地构建中仅执行速度比较快、可信度比较高、出错概率比较大的验证。利用晚上或者其他合适的时间执行全面的验证——我们这次构建称为全量构建。需要注意的是,这种情况下仍然要保证提交构建和本地构建的一致性。

iAnalysis

iAnalysis是一款轻量级的持续集成报告工具。该工具的核心思想是将持续集成构建过程中产生的数据以趋势和对比的方式展示出来。正如前文所说,我们在2009年的ThoughtWorks Away-Day上讨论了敏捷度量的话题,大家最后一致认为,数据有两种最基本的用法——横向对比和纵向对比。横向对比就是不同的人、不同的团队之间对比;纵向对比就是现在和过去对比。iAnalysis正是这种思想的体现。

关于作者

肖鹏,ThoughtWorks资深咨询师,程序员,敏捷过程教练。拥有7年以上软件开发实践经验,多次担任大中型企业敏捷流程改进咨询和培训,服务对象涉及通信设备制造、通信运营、互联网行业等。他关注于设计模式、架构模式、敏捷软件开发等领域,并致力于软件开发最佳实践的推广和应用。他曾参与翻译《Visual Studio 2005技术大全》,主持翻译《面向模式的软件架构》第四卷和第五卷等图书。


1 目前Cruise的开发任务已经不在中国公司了。

2 实际上这篇文章介绍的是Matthew所在团队在2000年早些时候已经在使用的实践,ThoughtWorks全球CEO郭晓先生当时也在这个团队。

3 为了与ThoughtWorks常用的术语保持一致,部分术语与雷镇和熊节同学所译略有差别,下同。

4 笔者对其格式略作处理,与原文稍有出入。

5 Martin最近在自己的一篇博客上对几种流行的配置管理工具做了对比。

6 注意,本文并非为指出第一版的缺陷,只是通过对比来说明作者论文中重点的变化。

7 这里只是借用“看板”这个词的字面含义,与精益中的看板有区别。


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

本文原文发表于InfoQ:http://www.infoq.com/cn/articles/ci-theory-practice

Share

我和敏捷团队的五个约定

我——作为一名测试人员——有一个与众不同的习惯:每当要加入一个新项目的时候,我总会找到项目中的同伴,真诚而亲切地说:“为了更好地合作,我有5个约定,希望大家能尽量遵守”。

约定1. 业务分析师们,我们其实是同一个角色的两种面孔,请叫上我们参加客户需求会议

我们的团队需要让客户频繁的得到可用的软件,客户的不断反馈会给软件的未来做出最正确的方向指引。

如果我们交付的软件有很多质量的问题,存在大量的缺陷,客户会被这些缺陷的奇怪行为干扰,没有办法把注意力放在软件本身的价值是否符合他们的真正需求上, 不能给出最有价值的反馈。所以,我们只有频繁的做测试,在每次交付之前都把质量问题找出来告诉我们的团队,问题才能及时的得到改正。

而我坚信“prevention is better than cure”(预防胜于治疗),我会要把工作的重点放在预防缺陷上,这样可以节省Dev们很多修复缺陷的时间与精力。

为了达到这个目的,我需要跟你一起参加客户需求会议,尽早的了解客户需求与使用软件的惯常行为。那么在你完成需求的验收条件的定义的时候,我也基本完成了测试用例的准备。

我们可以赶在开发人员们写代码之前就告诉他们我要测什么,让他们减少因为过于乐观而漏掉的一些重要的有破坏性的情况,减少缺陷的发生。这是我测试的一项重要任务。

如果你们在大部分需求都整理好了再交给我们,我会浪费掉等待的时间。更重要的是,开发好的软件里面已经有很多本来可以不存在的缺陷在里面了,开发人员们可能需要加班加点来保证在项目最终交付时间之前把它们改好。这样很容易产生新的缺陷的。

所以,请让我尽早了解需求,请不要让我到项目后期才能开始测试。

约定2. 开发人员们,虽然你们是编写自动化测试的专家,但请听听我们意见

我知道,对于你们,自动化测试不过是利用junit, rspec, selenium,watir,uiautomation等等写出的“另一段程序”而已。而对于80%的QA来说,编写自动化测试并不是一件简单的事情。

不过我仍然相信,有测试人员介入的自动化测试更有价值。

你们用单元测试,集成测试来保证代码的质量。然而你们的这些日常测试离代码更近,离最终用户还点远。很多测试都不是在测软件功能。

你们可以把功能测试写的又快又多,而我们可以指出什么功能测试最有必要加到自动化测试中。

你们平时大部分精力都在编码上,没有太多时间去查都有什么缺陷。而我们可以指出什么地方缺陷可能会出现的比较频繁,建议在这些脆弱的地方加自动化测试。

所以请听听我们的意见,我们可以给你们提供这些信息。

约定3. 项目经理们,请不要要求我们测试软件的所有路径

软件测试是一个永无止尽的任务。基本上没有什么软件简单到我们能够尝试完它的每一个可能的路径的。就连一个看似简单的微软计算器都有无穷尽的路径,无止尽的输入,更何况比这个更复杂的商用软件。

如果你们担心没有尝试过全部的路径不可靠,疑惑我们怎么敢说这个软件质量是好的还是坏,都有什么风险。请你们先注意,我们是跟业务分析师一样,都了解软件的价值的。价值可以帮我们做出判断,什么时候可以停止测试并对客户说我们的软件已经满足您的要求了,请放心使用。

因为我们了解价值,我们可以肯定的说哪些软件的使用方式是至关重要的,哪些是不太可能出现的。我们会在全面测试了软件以后,把主要精力放在价值高的功能点上。合理的利用项目有限的时间。

因为我们了解价值,我们可以正确的把发现的问题分类。我们可以帮助dev们把精力放在重要的缺陷上,避免把时间放在对于客户微不足道却不得不花费大量精力才能修正的问题上。

所以,请不要要求我们无止尽的测试一个软件。我们了解价值,请相信我们的判断。

约定4. 迭代经理们,如果对于交付风险有任何疑问,请来询问我

BA和Dev们都是关注一个软件在什么情况是可以良好的工作。而我们除了验证这些情况以外,大量的时候都用在寻找什么样的情况软件不能正常的运行。所以除 了针对定义好的软件行为进行测试,我们还会做很多探索性测试。我们通常可以通过这样的测试发现一些没有定义的、不曾预期的行为。这些行为往往将会构成软件 交付的风险。

我们会告诉你们现在都发生了什么问题,分别分布在哪里。

我们会告诉你们,在什么情况下软件可能会有异常行为,是不是会牵连到其他的部分,是否可以绕过去。

我们会告诉你们,哪些部分功能比较不稳定,需要更多的留意。

约定5. 测试人员们,那些敏捷实践对于我们也是有用的。

结对不是dev们的专利。我不希望总见到你们独自坐在自己的位置上冥思苦想。走出去,跟其他队友多多交流!

多跟测试队友交流,pair看看设计的测试用例是不是够全面,独自一个人想到的未必足够好。他们会给你诚恳的意见的。对他们,也请一样认真对待。

如果你发现开发人员们做出的架构决定使测试工作变得更困难。那么请大声地告诉他们,design for testability(提高你们设计的可测性)。

如果你发现业务分析师写的需求无法验证,定义的客户行为不够具体,一个用户故事中包含太多了功能点,等等,那么也请大声地告诉他,INVEST(独立,可协商,价值,可估算,短小,可测)。

也请你们多跟开发人员结对写自动化测试,既可以帮助你们学习怎样更好的编写自动化测试,也能帮助开发人员们结对更多的了解用户行为。

这就是我的五个约定,它们是我在团队中顺利展开工作的基础。


作者:覃其慧,ThoughtWorks敏捷咨询师。她参与了大量的敏捷软件开发的实践和敏捷咨询。目前主要关注以价值为驱动的敏捷测试。


本文原文发表于InfoQ:http://www.infoq.com/cn/articles/thoughtworks-practice-parti

Share