<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" media="screen" href="/~d/styles/atom10full.xsl"?><?xml-stylesheet type="text/css" media="screen" href="http://feeds.feedburner.com/~d/styles/itemcontent.css"?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0">
 
 <title>Happy Hacking</title>
 
 <updated>2009-08-01T19:44:32-07:00</updated>
 <id>http://dyang.github.com</id>
 <author>
   <name>Derek Yang</name>
   <email>yanghada@gmail.com</email>
 </author>

 
 <link rel="self" href="http://feeds.feedburner.com/dyang" type="application/atom+xml" /><atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="hub" href="http://pubsubhubbub.appspot.com" /><entry>
   <title>用模板（template）格式化mercurial输出</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/nNKQb2lgmrQ/hg_template.html" />
   <updated>2009-07-27T00:00:00-07:00</updated>
   <id>http://dyang.github.com/scm/2009/07/27/hg_template</id>
   <content type="html">&lt;p&gt;做为一款可能被众多第三方工具集成的工具软件，Mercurial内置了模板（template）功能，允许用户自定义Mercurial命令的输出格式。这不，今天我和同事 &lt;a href="http://www.blogjava.net/programmer/"&gt;许伟&lt;/a&gt; 就需要把hg log的命令输出格式化成xml。&lt;/p&gt;
&lt;p&gt;根据 &lt;a href="http://hgbook.red-bean.com/read/customizing-the-output-of-mercurial.html"&gt;这里&lt;/a&gt; 的介绍，hg log支持&amp;#8212;template参数，允许用户以文本形式结合Mercurial内置的大量关键字来定义想要的格式模板。这样，一个以xml为目标结果的hg log模板就大概是这个样子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;changeset&amp;gt;
  &amp;lt;rev&amp;gt;{node}&amp;lt;/rev&amp;gt;
  &amp;lt;author&amp;gt;{author|escape}&amp;lt;/author&amp;gt;
  &amp;lt;desc&amp;gt;{desc|escape}&amp;lt;/desc&amp;gt;
  &amp;lt;files&amp;gt;
    &amp;lt;added&amp;gt;{file_adds|stringify|escape}&amp;lt;/added&amp;gt;
    &amp;lt;modified&amp;gt;{file_mods|stringify|escape}&amp;lt;/modified&amp;gt;
    &amp;lt;added&amp;gt;{file_adds|stringify|escape}&amp;lt;/added&amp;gt;
    &amp;lt;deleted&amp;gt;{file_dels|stringify|escape}&amp;lt;/deleted&amp;gt;
  &amp;lt;/files&amp;gt;
&amp;lt;/changeset&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请注意其中对stringify过滤器的使用。这个在文件处理过程中是必需的，因为file_adds等对象在Mercurial内部被称为generator，它们不是字符串，所以没有替换方法，不能直接用escape过滤器去做替换。这么用看上去像是个workaround，而事实上它也的确是。大家如果感兴趣的话可以参考Mercurial的 &lt;a href="http://mercurial.selenic.com/bts/issue1164"&gt;这个bug&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;然而，这个模板存在一个问题：默认文件名是以空格来彼此区分的，所以如果有的文件名中包含空格，那么出来的结果是无法正确区分此文件和彼文件的。为了解决这个问题，我们就得用Mercurial的一个更高级的方法──样式（style）。&lt;/p&gt;
&lt;p&gt;和模板类似，样式也是以文本加关键字的组合来定义一个模板，但是有两点不同：1）样式对格式的要求更严格，2）样式必须保存在文件中。下面就是按照样式的要求重新定义的文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
changeset = '&amp;lt;changeset&amp;gt;\n&amp;lt;rev&amp;gt;{node}&amp;lt;/rev&amp;gt;\n&amp;lt;author&amp;gt;{author}&amp;lt;/author&amp;gt;\n&amp;lt;date&amp;gt;{date|date}&amp;lt;/date&amp;gt;\n&amp;lt;files&amp;gt;\n&amp;lt;modified&amp;gt;\n{file_mods}&amp;lt;/modified&amp;gt;\n&amp;lt;added&amp;gt;\n{file_adds}&amp;lt;/added&amp;gt;\n&amp;lt;/files&amp;gt;\n&amp;lt;/changeset&amp;gt;'
file_mod = '&amp;lt;file&amp;gt;{file_mod}&amp;lt;/file&amp;gt;\n'
file_add = '&amp;lt;file&amp;gt;{file_add}&amp;lt;/file&amp;gt;\n'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中值得一提的是file_mods等关键字的使用，当Mercurial发现一个复数的关键字引用的时候，就会在定义文件中找它的单数定义。比如说我们在changeset中定义了{file_mods}，Mercurial就会在文件中寻找file_mod的单数定义。file_adds、file_dels等也都是一个道理。这样我们就可以很细粒度地去控制单个文件的输出格式了。&lt;/p&gt;
&lt;p&gt;让人略感遗憾的是，Mercurial对样式如何使用介绍的非常有限。大家如果感兴趣，可以去研究Mercurial自带的几个样式文件，它们位于Mercurial安装目录的templates下面。&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/nNKQb2lgmrQ" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/scm/2009/07/27/hg_template.html</feedburner:origLink></entry>
 
 <entry>
   <title>用hg qimport和qfresh来合并未提交代码</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/a1WS6NrKUfA/qimport.html" />
   <updated>2009-06-20T00:00:00-07:00</updated>
   <id>http://dyang.github.com/scm/2009/06/20/qimport</id>
   <content type="html">&lt;p&gt;刚才发现自己的某一个hg工作目录里还有少量的修改没有提交，通过hg diff发现自己只是改了一个很显的spring使用错误，我不想针对这次修改再做一次单独的提交，而是想把它合并到此前的那次提交里，我该怎么办呢？&lt;/p&gt;
&lt;p&gt;如果我在一开始用的是hg qnew的方法来提交代码的话，那么问题就变的很简单了，我只需运行hg qrefresh即可。但是我当初用的是hg ci，也就是说，本地并没有一个被queue管理的patch可以刷新。那么，是不是还可以通过qrefresh的方法来解决呢？&lt;/p&gt;
&lt;p&gt;答案是肯定的，只不过要多走一步：&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;首先，运行下面的命令，把tip导入hg queue：&lt;br /&gt;
&lt;pre class="terminal"&gt;&lt;code&gt;hg qimport -r tip&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
	&lt;li&gt;然后就可以做qrefresh了：&lt;br /&gt;
&lt;pre class="terminal"&gt;&lt;code&gt;hg qrefresh&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;值得一提的是，hg qimport除了可以把一个指定的已提交版本（比如在上面例子中的tip）导入queue中之外，还可以从一个指定的文件里导入。在实际项目开发中，我和同事们常用这种方法在不同机器之间共享patch，非常方便。&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/a1WS6NrKUfA" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/scm/2009/06/20/qimport.html</feedburner:origLink></entry>
 
 <entry>
   <title>重构技巧──inline类成员变量</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/LmywFf3Fmfw/inline_field.html" />
   <updated>2009-04-22T00:00:00-07:00</updated>
   <id>http://dyang.github.com/code/refactoring/2009/04/22/inline_field</id>
   <content type="html">&lt;p&gt;这是今天在和 &lt;a href="http://whimet.blogspot.com"&gt;李教授&lt;/a&gt; pair的时候学到的：&lt;/p&gt;
&lt;h3&gt;问题&lt;/h3&gt;
&lt;p&gt;有一个这样的类定义（简化版）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
public class OnCurrentActivityPage {
  private String pipelineName;
  
  public OnCurrentActivityPage(ScenarioState scenarioState) {
    this.pipelineName = scenarioState.pipelineName();
  }

  public void triggerPipeline() {
    selenium.trigger(pipelineName);
  }

  public void rerunStage() {
    selenium.rerun(pipelineName, stageName);
  }

  public void pausePipeline() {
    selenium.pause(pipelineName);
  }

  // 很多其它方法也要用到pipelineName
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们发现在OnCurrentActivityPage构造函数里调用scenarioState.pipelineName()有bug，需要把它改成不缓存pipelineName而在每次需要它的时候直接调用scenarioState.pipelineName()。&lt;/p&gt;
&lt;h3&gt;解决方案一（人工替换）&lt;/h3&gt;
&lt;p&gt;最直接的办法是删掉pipelineName，把scenarioState保存成类成员变量，然后把所有使用pipelineName的代码替换成使用scenarioState.pipelineName()。由于这个类里面有不少方法（超过10个）需要修改，这个方案颇花时间。&lt;/p&gt;
&lt;h3&gt;解决方案二（自动重构）&lt;/h3&gt;
&lt;p&gt;Intellij支持对类成员变量做inline重构。但是前提是：&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;成员变量必须在声明的同时做初始化&lt;/li&gt;
	&lt;li&gt;成员变量必须被标记为final&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两点要求都很容易满足。为了满足第一点，我们可以把pipelineName的初始化移到声明处：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
private String pipelineName = scenarioState.pipelineName();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时候IDE会报错说scenarioState没有定义，不要理它，因为inline的时候Intellij会把该代码原样移走。&lt;/p&gt;
&lt;p&gt;为了满足第二点，我们只需要把pipelineName标记为final：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
private final String pipelineName = scenarioState.pipelineName();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，再在任何一个使用pipelineName的地方用Ctrl + N执行inline重构，并且指定重构范围为所有的pipelineName调用。转瞬之间，费时费力的重构被工具自动化完成了！&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/LmywFf3Fmfw" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/code/refactoring/2009/04/22/inline_field.html</feedburner:origLink></entry>
 
 <entry>
   <title>在Jetty中定位临时文件目录</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/gtAgxLoKBmQ/jetty_tmp_dir.html" />
   <updated>2009-04-20T00:00:00-07:00</updated>
   <id>http://dyang.github.com/others/2009/04/20/jetty_tmp_dir</id>
   <content type="html">&lt;p&gt;今天花了不少时间改一个使用Jetty部署Web应用程序的bug，对Jetty中设置Web应用程序临时文件路径的规则有了一定了解。俗话说“好记性不如烂笔头”，现在把学到的知识整理一下，加深一下学习印象，呵呵。&lt;/p&gt;
&lt;p&gt;Jetty在启动一个Web应用程序的时候，会按照以下顺序来决定当前应用的临时文件目录（javax.servlet.context.tempdir）：&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;通过WebAppContext.setTempDirectory设置的临时文件目录&lt;/li&gt;
	&lt;li&gt;上下文中已经定义的javax.servlet.context.tempdir所对应的目录&lt;/li&gt;
	&lt;li&gt;${jetty.home}/work目录&lt;/li&gt;
	&lt;li&gt;&lt;span class="caps"&gt;WEB&lt;/span&gt;-&lt;span class="caps"&gt;INF&lt;/span&gt;/work目录&lt;/li&gt;
	&lt;li&gt;${java.io.tmpdir}目录&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在通过上述方式定位到一个目录后，Jetty会在其中按照jetty_host_port_virtualHost_contextPath_hash的格式创建一个子目录，并把该子目录作为临时文件目录注册给javax.servlet.context.tempdir。&lt;/p&gt;
&lt;p&gt;这里面需要注意的是，除了第一种（并且仅当该目录为Jetty创建的前提下）和最后一种使用场景下Jetty会在Web应用程序退出以后自动删除临时目录以外，其余场景Jetty都会保留临时文件目录。这就意味着，如果在同一目录下部署应用程序的新版本，那么很有可能会出现文件版本不一致的问题！请大家在使用的时候要格外小心。&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/gtAgxLoKBmQ" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/others/2009/04/20/jetty_tmp_dir.html</feedburner:origLink></entry>
 
 <entry>
   <title>单纯的过程改进真的能提高开发团队的交付能力吗？</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/4HGvgUE8JqE/engineering-practices.html" />
   <updated>2009-04-07T00:00:00-07:00</updated>
   <id>http://dyang.github.com/agile/2009/04/07/engineering-practices</id>
   <content type="html">&lt;p&gt;今天中午和一位刚从客户那边做完敏捷咨询的同事聊天，这位同事和团队利用四个月的时间把客户的一个几十人的产品团队领进了敏捷的大门，教会了大伙怎么样进行敏捷需求分析，开展敏捷项目管理；手把手地和大伙一起用TDD的方法开发代码，编写测试，进行重构；用Cruise搭建起了持续集成平台，把原本零散滞后的编译集成过程自动化起来；甚至几个人特意为客户开发出了一套在他们开发平台上缺少的测试框架…… 短短的几个月下来，这个开发团队的交付能力产生了质的变化，而且更为关键的是，大家都看到开发团队是在延着一个正确的方向上走。&lt;/p&gt;
&lt;p&gt;上个周末，我和在另位一家公司做tech lead的一位朋友也聊起了类似的话题。她所在的项目已经运行几年时间了，然而她却还在为测试力度不够、设计质量不高、持续集成效果不佳等问题而头疼。而且我在谈话中得知，她们公司中的其他项目也都有着类似的问题。由于在公司层面很少能得到什么实质的支持，tech lead们只好自发组织起来，互相交流，以期摸索出一些有用的东西来解决问题。&lt;/p&gt;
&lt;p&gt;同样是开发团队，为什么一个团队仅仅用了四个月的时间就脱胎换骨，而另一个（其实是多个）却常年得不到实质的提高？&lt;/p&gt;
&lt;p&gt;原因是有多方面的，但我今天想说的是，软件开发归根到底还是一种工程层面上的活动。再好的项目管理方法，再好的项目管理人员，再好的项目监管平台，如果没有工程层面上的重视和提高，都只能是白费。&lt;/p&gt;
&lt;p&gt;上面例子中的后面一家公司在两年前开始开始实施CMMI，并且顺利通过了CMMI五级认证。公司给项目运作从头到脚建立了一套较为完整的管理体系，从需求过程管理到设计过程管理，从测试过程管理到开发过程管理，从资源管理到风险管理，无不涉及。项目经理们每周都要填写不少的文档，公司甚至还成立了专门的监管组织，专门负责以CMMI的标准来定期审核各项目的运作情况；在过程改进的同时，公司还在人员建设上下了功夫，陆续招入了一些有经验的项目经理。&lt;/p&gt;
&lt;p&gt;然而，过程改进层面上的繁荣并不能掩盖开发团队工程能力的不足。随着公司的战略转型，大量经验欠缺但成本较低技术人员的流入更加剧了这一矛盾。开发人员没有写测试的意识和经验，项目的交付压力使得大家尽可能快地写代码，而没有足够测试覆盖率的代码只能是越堆积问题越大；很少有项目能顺利地进行自动化系统测试，缺少了自动化测试的帮助，测试人员只能隔段时间进行一次“人肉回归（manual regression testing）”，耗时耗力，而且很容易出错；大多数的团队都还没有正确地应用持续集成，少数已经应用了的通常也只限于进行自动化编译和打包，更不要提持续发布和版本管理了；项目人员在拥挤、空气混浊的环境中办公，很多项目根本没有一块独立的白板；很多开发人员从来没有用过重构工具，很少有人关注在工具层面上提升生产效率，甚至很少有人能熟练运用开发工具的快捷键，而只是一次次低效率地用鼠标去点击菜单……&lt;/p&gt;
&lt;p&gt;说到这里，我没有任何贬低的意思，因为我也经历过类似的环境，也有过同样的困惑和迷茫。在经历了一个个的交付项目，一遍遍地重复上面的种种问题之后，我曾经对软件开发失去过信心。一直到加入ThoughtWorks，和身边的诸多高手共事以后，我才亲身体会到，软件项目归根到底还是一种工程层面上的活动，任何的社会性因素都不能够也不应该喧宾夺主。开发人员只有学会了正确地写单元测试，才能保证开发出来的代码在单位层面上是工作的，也才可以尽早地暴露潜在的问题；只有积累了大量好的测试，才能在系统演变的过程中持续获得回归回馈，否则时间越长，代码越难维护；开发人员只有学会怎样重构，才能不断改善系统的设计，让系统始终处于一个可工作、可调整的健康状态，而不是过早地病入膏肓，任何改动都可能会伤筋动骨；团队只有掌握了正确的自动化测试技能，才能开发出稳定、有效的系统测试，从而帮助测试人员分担测试负担，以便让测试人员把有限的精力投入到探索和创造上面；团队只有正确地认识、应用并且不断重构他们的持续集成过程，才能持续不断并且快速地获得系统质量的反馈，并且参照这些反馈来不断地改进和完善自己的开发过程……&lt;/p&gt;
&lt;p&gt;然而非常可惜的是，很多的组织只注意到了开发过程的改进，用“大跃进”的思路，期望通过自上而下的管理手段来解决问题，而忽略了最为重要的工程层面的努力，其结果也就可想而知。软件开发不是工业生产，开发团队如果只是周期性地写写表格、做做汇报、分析一下交付风险，制定一些应对措施的话，其交付能力是不会有什么实质的提高的。&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/4HGvgUE8JqE" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/agile/2009/04/07/engineering-practices.html</feedburner:origLink></entry>
 
 <entry>
   <title>用hg qfold来实现文件的增量更新</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/1Y1Y9zfkDb0/hg_qfold.html" />
   <updated>2009-03-18T00:00:00-07:00</updated>
   <id>http://dyang.github.com/scm/2009/03/18/hg_qfold</id>
   <content type="html">&lt;p&gt;在日常开发中，我常常使用mercurial提供的queue功能来对本地代码进行版本管理。然而，hg qrefresh不支持增量式更新的设计却让我非常头疼。&lt;/p&gt;
&lt;p&gt;举个例子：假设我本地queue里面已经有一个名为script的patch了，我在它的基础上又改了三个文件，现在我想把其中的两个文件更新到script patch里，用hg qrefresh的话就很别扭──直接运行hg qrefresh而不加任何参数会把所有三个文件更新上去，这是我不想看到的；而运行hg qrefresh -I file1 file2的话又会以这两个文件替换script patch里原有的所有内容，这更不是我想看到的！&lt;/p&gt;
&lt;p&gt;这个问题可以通过使用hg qfold命令来解决。做法是：&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;运行hg qnew命令来把想要的两个文件加到一个新的patch里：&lt;br /&gt;
&lt;pre class="terminal"&gt;hg qnew -m &amp;#8220;fold me into script&amp;#8221; -I file1 file2 -f foldme.patch&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
	&lt;li&gt;把刚创建的qfoldme弹出，使得script patch位于queue的最顶端：&lt;br /&gt;
&lt;pre class="terminal"&gt;hg qpop -f&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
	&lt;li&gt;把foldme合并到script patch中去：&lt;br /&gt;
&lt;pre class="terminal"&gt;hg qfold foldme.patch&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样一来，就可以把两个想要的文件更新到想要的patch中去了。&lt;/p&gt;
&lt;p&gt;大家还有什么其它的办法吗？&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/1Y1Y9zfkDb0" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/scm/2009/03/18/hg_qfold.html</feedburner:origLink></entry>
 
 <entry>
   <title>用虚拟机+mercurial来合理分配运行测试时间</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/-MIFN3aO9oE/testing_vm.html" />
   <updated>2009-03-16T00:00:00-07:00</updated>
   <id>http://dyang.github.com/agile/scm/2009/03/16/testing_vm</id>
   <content type="html">&lt;p&gt;在开发过程中，我们常常需要在提交代码之前先在本地运行测试，看一看当前修改的质量如何。可是问题是，一旦测试耗时过长（比如说超过15分钟），那么必然会对开发进程造成影响，因为通常在本地运行测试的时候我们都会暂时停止开发；而且过长的测试时间也势必会影响持续集成的频率，毕竟我们不想花太多的时间在等待上。&lt;/p&gt;
&lt;p&gt;在Cruise上，我们运行本地测试的时间大约在15分钟左右，这在很大程度上阻碍了我们的持续集成步伐。解决这个问题，一方面是治本，那就是找出并解决影响测试速度的瓶颈来，我在github上有一个 &lt;a href="http://github.com/dyang/junit-time"&gt;项目&lt;/a&gt;, 专门用来做这个分析，最近一段时间会发布第一个版本；另一方面则是治标，也就是找到一个办法来让测试运行不影响本地的继续开发。&lt;/p&gt;
&lt;p&gt;想达到后一个目的其实并不难。最简单的办法就是让测试运行在一台虚拟机上，这样测试和开发就互相不影响了。有了这个想法，我和同事Pavan就在我们的Ubuntu开发机器上装了一个VirtualBox虚拟机环境（另一个备选的方案是KVM，但是考虑到VirtualBox的安装和配置更简易省时，我们还是选定了后者），在其上创建了一个Ubuntu 8.10服务器版的虚拟机，分配了1G内存（我们的开发用机有4G内存！），然后在其上安装好运行测试所需的环境（mercurial, git, ant, nant, ruby, rake&amp;#8230;）。&lt;/p&gt;
&lt;p&gt;接下来要做的就是把源代码从开发机器上转移到虚拟机了。由于我们用的工具mercurial对点对点协作模式有着天然的支持，因此做到这一点非常容易：我们首先在开发机器上运行hg serve，使得它可以对外提供源代码访问服务；然后再在虚拟机里做hg clone，把开发机器上的修改同步到虚拟机上。整个过程结束后，我们就可以在虚拟机上直接运行ant命令来运行测试了。&lt;/p&gt;
&lt;p&gt;如果工作机器上后续又出现了新的代码修改，我们的操作流程稍微有一点变化。由于我们在开发过程中都是使用 &lt;a href="http://hgbook.red-bean.com/hgbookch12.html"&gt;hg queue&lt;/a&gt; 来管理本地修改的，每次更新queue都会导致开发机器代码仓库中被修改的那条记录的版本发生变化，因此直接用hg pull -u更新到虚拟机的话就会产生版本冲突现象。解决的办法也很简单，就是在虚拟机里运行hg out，把相对于开发机器多出来的版本（其实也就是在开发机器上被更新过的版本）用hg strip命令消掉。然后再运行hg pull -u就不会出现冲突了。&lt;/p&gt;
&lt;p&gt;通过把测试工作转移到单独的虚拟机来进行，我们节省了大量的原本用于等待的时间，从而很大地提升了团队的工作效率。&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/-MIFN3aO9oE" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/agile/scm/2009/03/16/testing_vm.html</feedburner:origLink></entry>
 
 <entry>
   <title>持续集成是一种思维方式</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/ZK5SDc2uw7Y/ci.html" />
   <updated>2009-03-07T00:00:00-08:00</updated>
   <id>http://dyang.github.com/agile/ci/2009/03/07/ci</id>
   <content type="html">&lt;p&gt;我的工作内容决定了我对于持续集成这一话题有着浓厚的兴趣。在工作之余，我常常会拉着身边的朋友们问“你们是怎么做持续集成的？”&lt;/p&gt;
&lt;p&gt;通常，我会听到类似于这样的答案：&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;我们在用cc/cc.net/cc.rb/TeamCity/Hudson……&lt;/li&gt;
	&lt;li&gt;我们在用Subversion/&lt;span class="caps"&gt;TFS&lt;/span&gt;/……管理源代码&lt;/li&gt;
	&lt;li&gt;我们有一部分自动化的单元测试，每次提交代码以后都会运行&lt;/li&gt;
	&lt;li&gt;在正式发布产品之前，我们会先有一个内部的测试阶段（UAT/staging），只有当产品质量通过了内部测试以后才考虑向外发布&lt;/li&gt;
	&lt;li&gt;……&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些回答看起来就像是标准答案一样，曾经一度让我感觉很困惑，因为在和这些朋友的深度交谈中我往往会发现不少持续集成实施方面的缺陷，但是这些缺陷却很难通过了解他们采用哪些持续集成实践而判断出来。难道我们不能简单地通过实践层面来判断一个团队的持续集成能力吗？&lt;/p&gt;
&lt;p&gt;经过一段时间的思考，我找到了问题的答案：判断一个团队的持续集成能力，不能仅仅看他们采用了哪些持续集成的实践，而是要看他们能不能运用持续集成的思维来解决他们所遇到的问题，并且持续地改善他们的持续集成实践。&lt;/p&gt;
&lt;p&gt;这样说可能太抽象了，让我来举一个例子。前段时间，我的一个朋友谈到了他们项目上在采用的一些持续集成实践。他提到了daily build、自动化测试、分阶段部署以及持续交付等，听上去非常不错，而且产品交付也还算顺利。然而，他也提到了一个问题：每次临近发布的时候，他们就把持续集成服务器的自动build功能关掉，变成手动build。当团队想要一个build的时候，tech lead就要和QA lead坐下来，讨论系统目前的状况是否适合做一个build。双方会把自从上次build以后的修改都审查一遍，凭经验决定当前做build风险是不是很大。只有当感觉风险不大的时候才去生成build，然后进入复杂的人工测试阶段。&lt;/p&gt;
&lt;p&gt;这个过程听上去非常奇怪，到了发布阶段做事小心一些是对的，但仅仅依靠经验来判断产品的质量似乎太惨了些，为什么不相信自动化测试呢？他解释说，项目上的自动化测试并不多，覆盖率也并不理想，特别是一些比较重要的系统行为并没有被涵盖到，因此大家在感觉有压力的时候更倾向于相信人工测试的结果。&lt;/p&gt;
&lt;p&gt;那么为什么不增加自动化测试呢？他的回答也很无奈：自动化测试写起来成本不低，项目的交付压力又很大，大家没有足够的时间去写自动化测试。&lt;/p&gt;
&lt;p&gt;说到这里，我想在实际开发项目中摸爬滚打过的朋友都不会感到陌生。在现实的压力面前，我们往往选择最为“安全”的办法，那就是“头痛医头，脚痛医脚”。从表面来看，这样做是无可厚非的，而且似乎与持续集成的关系也不大，然而一旦仔细分析我们就会发现，对于类似这样问题的态度却恰恰反映了一个团队真实的持续集成能力。&lt;/p&gt;
&lt;p&gt;为什么呢？&lt;/p&gt;
&lt;p&gt;原因在于，持续集成和我们常常挂在嘴边的CMMI/Scrum/Agile等过程模型一样，从本质上来说属于一种思考问题的方式。CMMI/Scrum/Agile等过程模型关注于对过程进行分析和控制，它们运用的是一种过程的手段/语言。比如说，当项目无法顺利交付时，负责实施CMMI过程的人员（比如说项目经理等）通常会去审视项目上的监督机制，看看是不是可以在某一方面（比如说测试覆盖率）增加数据收集的力度；而负责实施agile过程的人员则有可能去思考是不是项目上的沟通出了问题，是不是出现了过多的 &lt;a href="http://en.wikipedia.org/wiki/Technical_debt"&gt;技术债务&lt;/a&gt; 等等。在找到问题的根源后，他们往往会通过改善过程来预防问题的再次发生。&lt;/p&gt;
&lt;p&gt;同理，持续集成也是一样的。当相关的问题出现时，持续集成能力强的团队会在解决具体问题的同时还去思考：这些问题为什么会出现？我们应该怎样做才能避免类似的问题再次出现？有什么办法能让问题一旦出现就能很容易被发现？相比之下，持续集成能力弱的团队往往更关注一个个具体问题的解决。他们的确有能力解决一个个的具体问题，然而非常可惜的是，由于没有从持续集成的角度来审视这些问题，这些问题在产品发布过程中还会再次出现，如果问题比较严重的话，还会把整个团队拖进一个痛苦的恶性循环中。&lt;/p&gt;
&lt;p&gt;那么，有办法能解决这个问题吗？&lt;/p&gt;
&lt;p&gt;答案是有的，但是软件开发里没有 &lt;a href="http://en.wikipedia.org/wiki/Silver_bullet"&gt;银弹&lt;/a&gt;, 治标更要治本，想真正解决问题就必须找到产生问题的根源。比如说，在上面提到的这个例子里面，问题的根源就在于没有足够的自动化测试。&lt;/p&gt;
&lt;p&gt;说到这里，我们遇到了阻碍持续集成实施的一个难点。我不想在这里写很多关于如何编写自动化测试的内容，因为在我和很多同行的交流中，我感觉阻碍推行自动化测试（或者任何其它持续集成实践）的难点更多是在文化和交流上，而不是在技术上。我只是想介绍一下Cruise团队是怎样面对这个问题的，希望能给大家提供某种参考。&lt;/p&gt;
&lt;p&gt;Cruise有着一个10人左右的团队，在这个团队里只有一名全职的QA，这个比例可能比绝大多数的团队都要低，但是从去年7月至今，我们已经高质量地发布了1.0、1.1和1.2三个版本，而且发布的过程都非常顺利。这在很大程度上归功于我们所写的大量测试。在Cruise的代码库里，我们有着单元测试、集成测试、系统测试和性能测试。我们采取了一系列的措施来保证这些测试会一直为我们提供最大的价值：&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;我们采取TDD的开发模式，所以单元、集成测试往往会先于产品代码而出现&lt;/li&gt;
	&lt;li&gt;每一个新功能的开发都必须包含完整的自动化系统测试。在我们的项目 &lt;a href="http://www.thoughtworks.com/mingle"&gt;管理工具&lt;/a&gt; 中，任何一个新功能在可以交付给QA做人工测试之前，必须要经过自动化的系统测试。开发自动系统测试的时间有时候甚至会超过开发相应的产品代码的时间&lt;/li&gt;
	&lt;li&gt;每当遇到一个bug的时候，我们都会分析这个bug产生的原因，除非该功能很不适合做自动化测试（这种情况非常少见），否则我们都会增加测试来确保以后该问题不再复现&lt;/li&gt;
	&lt;li&gt;如果遇到bug后，我们发现现有的测试效果不好，我们会重写那些测试。比如说，一段时间里我们遇到了不少与版本控制系统集成相关的bug，我们发现这部分功能更多地是在做mock测试，虽然mock测试速度快，但在速度和质量之间我们更倾向于后者，所以我们把这部分测试全部重写成了真实的集成测试，从此这部分功能的质量问题大幅减少&lt;/li&gt;
	&lt;li&gt;如果系统中某个部分存在着性能问题，我们就增加相应的性能测试。我们甚至自己开发了一套简单有效的性能测试框架&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;问题的关键在于，这些措施都不是从第一天开始就有的，其中的绝大多数都是我们在解决了一个个具体的问题之后陆续做出的调整，而这恰恰是实施持续集成的根本所在：衡量一个团队的持续集成能力，不是看他们目前都在做些什么，而要看他们在遇到问题之后能不能举一反三，能不能对持续集成过程做出相应的修改来预防更多类似问题的出现。换句话说，持续集成不仅是一种工具，也不仅是一套方法，它更是一种提高产品质量、提高项目交付能力的思维方式。一旦团队拥有了这种正确的思维方式，那么遇到的问题越多，解决问题的办法也就会越多，团队的交付能力也就会越强。&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/ZK5SDc2uw7Y" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/agile/ci/2009/03/07/ci.html</feedburner:origLink></entry>
 
 <entry>
   <title>用visitor来封装实现细节</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/A0nqBxksm2M/encapsulation_with_visitor.html" />
   <updated>2009-03-06T00:00:00-08:00</updated>
   <id>http://dyang.github.com/code/refactoring/2009/03/06/encapsulation_with_visitor</id>
   <content type="html">&lt;p&gt;前几天我写过一篇 &lt;a href="http://dyang.github.com/2009/02/18/encapsulation.html"&gt;用封装来解决代码重复&lt;/a&gt; ,其中谈到了设计缺少封装性所带来的一个问题────重复。其实，缺乏封装性所带来的问题还有很多，比如说对变化的适应性。&lt;/p&gt;
&lt;p&gt;下面我来举个实际的例子。用过Cruise的朋友都知道，Cruise里面有一个核心的概念叫pipeline。用户定义好他们想要的pipeline以后，Cruise就可以把这些pipeline从配置文件里读出来使用了。使用方式类似于这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
public class CruiseConfig {
    List&amp;lt;Pipeline&amp;gt; pipelines = new ArrayList&amp;lt;Pipeline&amp;gt;();
    
    public List&amp;lt;Pipeline&amp;gt; getPipelines() {
        return pipelines;
    }
}

public class TriggerService {
    public void trigger() {
        for (Pipeline pipeline : cruiseConfig.pipelines) {
            // ...
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里面有几个地方比较关键：&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;CruiseConfig里面包含了pipeline的定义信息，该信息目前是以ArrayList的形式保存在pipelines字段里&lt;/li&gt;
	&lt;li&gt;CruiseConfig通过一个getter把pipelines的定义信息直接暴露给外部使用者，也就是TriggerService&lt;/li&gt;
	&lt;li&gt;TriggerService直接访问cruiseConfig.pipelines以获得想要的pipeline定义信息&lt;/li&gt;
	&lt;li&gt;TriggerService只是众多外部使用者之一……&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个设计最初并没有什么问题，直到最近我们决定在pipeline之上再引入一个新的概念────pipeline group。有了新的需求，再回过头来看这段代码里面所体现的模型，我们就发现原有设计的严重不足了。其中最主要的不足有：&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;外部使用者不应该直接访问到pipeline，而是应该先访问group，再到pipeline&lt;/li&gt;
	&lt;li&gt;即使为了向后兼容，我们暂时允许外部使用者直接访问pipeline的话（假定所有的pipeline都是在一个组里），由于CruiseConfig内部用于表示pipeline group的数据结构变化了，再把ArrayList原封不动地交给外面就是错误的&lt;/li&gt;
	&lt;li&gt;如果要改，getPipelines方法已经有了不少的地方在调用，一个个地改工作量着实不小&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;经过对这些问题的分析，我们发现问题的根源在于设计的封装性不足。也就是说，CruiseConfig向外暴露了不应该暴露的实现细节，而暴露的细节越多，外部对于这些细节的依赖程度就越高，一旦改动起来，其所带来的影响也就越大了。那么应该怎么解决它呢？&lt;/p&gt;
&lt;p&gt;经过一番重构，我们把代码改成了下面的样子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
public class CruiseConfig {
    List&amp;lt;Group&amp;gt; groups = new ArrayList&amp;lt;Group&amp;gt;();
    
    public void accept(PipelineVisitor visitor) {
        for (Pipeline pipeline : defaultGroup()) {
            visitor.visit(pipeline);
        }
    }
}

public interface PipelineVisitor {
    public void visit(Pipeline pipeline);
}

public class TriggerService {
    public void trigger() {
        cruiseConfig.accept(new PipelineVisitor() {
            public void visit(Pipeline pipeline) {
                // ...
            }
        } );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中最主要的变化有几处：&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;CruiseConfig不再直接向外部暴露pipelines的实现细节，因为这种细节对于外部来说是既不需要知道也不应该知道的&lt;/li&gt;
	&lt;li&gt;CruiseConfig按照某一种简单的约定来向外部提供pipeline信息，这种约定（在例子里是以PipelineVisitor接口的方式提供的）只把外部最关心的信息本身（pipeline对象）暴露出去，至于其它的细节（比如说pipeline是以什么方式存储的）则不会暴露了&lt;/li&gt;
	&lt;li&gt;TriggerService为了获得pipeline信息，现在只需要按照PipelineVisitor的定义去创建一个对象，然后把该对象交给CruiseConfig就可以了。这个对象只关心怎么做好自己的操作就可以了&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;经过这样一番重构，CruiseConfig很好地把pipeline的实现细节封装起来，通过用一个简单而且明确的接口来向外界提供pipeline信息，它把外部对于pipeline定义的依赖程度降到了最低。以后如果再有关于pipeline存储方式或者设计方式的变化，只要外边的接口不变，那么使用者就不需要改变了。&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/A0nqBxksm2M" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/code/refactoring/2009/03/06/encapsulation_with_visitor.html</feedburner:origLink></entry>
 
 <entry>
   <title>在Mercurial中查看被重命名文件的历史</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/jcMAdkHMC5E/hg-mv.html" />
   <updated>2009-02-26T00:00:00-08:00</updated>
   <id>http://dyang.github.com/scm/2009/02/26/hg-mv</id>
   <content type="html">&lt;p&gt;在Mercurial中，如果我们通过hg mv oldname newname重命名并且提交了一个文件，那么通过hg log newname默认是无法看到该文件在重命名之前的历史的。如果想查看该文件在历史上的所有记录，那么可以使用-f参数：&lt;/p&gt;
&lt;pre class="terminal"&gt;&lt;code&gt;hg log -f newname&lt;/code&gt;&lt;/pre&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/jcMAdkHMC5E" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/scm/2009/02/26/hg-mv.html</feedburner:origLink></entry>
 
 <entry>
   <title>用封装来解决代码重复</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/ke3O8CxDmLM/encapsulation.html" />
   <updated>2009-02-18T00:00:00-08:00</updated>
   <id>http://dyang.github.com/code/refactoring/2009/02/18/encapsulation</id>
   <content type="html">&lt;p&gt;最近一段时间，项目上改进了Cruise中artifact repository部分的设计，在原有的代码基础之上做了一次重构，同时加入了新的功能。&lt;/p&gt;
&lt;p&gt;在重构过程中，我们发现原有的代码存在不少的问题。比如说下面这个例子，为了得到存放一个特定job的artifact repository路径，原有的代码类似于:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;File artifactPath = new File(cruiseConfig.getArtifactRoot(), String.valueOf(jobId));&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样写代码至少存在两个问题：&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;重复：每次调用的时候都要做类似的文件名拼接&lt;/li&gt;
	&lt;li&gt;暴露实现细节：artifact repository的内部存放结构属于Cruise中的领域设计细节，这一细节是有可能发生变化的（事实上在目前开发的Cruise 2.0中这一细节的确发生变化了）。我们应该通过面向对象的封装特性来隐藏这一细节，而不应该把它直接暴露在外面&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;新的设计把artifact的内部实现细节封装给了一个ArtifactService类。其中findArtifact方法解决了上面的问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public File findArtifact(JobIdentifier jobId) {
  return new File(.....);
}&lt;/pre&gt;&lt;p&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;当系统中只有这一个方法了解artifact repository的内部结构时，再改动起来就非常容易了。&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/ke3O8CxDmLM" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/code/refactoring/2009/02/18/encapsulation.html</feedburner:origLink></entry>
 
 <entry>
   <title>向github迁移</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/plyGyFW_Kok/move-to-github.html" />
   <updated>2009-01-20T00:00:00-08:00</updated>
   <id>http://dyang.github.com/others/2009/01/20/move-to-github</id>
   <content type="html">&lt;p&gt;前段时间我在blog里提到了一些目前我所用的blog服务的 &lt;a href="http://dyang.github.com/2008/12/30/blog-automation.html"&gt;不爽&lt;/a&gt; ，从那开始我就一直在寻找替换方案，今天终于有了答案──github.com。&lt;/p&gt;
&lt;p&gt;没错，就是那个提供git repository空间和服务的站点。它前段时间推出的 &lt;a href="http://github.com/blog/272-github-pages"&gt;github pages&lt;/a&gt; 服务正好能满足我的要求：用git来管理blog内容，自动发布（git push origin master），用textile来写blog，用户有修改页面的自由度（这样我就可以加上google analytics等）……另外，由于我在github上已经拥有多个开源项目，我对github的使用流程非常熟悉，因此把blog也迁移过去对我来说是一件再好不过的事情了。&lt;/p&gt;
&lt;p&gt;大致想了想，下面是迁移所要做的一些准备工作：&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;创建一个新的git repository: &lt;a href="http://dyang.github.com"&gt;http://dyang.github.com&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;模板引擎：我不希望从头开始写所有的html/css/javascript，所以想找一个合适的模板引擎来自动生成页面原型。看了一些blog， &lt;a href="http://github.com/mojombo/jekyll/"&gt;Jekyll&lt;/a&gt; 似乎不错，接下来我会尝试一下&lt;/li&gt;
	&lt;li&gt;现有数据的迁移
	&lt;ul&gt;
		&lt;li&gt;需要把最早的一些少量blog转换成textile，还好需要转换的数量不多，所以就人肉做了&lt;/li&gt;
		&lt;li&gt;重构目录结构。如果使用模板引擎的话，那么目录结构就取决于模板引擎的要求了&lt;/li&gt;
	&lt;/ul&gt;&lt;/li&gt;
	&lt;li&gt;加入google analytics&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我会在随后把迁移的过程以及结果写出来。&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/plyGyFW_Kok" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/others/2009/01/20/move-to-github.html</feedburner:origLink></entry>
 
 <entry>
   <title>用脚本自动转换和发布blog</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/PcXP6YTL5vA/blog-automation.html" />
   <updated>2008-12-30T00:00:00-08:00</updated>
   <id>http://dyang.github.com/code/refactoring/2008/12/30/blog-automation</id>
   <content type="html">&lt;p&gt;昨天我在一篇 &lt;a href="http://dyang.github.com/2008/12/29/refactor-blog.html"&gt;blog&lt;/a&gt; 里提到了自己想做的一自动化转换和发布blog的想法。说干就干，在一番google之后我写了下面这段脚本来自动转换blog：&lt;/p&gt;
&lt;pre&gt;
&lt;code&gt;
require 'rubygems'
require 'redcloth'

if ARGV.length &amp;lt; 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 &amp;lt;&amp;lt; textile.to_html }
end
&lt;/code&gt;
&lt;/pre&gt;
&lt;p&gt;这段代码会把输入的textile文件自动转移成html文件。为了区分文件的类型，我重构了本地的blog目录，把原有的blog文本文件从blog根目录挪到了textile目录下，同时在textile同一级创建了html目录，用于保存转换以后的结果。&lt;/p&gt;
&lt;p&gt;格式转移搞定了，接下来就是自动发布。google了一番以后，我又写了下面这个脚本：&lt;/p&gt;
&lt;pre&gt;
&lt;code&gt;
require 'xmlrpc/client.rb'

if ARGV.length &amp;lt; 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 =&amp;gt; 'test', :description =&amp;gt; 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 =&amp;gt; e
    puts "ERROR: Code: #{e.faultCode}"
    puts "ERROR: Msg: #{e.faultString}"
  end
end

&lt;/code&gt;
&lt;/pre&gt;
&lt;p&gt;这段脚本非常好理解，它调用了Wordpress支持的metaWeblog API，通过传入适当的参数来实现blog的发布。但是目前我只能用它来发布英文内容，一旦标题或者正文里有中文字符就会返回一个id为-32700的格式错误。google了一下， &lt;a href="http://www.21andy.com/blog/20080217/959.html"&gt;这里&lt;/a&gt; 提供了一种解决方案，但它要求修改服务器端的一个文件，而我的blog提供商是不允许这么做的。这让我很失望。看来，我需要再找一个地方托管我的blog了。&lt;/p&gt;
&lt;p&gt;顺便提一下的是，同事 &lt;a href="http://zztin.com/tin/"&gt;Tin&lt;/a&gt; 提醒我Wordpress也支持邮件发布。然而让我再次失望的是，我现在的blog提供商并没有开放这个功能，而且也没有办法自己上传或者加载plugin去支持。发邮件去问也没有回应，看来我也许真的要考虑迁出了。&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/PcXP6YTL5vA" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/code/refactoring/2008/12/30/blog-automation.html</feedburner:origLink></entry>
 
 <entry>
   <title>重构blog</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/uKEi2K9vrQI/refactor-blog.html" />
   <updated>2008-12-29T00:00:00-08:00</updated>
   <id>http://dyang.github.com/refactoring/2008/12/29/refactor-blog</id>
   <content type="html">&lt;p&gt;同事Tin前几天写了一篇非常棒的blog &lt;a href="http://zztin.com/tin/2008/12/refactoring_my_blog/"&gt;《重构我的blog》&lt;/a&gt; ，讲的是通过把树形结构的category分类转换成扁平结构的tag分类来更好地组织自己blog的内容，并方便外界订阅和查询。这篇文章给我了很多启示。联想到自己这段时间在使用blog时遇到的一些问题，我觉得是时候把自己管理blog的方式来重构一下了。&lt;/p&gt;
&lt;h2&gt;问题&lt;/h2&gt;
&lt;p&gt;既然是重构，那么肯定是要解决现实的问题。我在管理自己的blog的时候遇到的主要一些问题是：&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;本地编辑：我现在发布blog的方式是先在本地把内容写好，然后再通过Wordpress的在线编辑器进行简单的编辑，对一些比较细的效果用html直接编码，预览满意后就发布出去。不过，我一直很不习惯使用在线编辑工具，一方面是因为我是个狂热的vim/gvim用户，很难接受在线编辑工具的简陋和低效；另一方面，在线编辑受到的限制实在太多，稍一不慎就会session超时或者内容丢失，让人抓狂。因此，我希望能把编辑这一部拿到本地用vim/gvim来做。&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
	&lt;li&gt;html转换：大家可以看到，我blog上面所用的格式并不复杂，基本上也就是加一些链接，分几个小节，或者引用一下代码等等。这些都可以用非常简单的html代码来搞定。但即使是简单，我也不太习惯每次写完以后都要手动地添加html代码。程序员的本能使我希望自己只用一种表达形式把blog写出来，然后让格式转换交给工具去做。&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
	&lt;li&gt;同步：我在本地有一个专门的目录用于存放blog，用git来管理。每一篇blog都先在本地以文本格式写好，然后再发布出去。在发布之后，我常常需要做一些细节上的修改，比如说改错别字等。通常我就随手在线修改了，而这就意味着在线的版本和我本地的版本不一致，本地再使用版本管制工具也就失去了意义。我要求blog的创建和后续修改都只在一处进行，其它位置的更新都应该通过同步来完成，这个过程一定要自动化。&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
	&lt;li&gt;集成：Cruise的工作背景让我要求把上面这些步骤都自动化起来，除了写blog要由人来亲自做以外，其它的工作都是重复性的，应该交给机器去做。最理想的情况下是在本地写好blog文本，然后运行一个rake/ant命令，在瞬间完成格式转换和远程同步。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;解决办法&lt;/h2&gt;
&lt;ul&gt;
	&lt;li&gt;对于blog内容我肯定会用vim/gvim来编辑，现在的问题是我还需要一个能运行在Linux上的Wordpress客户端来编辑其它的blog metadata（如tag，分类等）。目前我google的结果是 &lt;a href="http://dropline.net/past-projects/drivel-blog-editor/"&gt;drivel&lt;/a&gt; 。我会在最近花一些时间来试用并且把试用结果写出来。&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
	&lt;li&gt;对于html转换，我最初的想法是用xml来写blog，然后用xslt把xml转换成html。这个过程可以用rake来自动化。昨天我按这个方式试了试，感觉很不好。一方面是我要自己定义一套blog的schema，感觉很轮子；另一方面是我很讨厌写那些方括号，因为我不直接用html来写blog的原因之一就是想避免写那些没什么意义的方括号。所以就把这个方案放弃了。第二套方案是用texile来写blog，然后用现有的texile library来转换成html。这种技术现在已经非常成熟了，如果用Ruby的话就有Redcloth可用，两行代码就能搞定问题。再写几行rake代码就可以很方便地把整个过程集成起来，感觉非常爽。这两天我就要抽时间把它做出来。&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
	&lt;li&gt;同步方面，我初步的想法是学一下Wordpress的Xml-rpc API，然后写脚本来调用。这方面技术应该已经非常成熟了，所以我对实现这一点很有信心。&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
	&lt;li&gt;上面这几个方案都是用脚本来自动化的。这样我就很容易用rake把整个过程串起来，实现一键发布的目的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;这是我第一次尝试用自己平时工作中的一些技术（比如写脚本）和思路（比如消除重复和进行自动化）来改善自己管理blog的方法。有一些东西也许看上去必要性不是很大，不过想一想这么做还是挺有意思的。而且更让我感兴趣的是延着这个思路下去还会有什么更有意思的事会发生。：）&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/uKEi2K9vrQI" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/refactoring/2008/12/29/refactor-blog.html</feedburner:origLink></entry>
 
 <entry>
   <title>一次有趣的开发经历</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/DlEYAF_M8RI/pair-with-pavan.html" />
   <updated>2008-12-28T00:00:00-08:00</updated>
   <id>http://dyang.github.com/agile/ci/2008/12/28/pair-with-pavan</id>
   <content type="html">&lt;p&gt;周五下午和同事Pavan一起结对编程，为Cruise 1.2增加svn external的支持。几个小时过后，我们提交的代码漂亮地通过了所有的单元/集成/验收测试。在这个过程中，我觉得有一些很有趣的细节非常能体现出我们的工作方式，所以现在把它们列出来：&lt;/p&gt;
&lt;h2&gt;集体重构&lt;/h2&gt;
&lt;p&gt;我们的修改是以前一段时间项目里大伙凑在一起做的一些修改为基础而进行的。在Cruise项目上，我们有一个不成文的习惯，就是隔一段时间大伙就凑在会议室里，拿一台笔记本连上投影仪，找出一段写的不好的代码做集体重构。每次开始的时候会有人负责提出一个问题，例如一段写的很糟糕的代码，或者一个看上去很牵强的设计，然后大家一起集思广议想解决办法，并由一个人拿着电脑对着投影仪直接修改。在这个过程中有时候写代码的人思路会中断，这时就会有人抢过电脑来继续写。这种方法的效果非常不错，特别是当大伙不断对一个问题追问“为什么”的时候，往往会让我们原本非常狭窄的思路一下子拓宽起来。&lt;/p&gt;
&lt;h2&gt;分布式版本控制&lt;/h2&gt;
&lt;p&gt;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里，我们平时会穿插着使用。&lt;/p&gt;
&lt;h2&gt;工欲善其事，必先利其器&lt;/h2&gt;
&lt;p&gt;我们必须要在本地创建相应的测试环境才能验证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里去。&lt;/p&gt;
&lt;p&gt;说到工具，我还要顺便提一下我们的开发环境。Cruise团队里的每一个pair除了各自拥有公司配发的笔记本以外，还共同拥有一台非常强大的开发机器（Core 2 Quad &lt;span class="caps"&gt;CPU&lt;/span&gt; * 4, 4G RAM），采用双24寸液晶显示器。操作系统用的是Ubuntu，开发工具用的是IntelliJ。这些强大而灵活的工具让我们能够最大程度地激发工作热情，提高工作效率。&lt;/p&gt;
&lt;h2&gt;测试，测试&lt;/h2&gt;
&lt;p&gt;我和Pavan在测试过程中发现了一个bug，凭经验我们很快找到了导致bug产生的一个方法。不过，我们并没有急于把它消灭掉，而是给现有的方法增加了一个测试。当然，这个测试是不通过的，所以我们接下来的目标就是改正代码，让测试通过。几分钟过后，我们消灭了bug，并且使得系统的测试覆盖率又小小地增加了一点。:)&lt;/p&gt;
&lt;h2&gt;持续集成与及时反馈&lt;/h2&gt;
&lt;p&gt;当我们通过了本地的所有单元和集成测试以后，就信心满满地把本地的提交push到了汇总的mercurial repository里，很快，我们自己UAT环境里的Cruise Server就检测出了这次代码提交，并且开始进行集成。我们的第一个stage是在Linux和Windows平台下并行运行所有的单元测试，然而这个 stage中的一个Windows job却在几分钟后失败了。通过阅读Cruise给出的失败信息，我们发现刚刚提交的代码没有考虑带空格的URI这一种情况，而这种情况在Windows 下非常常见。获得了这个反馈，我们接下来的工作就是在本地增加一个处理带空格的URI这种情况的测试，看它运行失败，然后修复，提交。不一会的功夫， Cruise显示所有测试全部通过！&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/DlEYAF_M8RI" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/agile/ci/2008/12/28/pair-with-pavan.html</feedburner:origLink></entry>
 
 <entry>
   <title>Sunbird</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/q_VH4X9qVAI/sunbird.html" />
   <updated>2008-12-16T00:00:00-08:00</updated>
   <id>http://dyang.github.com/linux/2008/12/16/sunbird</id>
   <content type="html">&lt;p&gt;这段时间我一直在找一个适合自己的日历软件，我的需求是：&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;不要包含邮件功能。我在notes上处理公司邮件，在gmail里管理个人邮件，不希望再引入一个多余的邮件管理软件&lt;/li&gt;
	&lt;li&gt;支持Linux，因为那是我日常最主要使用的操作系统&lt;/li&gt;
	&lt;li&gt;支持CalDAV协议，这样我就可以通过它来访问我在google calender上的日历&lt;/li&gt;
	&lt;li&gt;可以管理多个日历。我在google calender上面除了有个人的日历以外，还有一份和家人共享的日历&lt;/li&gt;
	&lt;li&gt;简单，除了日历功能以外不要加入任何其它多余的功能&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;经过一番搜寻，我最终选定了 &lt;a href="http://www.mozilla.org/projects/calendar/sunbird"&gt;Sunbird&lt;/a&gt; 。&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/q_VH4X9qVAI" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/linux/2008/12/16/sunbird.html</feedburner:origLink></entry>
 
 <entry>
   <title>自动删除不需要的方法调用</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/xaaGZ-2TC60/remove-unused-methods.html" />
   <updated>2008-11-22T00:00:00-08:00</updated>
   <id>http://dyang.github.com/code/2008/11/22/remove-unused-methods</id>
   <content type="html">&lt;p&gt;在ThoughtWorks，我们对重构时可用的一些技巧非常着迷。相比于挽起袖子来进行一次次“胆大包天”的大幅重构，我们更喜欢把重构分解成一个个小且可控的步骤，分而治之。下面就是一例：&lt;/p&gt;
&lt;p&gt;上周五，我和克里斯同学在测我们新写的数据库迁移脚本时发现代码中反复出现下面两句： &lt;br /&gt;
&lt;pre&gt;&lt;code&gt;DbFixture db = new DbFixture();
db.initialize();&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;问题出现了：1）当DbFixture被实例化以后，它还不能被直接使用，用户必须要调用initialize方法去初始化其状态，否则任何对dbFixture实例的方法调用都会导致未知的错误；2）用户在每次使用DbFixture的时候都必须要重复这两句方法调用，这是一种重复。&lt;/p&gt;
&lt;p&gt;在仔细读了initialize方法之后，我和克里斯认为DbFixture的构造函数完全可以自己调用这个方法，从而可以解决上述两个问题。那么怎么改呢？&lt;/p&gt;
&lt;p&gt;方法一：&lt;/p&gt;
&lt;ol&gt;
	&lt;li&gt;修改DbFixture的构造函数，让它在退出函数前调用initialize方法。&lt;/li&gt;
	&lt;li&gt;手动找到并删除所有调用initialize的外部代码。&lt;/li&gt;
	&lt;li&gt;把initialize从public访问级别改成private。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这种方法的好处是比较直观。但问题在于第2步比较费力，因为我们必须要手动去删除每一处外部引用，而我们当时已经有好几个测试类在引用initialize方法，这让我们又找到了方法二。&lt;/p&gt;
&lt;p&gt;方法二：&lt;/p&gt;
&lt;ol&gt;
	&lt;li&gt;在DbFixture内复制initialize并生成一个initialize_temp方法，确保其内容和initialize完全一致。&lt;/li&gt;
	&lt;li&gt;修改DbFixture的构造函数，让它在退出函数前调用initialize_temp方法。&lt;/li&gt;
	&lt;li&gt;删掉原有initialize方法的方法体，让它成为一个空方法。&lt;/li&gt;
	&lt;li&gt;在initialize上执行inline重构（在intelliJ里意味着运行Ctrl + N）。所有外部调用全部自动删除！&lt;/li&gt;
	&lt;li&gt;把initialize_temp重命名为initialize。&lt;/li&gt;
	&lt;li&gt;把initialize从public访问级别改成private。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;方法二的关键点在第4步，通过适当运用inline自动地删除所有的外部调用。&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/xaaGZ-2TC60" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/code/2008/11/22/remove-unused-methods.html</feedburner:origLink></entry>
 
 <entry>
   <title>寻找问题的根源</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/hqa_14w6AuI/root-cause.html" />
   <updated>2008-11-19T00:00:00-08:00</updated>
   <id>http://dyang.github.com/code/2008/11/19/root-cause</id>
   <content type="html">&lt;p&gt;上午我和 &lt;a href="http://iamhukai.blogspot.com"&gt;胡凯同学&lt;/a&gt; 结对重构Cruise中的一段自动化测试代码（没错，我们不会因为一段代码是测试代码而降低对它的质量要求）。其中的一个方法是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
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);
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段测试代码会在一分钟之内不断轮循后台，如果系统状态通过了matcher所定义的检测条件就成功返回，否则就会产生一个断言错误，从而使测试失败。然而，它看上去却让人很不舒服。原本很简单的逻辑因为其中加了一个try/catch而变得复杂。虽然这样做是有原因的（在测试代码运行的时候，由于系统中使用了缓存，因此如果测试机器运行略微慢一些的话，在前面几秒钟之内访问是有可能产生pipeline还未创建完成的情况的），但补获异常并且吃掉它本身这种非常hack的做法却让我们非常起疑，多年的经验告诉我们，这里面一定有更深层次的问题！&lt;/p&gt;
&lt;p&gt;仔细查看代码，我们发现异常是从下面这段代码里产生的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
class Pipelines {
    public Pipeline findPipeline(String pipelineName) {
        for (int i = 0; i &amp;lt; this.size(); i++) {
            Pipeline pipeline = get(i);
            if (pipeline.is(pipelineName)) {
                return pipeline;
            }
        }
        throw new PipelineNotFoundException("Pipeline " + pipelineName + " is not found");
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看上去这段代码也没什么问题，我们在系统状态不正确的时候通过防范性手段来把问题马上暴露出去，这似乎说的过去。但是，这个异常却使得调用代码逻辑变得复杂起来，这问题该怎么解决呢？&lt;/p&gt;
&lt;p&gt;这时，胡凯同学想到了一个好办法。他拿起键盘，迅速地把代码改成了下面的样子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
class Pipelines {
    public Pipeline findPipeline(String pipelineName) {
        for (int i = 0; i &amp;lt; this.size(); i++) {
            Pipeline pipeline = get(i);
            if (pipeline.is(pipelineName)) {
                return pipeline;
            }
        }
        return Pipeline.NULL;
    }
    // ....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pipeline.NULL会创建一个NullPipeline，当matcher检测它的状态的时候，只需要简单地返回一个合适的默认值就可以了。这样一来，我们前面说的那段代码就被简化成了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
class Assert {
    public static void assertWillHappen(CruiseSession cruiseSession, Matcher matcher) {
        while (within(ONE_MINUTE)) {
            if (matcher.matches(cruiseSession)) {
                return;
            }
            sleep();
        }
        assertThat(cruiseSession, matcher);
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这次重构使我再次感受到了，当一段代码出了问题的时候，不应该简单地头痛医头、脚痛医脚，而应该去探寻产生问题的真正根源。&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/hqa_14w6AuI" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/code/2008/11/19/root-cause.html</feedburner:origLink></entry>
 
 <entry>
   <title>有选择地创建Mercurial patch</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/Ps4Kwn-APKg/hg-qnew.html" />
   <updated>2008-10-05T00:00:00-07:00</updated>
   <id>http://dyang.github.com/scm/2008/10/05/hg-qnew</id>
   <content type="html">&lt;p&gt;在使用mercurial的过程中，有时我们需要基于当前修改的文件的一部分来创建patch。举例来说，在修改了多个文件之后，你忽然发现其中的几处修改属于一个重构，而另一些修改则是在添加新的功能。你想把重构的那部分修改独立出来，这样你就可以自由选择什么时候提交重构，什么时候提交新功能了。&lt;/p&gt;
&lt;p&gt;一种解决办法是创建patch。hg qnew命令提供了-X和-I参数来指定哪些文件中的修改应该被排除或者添加到patch之中。&lt;/p&gt;
&lt;p&gt;比如，你目前已经改动了file1和file2，而你只想把file1的修改纳入patch之中。那么就可以执行： &lt;br /&gt;
&lt;pre class="terminal"&gt;&lt;code&gt;hg qnew -m ‘patching changes to file1…’ -f mod_file1.diff -I file1&lt;/code&gt;&lt;/pre&gt; 或者：&lt;br /&gt;
&lt;pre class="terminal"&gt;&lt;code&gt;hg qnew -m ‘patching changes to file1…’ -f mod_file1.diff -X file2&lt;/code&gt;&lt;/pre&gt; 你也可以用下面的命令来达到同样的效果：&lt;br /&gt;
&lt;pre class="terminal"&gt;&lt;code&gt;hg qnew -m ‘patching changes to file1…’ -f mod_file1.diff file1&lt;/code&gt;&lt;/pre&gt; 这样生成的patch就只包含对file1的修改了。&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/Ps4Kwn-APKg" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/scm/2008/10/05/hg-qnew.html</feedburner:origLink></entry>
 
 <entry>
   <title>团队设计能力的交接</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/kQOGn6htatU/team-design.html" />
   <updated>2008-08-18T00:00:00-07:00</updated>
   <id>http://dyang.github.com/agile/2008/08/18/team-design</id>
   <content type="html">&lt;p&gt;在实践中，绝大多数软件开发项目在初期都会配备相对资深的设计人员，他们通过自己的经验以及设计/实施能力往往能够确保项目成功启动并且步入正轨。当项目进入成熟期以后，出于成本的考虑，这一部分资深人员往往会被调离项目，从而对技术交接带来了很大的挑战。在我过去几年参与过的项目中，这种情况就屡屡发生。在我刚刚离开的一个项目上，初期投入的几名资深人员（一名架构师，一名高级DBA，一名高级程序员）先后在不同阶段被调离了项目，而项目的tech lead也在一年半之内更换了三位。这就直接导致了项目的一些关键设计思路没有被很好地理解和传递下去（当然，我们也不能理所当然地认为所有的初期设计思路都是对的、都需要保留）。&lt;/p&gt;
&lt;p&gt;举例来说，项目初期采用了领域驱动的设计方式，tech lead领导团队对目标领域中的核心概念进行了分析，并提炼出了一套当时行之有效的模型。然而，具体的代码虽然容易理解，而为什么要进行领域建模、如何建模、建模时到底追求哪些目标、有哪些东西是我们想通过设计去避免的……这些最根本的东西却很难通过短时间被所有开发人员理解。换句话说，领域驱动设计的过程要远比它所产生的结果重要，而这个过程代表着一种能力，是一个成长中的团队很难在短时间内真正掌握的。这也就导致了tech lead几度更换、代码基数越来越大以后，不断产生的代码质量不高，一些核心的设计点被破坏。比如说，一些本应被封装在领域模型里的代码被直接写在服务层里，一些实体对象的唯一标识（identity）并不唯一等等。&lt;/p&gt;
&lt;p&gt;解决这种问题没有什么妙方，项目的管理人员应该对项目的设计能力有一个客观的把握，而且要随着时间的变化注意观察这种能力，在设计能力与商业成本之间尽可能做出一个平衡来。这一点其实往往挺难做到的，因为在现实中，特别是在外包这个成本敏感的领域里，商业决策往往影响着甚至直接决定着技术决策（如果有可能的话一定避免这么做！）。比如在我曾经服务过的一家软件公司里，项目上的高级技术人员往往在展露自身实力后就被调离到其它项目去，要么负责启动，要么负责救火。这种情况下指望原有团队能够不失真地把设计思路接下来是很难的。&lt;/p&gt;
&lt;p&gt;当然现实也并不总是这么悲观，良好的团队内部沟通在很大程度上能把潜在的问题降到最低。比如说，敏捷开发所提倡的结对编程就在实践层面上对解决这一问题提供了一种思路。设想一下，如果一名有经验的开发人员和一名初级开发人员有着充足的结对时间，那么前者的一些好的设计思路、开发经验甚至编程习惯就会潜移默化地影响到后者，这对于帮助后者提高自身设计能力是再好不过的方式了&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/kQOGn6htatU" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/agile/2008/08/18/team-design.html</feedburner:origLink></entry>
 
 <entry>
   <title>禁掉烦人的system beep</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/ph9ULqrzUg4/no-beep.html" />
   <updated>2008-08-02T00:00:00-07:00</updated>
   <id>http://dyang.github.com/linux/2008/08/02/no-beep</id>
   <content type="html">&lt;p&gt;见过几种不同的方法，最绝的是修改/etc/modprobe.d/blacklist文件，添加下面这行：&lt;br /&gt;
&lt;pre class="terminal"&gt;&lt;code&gt; blacklist pcspkr&lt;/code&gt;&lt;/pre&gt; 重启生效。&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/ph9ULqrzUg4" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/linux/2008/08/02/no-beep.html</feedburner:origLink></entry>
 
 <entry>
   <title>Cruise 1.0正式发布!</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/4V0R20j3Iq8/cruise-1.0.html" />
   <updated>2008-07-28T00:00:00-07:00</updated>
   <id>http://dyang.github.com/ci/2008/07/28/cruise-1.0</id>
   <content type="html">&lt;p&gt;今天，我所在的项目──Cruise──正式发布了第一个公众版本! 大家如果感兴趣可以点击 &lt;a href="http://studios.thoughtworks.com/cruise-continuous-integration"&gt;这里&lt;/a&gt; 获得更多信息：&lt;br /&gt;
&lt;img src="/images/cruise_1.0.png" alt="" /&gt;&lt;br /&gt;
Cruise提供30天的免费试用，并且在试用期过后继续提供两个agent的免费使用许可，欢迎大家使用并提出宝贵意见！&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/4V0R20j3Iq8" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/ci/2008/07/28/cruise-1.0.html</feedburner:origLink></entry>
 
 <entry>
   <title>在Ubuntu上安装fcitx</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/SN84l5K56j4/fcitx.html" />
   <updated>2008-07-27T00:00:00-07:00</updated>
   <id>http://dyang.github.com/linux/2008/07/27/fcitx</id>
   <content type="html">&lt;p&gt;最近刚刚更换了新项目，开发工作从原来的Windows + .NET平台转移到了Linux + Java上，因此我需要重新安装和配置自己电脑上的操作系统和开发环境。&lt;br /&gt;
操作系统选的是Ubuntu 7.10，安装还算顺利，然而在配置中文区域及输入法的时候却耗费了很多力气。现在把我经历的过程记录一下，希望能帮有同样需求的朋友少走些弯路。&lt;/p&gt;
&lt;h2&gt;区域设置&lt;/h2&gt;
&lt;p&gt;fcitx要求操作系统支持中文区域（locale）。首先用以下命令检查系统是否支持中文区域：&lt;br /&gt;
&lt;pre class="terminal"&gt; &lt;code&gt;locale -a &lt;/code&gt; &lt;/pre&gt;&lt;br /&gt;
如果输出的结果不包括zh_CN.*（比如zh_CN.&lt;span class="caps"&gt;UTF&lt;/span&gt;-8），那么就需要进行第二步的安装。&lt;/p&gt;
&lt;h2&gt;安装中文区域&lt;/h2&gt;
&lt;p&gt;通常，这意味着运行命令sudo locale-gen zh_CN，然而根据网上资料所述，由于Linux内核2.6.22-15中存在的一个bug，运行上述命令会导致系统挂起而无法继续。而该问题在2.6.22-14中并不存在，因此解决办法之一便是：&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;重起系统，进入2.6.22-14 recovery mode&lt;/li&gt;
	&lt;li&gt;运行命令： &lt;pre class="terminal"&gt; &lt;code&gt; sudo locale-gen zh_CN &lt;/code&gt; &lt;/pre&gt;&lt;/li&gt;
	&lt;li&gt;上述命令会迅速执行完毕，再运行locale -a，确认系统已经将中文区域列入支持范围&lt;/li&gt;
	&lt;li&gt;重起系统，返回2.6.22-15内核&lt;/li&gt;
	&lt;li&gt;运行sudo dpkg-reconfigure locales，完成区域文件的生成（如果需要的话）和重新配置&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;安装fcitx&lt;/h2&gt;
&lt;p&gt;运行如下命令：&lt;br /&gt;
&lt;pre class="terminal"&gt; &lt;code&gt; sudo apt-get install fcitx &lt;/code&gt; &lt;/pre&gt;&lt;/p&gt;
&lt;h2&gt;配置fcitx&lt;/h2&gt;
&lt;p&gt;修改/etc/enviroment，增加如下一行：&lt;br /&gt;
&lt;pre class="terminal"&gt; &lt;code&gt; LC_CTYPE=”zh_CN.UTF-8″ &lt;/code&gt; &lt;/pre&gt;&lt;br /&gt;
在/etc/X11/Xsession.d/下新建95xinput文件，写入：&lt;br /&gt;
&lt;pre class="terminal"&gt;&lt;code&gt;export XMODIFIERS=”@im=fcitx”
export XIM=fcitx
export XIM_PROGRAM=fcitx
export GTK_IM_MODULE=fcitx
export QT_IM_MODULE=fcitx
fcitx &lt;/code&gt; &lt;/pre&gt;&lt;br /&gt;
重起X系统，在需要输入中文的时候运行Ctrl + Space就可以调出fcitx了。&lt;br /&gt;
最后提醒一下：如果Ctrl + Space还无法调出fcitx，那么请确认X Input Method是被激活的。通常这意味着右键点击目标程序（如terminal）的输入区域，在弹出菜单中选Input Methods &amp;#8594; X Input Method。&lt;br /&gt;
好了，祝大家fcitx用的愉快！&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/SN84l5K56j4" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/linux/2008/07/27/fcitx.html</feedburner:origLink></entry>
 
 <entry>
   <title>两个有趣的alias</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/gRj1d9vAsk4/alias.html" />
   <updated>2008-07-27T00:00:00-07:00</updated>
   <id>http://dyang.github.com/code/linux/2008/07/27/alias</id>
   <content type="html">&lt;p&gt;配置Ubuntu环境，最常用的命令恐怕非apt-get install和aptitude search莫属。不愿每次都敲完整的命令，所以顺手配了两个alias：&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;alias zhao=’aptitude search $1′
alias zhuang=’sudo apt-get install $1′ &lt;/code&gt; &lt;/pre&gt;&lt;br /&gt;
简单而且有趣，呵呵。:)&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/gRj1d9vAsk4" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/code/linux/2008/07/27/alias.html</feedburner:origLink></entry>
 
 
</feed>
