用脚本自动转换和发布blog

Published on Tuesday, December 30th, 2008

昨天我在一篇 blog 里提到了自己想做的一自动化转换和发布blog的想法。说干就干,在一番google之后我写了下面这段脚本来自动转换blog:


require 'rubygems'
require 'redcloth'

if ARGV.length < 1 then
  puts "please tell me which textile file to convert: bc.rb [FILE]"
else
  filename = ARGV[0].split('/').last.split('.').first
  blog = open(ARGV[0]) { |f| f.read}
  textile = RedCloth.new blog
  open('html/' + filename + '.html', 'w') {|f| f << textile.to_html }
end

这段代码会把输入的textile文件自动转移成html文件。为了区分文件的类型,我重构了本地的blog目录,把原有的blog文本文件从blog根目录挪到了textile目录下,同时在textile同一级创建了html目录,用于保存转换以后的结果。

格式转移搞定了,接下来就是自动发布。google了一番以后,我又写了下面这个脚本:


require 'xmlrpc/client.rb'

if ARGV.length < 1 then
  puts "please tell me which html file to post: bc.rb [FILE]"
else
  blog = ARGV[0]
  blog_body = open(blog) { |f| f.read }
  blog_content = {:title => 'test', :description => blog_body }

  proxy = XMLRPC::Client.new("dyang.wordpress.com.cn", "/xmlrpc.php", 80)
  begin
    puts "posting new blog:"
    resp = proxy.call("metaWeblog.newPost", "1", "myusername", "mypassword", blog_content, false)
    puts resp
  rescue XMLRPC::FaultException => e
    puts "ERROR: Code: #{e.faultCode}"
    puts "ERROR: Msg: #{e.faultString}"
  end
end


这段脚本非常好理解,它调用了Wordpress支持的metaWeblog API,通过传入适当的参数来实现blog的发布。但是目前我只能用它来发布英文内容,一旦标题或者正文里有中文字符就会返回一个id为-32700的格式错误。google了一下, 这里 提供了一种解决方案,但它要求修改服务器端的一个文件,而我的blog提供商是不允许这么做的。这让我很失望。看来,我需要再找一个地方托管我的blog了。

顺便提一下的是,同事 Tin 提醒我Wordpress也支持邮件发布。然而让我再次失望的是,我现在的blog提供商并没有开放这个功能,而且也没有办法自己上传或者加载plugin去支持。发邮件去问也没有回应,看来我也许真的要考虑迁出了。


重构blog

Published on Monday, December 29th, 2008

同事Tin前几天写了一篇非常棒的blog 《重构我的blog》 ,讲的是通过把树形结构的category分类转换成扁平结构的tag分类来更好地组织自己blog的内容,并方便外界订阅和查询。这篇文章给我了很多启示。联想到自己这段时间在使用blog时遇到的一些问题,我觉得是时候把自己管理blog的方式来重构一下了。

问题

既然是重构,那么肯定是要解决现实的问题。我在管理自己的blog的时候遇到的主要一些问题是:

  • 本地编辑:我现在发布blog的方式是先在本地把内容写好,然后再通过Wordpress的在线编辑器进行简单的编辑,对一些比较细的效果用html直接编码,预览满意后就发布出去。不过,我一直很不习惯使用在线编辑工具,一方面是因为我是个狂热的vim/gvim用户,很难接受在线编辑工具的简陋和低效;另一方面,在线编辑受到的限制实在太多,稍一不慎就会session超时或者内容丢失,让人抓狂。因此,我希望能把编辑这一部拿到本地用vim/gvim来做。
  • html转换:大家可以看到,我blog上面所用的格式并不复杂,基本上也就是加一些链接,分几个小节,或者引用一下代码等等。这些都可以用非常简单的html代码来搞定。但即使是简单,我也不太习惯每次写完以后都要手动地添加html代码。程序员的本能使我希望自己只用一种表达形式把blog写出来,然后让格式转换交给工具去做。
  • 同步:我在本地有一个专门的目录用于存放blog,用git来管理。每一篇blog都先在本地以文本格式写好,然后再发布出去。在发布之后,我常常需要做一些细节上的修改,比如说改错别字等。通常我就随手在线修改了,而这就意味着在线的版本和我本地的版本不一致,本地再使用版本管制工具也就失去了意义。我要求blog的创建和后续修改都只在一处进行,其它位置的更新都应该通过同步来完成,这个过程一定要自动化。
  • 集成:Cruise的工作背景让我要求把上面这些步骤都自动化起来,除了写blog要由人来亲自做以外,其它的工作都是重复性的,应该交给机器去做。最理想的情况下是在本地写好blog文本,然后运行一个rake/ant命令,在瞬间完成格式转换和远程同步。

解决办法

  • 对于blog内容我肯定会用vim/gvim来编辑,现在的问题是我还需要一个能运行在Linux上的Wordpress客户端来编辑其它的blog metadata(如tag,分类等)。目前我google的结果是 drivel 。我会在最近花一些时间来试用并且把试用结果写出来。
  • 对于html转换,我最初的想法是用xml来写blog,然后用xslt把xml转换成html。这个过程可以用rake来自动化。昨天我按这个方式试了试,感觉很不好。一方面是我要自己定义一套blog的schema,感觉很轮子;另一方面是我很讨厌写那些方括号,因为我不直接用html来写blog的原因之一就是想避免写那些没什么意义的方括号。所以就把这个方案放弃了。第二套方案是用texile来写blog,然后用现有的texile library来转换成html。这种技术现在已经非常成熟了,如果用Ruby的话就有Redcloth可用,两行代码就能搞定问题。再写几行rake代码就可以很方便地把整个过程集成起来,感觉非常爽。这两天我就要抽时间把它做出来。
  • 同步方面,我初步的想法是学一下Wordpress的Xml-rpc API,然后写脚本来调用。这方面技术应该已经非常成熟了,所以我对实现这一点很有信心。
  • 上面这几个方案都是用脚本来自动化的。这样我就很容易用rake把整个过程串起来,实现一键发布的目的。

总结

这是我第一次尝试用自己平时工作中的一些技术(比如写脚本)和思路(比如消除重复和进行自动化)来改善自己管理blog的方法。有一些东西也许看上去必要性不是很大,不过想一想这么做还是挺有意思的。而且更让我感兴趣的是延着这个思路下去还会有什么更有意思的事会发生。:)


一次有趣的开发经历

Published on Sunday, December 28th, 2008

周五下午和同事Pavan一起结对编程,为Cruise 1.2增加svn external的支持。几个小时过后,我们提交的代码漂亮地通过了所有的单元/集成/验收测试。在这个过程中,我觉得有一些很有趣的细节非常能体现出我们的工作方式,所以现在把它们列出来:

集体重构

我们的修改是以前一段时间项目里大伙凑在一起做的一些修改为基础而进行的。在Cruise项目上,我们有一个不成文的习惯,就是隔一段时间大伙就凑在会议室里,拿一台笔记本连上投影仪,找出一段写的不好的代码做集体重构。每次开始的时候会有人负责提出一个问题,例如一段写的很糟糕的代码,或者一个看上去很牵强的设计,然后大家一起集思广议想解决办法,并由一个人拿着电脑对着投影仪直接修改。在这个过程中有时候写代码的人思路会中断,这时就会有人抢过电脑来继续写。这种方法的效果非常不错,特别是当大伙不断对一个问题追问“为什么”的时候,往往会让我们原本非常狭窄的思路一下子拓宽起来。

分布式版本控制

Cruise选择Mercurial做为版本控制系统。大家在集体写了svn external的初始代码之后,在一台笔记本上留下了一个patch。我和Pavan要做的第一件事就是把这个patch迁移到我们工作所用的pair station上来。方法非常简单:首先把那个patch通过scp拿到本地来,然后运行hg qimport指定patch文件的存储路径就可以了。当然,还可以用hg pull把那台笔记本上的修改都pull过来,或者利用hg transplant把那个指定的版本pull过来。这几种方法都可以达到我们的目的。前一种操作多一些,但会把拿过来的patch加入到本地的queue中,是我个人最习惯的用法;第二种操作很简单,但是有可能pull过来的修改过多;第三种操作简便,效率理想,只是并不会把拿来的patch加入到queue里,我们平时会穿插着使用。

工欲善其事,必先利其器

我们必须要在本地创建相应的测试环境才能验证svn external功能,但是我们并不想每次都手动地去svn co, svn propset, svn ci, svn up, svn propget…。这些重复的操作应该而且也非常适合加以自动化。解决办法非常有趣:我们在本地创建了一个新的目录,在里面写了几个shell脚本。第一个用于创建svn external环境,其中包括复制出一个用于测试的svn repository,把它check out到一个指定的客户端目录,在客户端目录里执行svn propset/ci来创建svn external,以及执行svn up来更新客户端目录。第二个脚本用来启动Cruise Server和Agent。第三个脚本用来停止Cruise Server和Agent。有了这几个脚本,我们就能非常方便高效地运行测试了。当然,我们没有忘记把这些脚本都提交到Cruise的repository里去。

说到工具,我还要顺便提一下我们的开发环境。Cruise团队里的每一个pair除了各自拥有公司配发的笔记本以外,还共同拥有一台非常强大的开发机器(Core 2 Quad CPU * 4, 4G RAM),采用双24寸液晶显示器。操作系统用的是Ubuntu,开发工具用的是IntelliJ。这些强大而灵活的工具让我们能够最大程度地激发工作热情,提高工作效率。

测试,测试

我和Pavan在测试过程中发现了一个bug,凭经验我们很快找到了导致bug产生的一个方法。不过,我们并没有急于把它消灭掉,而是给现有的方法增加了一个测试。当然,这个测试是不通过的,所以我们接下来的目标就是改正代码,让测试通过。几分钟过后,我们消灭了bug,并且使得系统的测试覆盖率又小小地增加了一点。:)

持续集成与及时反馈

当我们通过了本地的所有单元和集成测试以后,就信心满满地把本地的提交push到了汇总的mercurial repository里,很快,我们自己UAT环境里的Cruise Server就检测出了这次代码提交,并且开始进行集成。我们的第一个stage是在Linux和Windows平台下并行运行所有的单元测试,然而这个stage中的一个Windows job却在几分钟后失败了。通过阅读Cruise给出的失败信息,我们发现刚刚提交的代码没有考虑带空格的URI这一种情况,而这种情况在Windows下非常常见。获得了这个反馈,我们接下来的工作就是在本地增加一个处理带空格的URI这种情况的测试,看它运行失败,然后修复,提交。不一会的功夫,Cruise显示所有测试全部通过!


Sunbird

Published on Tuesday, December 16th, 2008

这段时间我一直在找一个适合自己的日历软件,我的需求是:

- 不要包含邮件功能。我在notes上处理公司邮件,在gmail里管理个人邮件,不希望再引入一个多余的邮件管理软件。
- 支持Linux,因为那是我日常最主要使用的操作系统。
- 支持CalDAV协议,这样我就可以通过它来访问我在google calender上的日历
- 可以管理多个日历。我在google calender上面除了有个人的日历以外,还有一份和家人共享的日历。
- 简单,除了日历功能以外不要加入任何其它多余的功能。

经协一番搜寻,我最终选定了Sunbird


自动删除不需要的方法调用

Published on Saturday, November 22nd, 2008

在ThoughtWorks,我们对重构时可用的一些技巧非常着迷。相比于挽起袖子来进行一次次“胆大包天”的大幅重构,我们更喜欢把重构分解成一个个小且可控的步骤,分而治之。下面就是一例:

上周五,我和克里斯同学在测我们新写的数据库迁移脚本时发现代码中反复出现下面两句:

DbFixture db = new DbFixture();
db.initialize();

问题出现了:1)当DbFixture被实例化以后,它还不能被直接使用,用户必须要调用initialize方法去初始化其状态,否则任何对dbFixture实例的方法调用都会导致未知的错误;2)用户在每次使用DbFixture的时候都必须要重复这两句方法调用,这是一种重复。

在仔细读了initialize方法之后,我和克里斯认为DbFixture的构造函数完全可以自己调用这个方法,从而可以解决上述两个问题。那么怎么改呢?

方法一:

1)修改DbFixture的构造函数,让它在退出函数前调用initialize方法。
2)手动找到并删除所有调用initialize的外部代码。
3)把initialize从public访问级别改成private。

这种方法的好处是比较直观。但问题在于第2步比较费力,因为我们必须要手动去删除每一处外部引用,而我们当时已经有好几个测试类在引用initialize方法,这让我们又找到了方法二。

方法二:

1)在DbFixture内复制initialize并生成一个initialize_temp方法,确保其内容和initialize完全一致。
2)修改DbFixture的构造函数,让它在退出函数前调用initialize_temp方法。
3)删掉原有initialize方法的方法体,让它成为一个空方法。
4)在initialize上执行inline重构(在intelliJ里意味着运行Ctrl + N)。所有外部调用全部自动删除!
5)把initialize_temp重命名为initialize。
6)把initialize从public访问级别改成private。

方法二的关键点在第4步,通过适当运用inline自动地删除所有的外部调用。


寻找问题的根源

Published on Wednesday, November 19th, 2008

上午我和胡凯同学结对重构Cruise中的一段自动化测试代码(没错,我们不会因为一段代码是测试代码而降低对它的质量要求)。其中的一个方法是这样的:

class Assert {
    public static void assertWillHappen(CruiseSession cruiseSession, Matcher matcher) {
        while (within(ONE_MINUTE)) {
            try {
                if (matcher.matches(cruiseSession)) {
                    return;
                }
            } catch (PipelineNotFoundException e) {
                // ignore and continue
            }
            sleep();
        }
        assertThat(cruiseSession, matcher);
    }
    // ...
}

这段测试代码会在一分钟之内不断轮循后台,如果系统状态通过了matcher所定义的检测条件就成功返回,否则就会产生一个断言错误,从而使测试失败。然而,它看上去却让人很不舒服。原本很简单的逻辑因为其中加了一个try/catch而变得复杂。虽然这样做是有原因的(在测试代码运行的时候,由于系统中使用了缓存,因此如果测试机器运行略微慢一些的话,在前面几秒钟之内访问是有可能产生pipeline还未创建完成的情况的),但补获异常并且吃掉它本身这种非常hack的做法却让我们非常起疑,多年的经验告诉我们,这里面一定有更深层次的问题!

仔细查看代码,我们发现异常是从下面这段代码里产生的:

class Pipelines {
    public Pipeline findPipeline(String pipelineName) {
        for (int i = 0; i < this.size(); i++) {
            Pipeline pipeline = get(i);
            if (pipeline.is(pipelineName)) {
                return pipeline;
            }
        }
        throw new PipelineNotFoundException("Pipeline " + pipelineName + " is not found");
    }
    // ...
}

看上去这段代码也没什么问题,我们在系统状态不正确的时候通过防范性手段来把问题马上暴露出去,这似乎说的过去。但是,这个异常却使得调用代码逻辑变得复杂起来,这问题该怎么解决呢?

这时,胡凯同学想到了一个好办法。他拿起键盘,迅速地把代码改成了下面的样子:

class Pipelines {
    public Pipeline findPipeline(String pipelineName) {
        for (int i = 0; i < this.size(); i++) {
            Pipeline pipeline = get(i);
            if (pipeline.is(pipelineName)) {
                return pipeline;
            }
        }
        return Pipeline.NULL;
    }
    // ....
}

Pipeline.NULL会创建一个NullPipeline,当matcher检测它的状态的时候,只需要简单地返回一个合适的默认值就可以了。这样一来,我们前面说的那段代码就被简化成了:

class Assert {
    public static void assertWillHappen(CruiseSession cruiseSession, Matcher matcher) {
        while (within(ONE_MINUTE)) {
            if (matcher.matches(cruiseSession)) {
                return;
            }
            sleep();
        }
        assertThat(cruiseSession, matcher);
    }
    // ...
}

这次重构使我再次感受到了,当一段代码出了问题的时候,不应该简单地头痛医头、脚痛医脚,而应该去探寻产生问题的真正根源。


有选择地创建Mercurial patch

Published on Sunday, October 5th, 2008

在使用mercurial的过程中,有时我们需要基于当前修改的文件的一部分来创建patch。举例来说,在修改了多个文件之后,你忽然发现其中的几处修改属于一个重构,而另一些修改则是在添加新的功能。你想把重构的那部分修改独立出来,这样你就可以自由选择什么时候提交重构,什么时候提交新功能了。

一种解决办法是创建patch。hg qnew命令提供了-X和-I参数来指定哪些文件中的修改应该被排除或者添加到patch之中。

比如,你目前已经改动了file1和file2,而你只想把file1的修改纳入patch之中。那么就可以执行:

hg qnew -m ‘patching changes to file1…’ -f mod_file1.diff -I file1

或者:

hg qnew -m ‘patching changes to file1…’ -f mod_file1.diff -X file2

你也可以用下面的命令来达到同样的效果:

hg qnew -m ‘patching changes to file1…’ -f mod_file1.diff file1

这样生成的patch就只包含对file1的修改了。


自有产品开发的创新特性

Published on Sunday, October 5th, 2008

Cruise是我参与过的第一个自有产品开发项目,在这之前的几年里我都一直在做离岸外包。几个月时间下来,对于外包和自有产品开发的差异我着实体会到了不少,而其中最让我印象深刻的就是自有产品开发所具有的强烈的创业感。这集中体现在:

1 产品(而不是项目!)成功与否的未知性

对于做产品的团队来说,其所开发的产品往往集中体现了其所在机构的某种商业计划,而且在大多数时间里这种商业计划体现了一种对未来的投资。比如对于Cruise来说,它体现了ThoughtWorks致力于调整其商业模式和收入模式的一种尝试。ThoughtWorks Studio的成立就是一项目对未来的投资,而Mingle、Cruise、Twist等产品都分别是整个投资计划中的不同环节。假如投资失败(希望不要发生!),那么其影响力不亚于一次创业的失败,因此从这个角度来说,产品开发就是一次创业。

做外包虽然也要交付产品,但是外包提供方和发包商之间的权责是受合同约束的。通常情况下,外包服务提供商只对项目交付负有合同约定的责任,而发包商则要负方向性的责任。也就是说,如果项目失败了,前者失去的是一个客户以及做为服务提供方的信誉,而后者却要对整个产品、项目在整个商业计划中的失利负责。两者之间虽然有不可分割的联系,但其本质却是截然不同的。因此,外包项目的成员往往不会对整个商业链条有完整的理解,其创业感也自然也就不够强烈甚至不存在了。

2 创新与生俱来的未知感

创业从本质上来讲是一个构思+执行的过程。而这一点和一个创新型的产品研发过程非常吻合。创新本身所具有的未知性会让所有参与者既激动又不安。这一点在Cruise项目上体现得淋漓尽致。Cruise体现了ThoughtWorks在持续集成和发布管理领域的理念和最佳实践,由于这些理念和最佳实践完全来自ThoughtWorks十几年的实际项目实践,因此没有任何人可以在任何的学术资料里找到参考,而只能依靠群体的智慧一点点沉淀和提炼。

比如说,Cruise 1.0与市场上同类产品最大的区别就在于我们对整个持续集成和发布过程进行了建模,创新性地提出了pipeline的概念。在Cruise之前,市场上的主流同类型产品均是以项目为单位,每一次构建均是对一个项目的构建(编译、测试、打包等),而引入pipeline以后,用户可以根据自己的需要把整个过程进行建模,从而实现从源代码改动到质量保证再到最终部署的全过程自动化。这样最大的好处便是把产品的整个发布周期──从源代码改动到最终的部署──完整并且形象地表现出来,从而让用户在关注细节的同时不失对整个价值链的宏观把握。这一概念在Cruise 1.0推出后获得了用户极大的好评,一些同类产品也开始跟进。可以说,做为一种创新,pipeline取得了应有的成功。然而并不是任何创新都会取得成功,在推出pipeline背后所涉及到的权衡和思考是市场所感受不到的,pipeline成为了Cruise 1.0能否成功的最大赌注。

当然,pipeline并非唯一的创新之处,Cruise中很多细节都包含了我们对持续集成和发布管理的独特思维。在Cruise 1.0发布之后,我们根据市场上用户的反馈,对其中的很多细节进行了调整。然而,做为一项创新型的产品,Cruise并没有把自身定位在仅是满足市场眼前的需求即可。Cruise团队的一项产品设计理念是,永远不要仅仅提供那些最终用户要求的功能,而要留出足够的空间来引入我们的创新。市场是不会对创新提出明确的需求的,这时候就需要产品设计团队能够运用经验和想像力来引领市场了。

两相比较,外包从本质上而言并没有远离创新。我在几年前曾经参与设计过的一个产品还大量地引入了很多有别与行业同类型产品的设计元素。然而,外包本身的合作特点使得不是所有的团队都能自如地设计产品。有很多团队往往被放置在了价值链的最末端──他们只是被当做了装配产品的工匠,而设计往往成为了价值链上游小部分人的专利。在这种合作模式下,创新设计成了外包提供方团队难以奢求的梦想。毕竟还有那么多的东西(了解复杂的业务逻辑、提升性能……)要去考虑,又有多少团队能够有多余的精力去涉足产品设计上的创新呢?

3 多角色团队,更高的参与度

Cruise是一个很有趣的项目──我们这些项目成员自己就是产品的最终用户。我们每天都在用Cruise来开发Cruise!在这种情况下,我们每个人都对目标业务领域有很深的认识,而且熟能生巧,我们很容易对产品的功能提出自己的观点。比如说,我们常常需要去了解一个build失败到底是由于功能问题引起的还是受环境影响了,这时我们往往就要重新运行一下整个pipeline(在Cruise 1.0里我们提供了手动运行一个pipeline的功能)。然而,我们的pipeline里包含了多个stage(每一个stage可以被理解为整个发布环节中的一个阶段,一些典型的stage包含dev、ut、ft、UAT、prod等等。),每次重新运行都颇为耗时,因此我们希望能加入一个重新运行stage的功能。在经过了充分的内部讨论和分析以后,我们决定在1.1版本中加入这个功能。目前,这个功能已经基本开发完成(我很幸运地参与了这个功能的开发,呵呵),很快就会随着1.1版本而发布了。

在这种情况下,每个团队成员往往扮演多个角色──开发人员有时会变成BA(business analyst)或者QA,BA有时会参与开发人员的设计讨论,并为开发人员的建模提出领域上的依据或者意见……团队成员之间非常紧密地彼此协作,我们的目标是设计和开发出一套自己喜爱并且对用户带来价值的产品。在实现这个目标的过程中,角色只会使我们的专业技能更为酣畅淋漓地展示,而丝毫没有带来任何程度上的阻碍。

相比之下,我所参与过的外包项目都或多或少地缺乏这种自由度。由于游戏规则已经在商业合同中做出了明确的约定,开发团队在发包方的整个价值链条里的地位往往是固定而且有限的。信息的不对称使得开发团队无法全面地了解产品的来龙去脉(我参与过的一个项目上,发包商甚至有意在我们和最终用户之间做出信息过滤,开发团队基本无法了解最终用户的真实反馈,更何谈去根据这些反馈做出调整了),而名不正言不顺的道理也使得开发团队无心去做什么“分外之事”,只是集中精力去写代码。在这种情况下,做的越少出错的可能性越小,谁还愿意去多做些什么呢?

以上是我根据过去几年的项目经验总结出的一些感想体会。由于项目的特性往往决定了每个人的看法,因此也许这些感想并不反映二者的本质差异。不过这也并不要紧,因为体验本身就是挺有趣的一个过程。:)


破窗子效应

Published on Monday, August 18th, 2008

《程序员修炼之道》一书把破窗子(broken window)效应从社会学移植到软件开发过程中来。它认为,一个小的破败的际象(比如说一幢楼里有一扇窗子被打破了),如果不加处理,那么久而久之就会被放大为整个事物(整幢楼)的破败。

这一现象在软件开发团队中也同样适用。初期良好简洁的设计往往维持不了太久,随着商业需求的变化,团队人员的更换,越来越多的边缘情况被发现,设计中会出现越来越多的欠佳之处。比如开发人员可能需要一个服务类来做LDAP查询,但现有的一个类可能不太容易复用,这时交付的压力又使得开发人员没有充足的时间去重构现有的类以满足新的需求,只好临时写一个新类满足眼下之需,同时寄希望于日后有时间再把重构补上;再比如系统中可能大量采用mock测试对类进行单元测试,其中有些mock测试非但效果不好,而且在修改的时候还带来负面影响,这时候后续的开发人员往往倾向于向现有“标准”看齐而不是花费更高的成本去重写那些质量不高的测试。(这两个都是我遇见过的真实例子!)这些问题的积累都会导致系统中的破窗子越来越多。量变到了一定程度就会产生质变,如果开发团队无法把一些关键的破窗子修好,那么不但影响士气(大家一想到那些破窗子就会头疼),而且也会减缓团队的交付速度,同时由于这种成本是隐形而且很难量化的,如果处理不好它往往会导致客户与交付团队、管理层与执行层之间产生误解,并进而带来其它的问题。

解决这一问题的最根本方法是诚实、沟通和信任。诚实使得开发团队能够看到并且承认自己面临的问题,沟通确保开发团队眼中的问题能够被管理层或者客户看到,信任让决策者能够排出正确的优先级,在追求显性的商业价值的同时也不忽视隐性的商业成本。只要上述三点做好了,那么问题的存在就不足为惧了。


团队设计能力的交接

Published on Monday, August 18th, 2008

在实践中,绝大多数软件开发项目在初期都会配备相对资深的设计人员,他们通过自己的经验以及设计/实施能力往往能够确保项目成功启动并且步入正轨。当项目进入成熟期以后,出于成本的考虑,这一部分资深人员往往会被调离项目,从而对技术交接带来了很大的挑战。在我过去几年参与过的项目中,这种情况就屡屡发生。在我刚刚离开的一个项目上,初期投入的几名资深人员(一名架构师,一名高级DBA,一名高级程序员)先后在不同阶段被调离了项目,而项目的tech lead也在一年半之内更换了三位。这就直接导致了项目的一些关键设计思路没有被很好地理解和传递下去(当然,我们也不能理所当然地认为所有的初期设计思路都是对的、都需要保留)。

举例来说,项目初期采用了领域驱动的设计方式,tech lead领导团队对目标领域中的核心概念进行了分析,并提炼出了一套当时行之有效的模型。然而,具体的代码虽然容易理解,而为什么要进行领域建模、如何建模、建模时到底追求哪些目标、有哪些东西是我们想通过设计去避免的……这些最根本的东西却很难通过短时间被所有开发人员理解。换句话说,领域驱动设计的过程要远比它所产生的结果重要,而这个过程代表着一种能力,是一个成长中的团队很难在短时间内真正掌握的。这也就导致了tech lead几度更换、代码基数越来越大以后,不断产生的代码质量不高,一些核心的设计点被破坏。比如说,一些本应被封装在领域模型里的代码被直接写在服务层里,一些实体对象的唯一标识(identity)并不唯一等等。

解决这种问题没有什么妙方,项目的管理人员应该对项目的设计能力有一个客观的把握,而且要随着时间的变化注意观察这种能力,在设计能力与商业成本之间尽可能做出一个平衡来。这一点其实往往挺难做到的,因为在现实中,特别是在外包这个成本敏感的领域里,商业决策往往影响着甚至直接决定着技术决策(如果有可能的话一定避免这么做!)。比如在我曾经服务过的一家软件公司里,项目上的高级技术人员往往在展露自身实力后就被调离到其它项目去,要么负责启动,要么负责救火。这种情况下指望原有团队能够不失真地把设计思路接下来是很难的。

当然现实也并不总是这么悲观,良好的团队内部沟通在很大程度上能把潜在的问题降到最低。比如说,敏捷开发所提倡的结对编程就在实践层面上对解决这一问题提供了一种思路。设想一下,如果一名有经验的开发人员和一名初级开发人员有着充足的结对时间,那么前者的一些好的设计思路、开发经验甚至编程习惯就会潜移默化地影响到后者,这对于帮助后者提高自身设计能力是再好不过的方式了。