<?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>2012-03-19T15:49:48-07:00</updated>
 <id>http://dyang.github.com</id>
 <author>
   <name>Derek Yang</name>
   <email>yanghada@gmail.com</email>
 </author>

 
 <atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="self" type="application/atom+xml" href="http://feeds.feedburner.com/dyang" /><feedburner:info uri="dyang" /><atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="hub" href="http://pubsubhubbub.appspot.com/" /><entry>
   <title>Veggie in China 1.1发布</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/TK2-GA1Ox-8/vic-11.html" />
   <updated>2012-03-19T00:00:00-07:00</updated>
   <id>http://dyang.github.com/others/2012/03/19/vic-11</id>
   <content type="html">&lt;p&gt;我的iPhone应用 &lt;a href="http://www.veggieinchina.com"&gt;Veggie in China&lt;/a&gt; 的1.1版本今天 &lt;a href="http://itunes.apple.com/us/app/veggie-in-china/id473915713?ls=1&amp;amp;mt=8"&gt;正式发布&lt;/a&gt; ！&lt;/p&gt;
&lt;p&gt;与1.0版本相比，1.1版本的改动较少，主要集中在：&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;增加了天津、南京和苏州三个城市的素食餐馆数据&lt;/li&gt;
	&lt;li&gt;增加了糖、味精中英文词汇的支持&lt;/li&gt;
	&lt;li&gt;改进了一些界面设计&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我的朋友 &lt;a href="http://mac.linsheng.me"&gt;林胜&lt;/a&gt; 和 &lt;a href="http://www.diamondtin.com"&gt;田乐&lt;/a&gt; 对这个版本的开发和设计提供了很多帮助，在此一并致谢！&lt;/p&gt;
&lt;p&gt;下一个版本将会在功能上做出重大改动，敬请期待。&lt;/p&gt;
&lt;p&gt;P.S. &lt;br /&gt;
1. 如果您纳闷为什么会有人不吃香香的味精，那么请读 &lt;a href="http://www.ezhealthydiet.com/msg-side-effects.html"&gt;这篇文章&lt;/a&gt; 。&lt;br /&gt;
2. 如果您纳闷为什么会有人开发像Veggie in China这样“偏门”的应用，那么请读 &lt;a href="http://www.veggieinchina.com/story"&gt;这篇文章&lt;/a&gt; 。&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/TK2-GA1Ox-8" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/others/2012/03/19/vic-11.html</feedburner:origLink></entry>
 
 <entry>
   <title>iPhone</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/kcdCoQHkDoY/iphone.html" />
   <updated>2012-02-24T00:00:00-08:00</updated>
   <id>http://dyang.github.com/others/2012/02/24/iphone</id>
   <content type="html">&lt;p&gt;几星期以前，一个朋友S很神秘地对我说，他的一个老友L买了一部很便宜的iPhone手机，但那手机看上去和用起来似乎总有点感觉不太对头，而且不能连瑞典本地的无线网络。我顿时心里一惊。&lt;/p&gt;
&lt;p&gt;这两天，S又对我说，那台iPhone手机不光连不上网，而且音质奇差，默认带的铃声也似乎有那么点东方特色。我听了心里又是一惊。&lt;/p&gt;
&lt;p&gt;这其中原委是中国人都懂的。我便向朋友S解释了一下，说他的朋友L可能不小心买到了来自东方的山寨产品。朋友S听了以后虽然觉得不可思议，但也是大开眼界。出于情谊，他便给L打电话说明了一下。&lt;/p&gt;
&lt;p&gt;当天晚上，S把他们的电话内容给我发来邮件，结果让我这个东方人儿大吃一惊：&lt;/p&gt;
&lt;p&gt;S：“（别太伤心啦，）至少你还能把它当电话用用。”&lt;br /&gt;
L：“（谁伤心啦？！）至少别人都以为我有一部iPhone！！！”&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/kcdCoQHkDoY" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/others/2012/02/24/iphone.html</feedburner:origLink></entry>
 
 <entry>
   <title>软件与性格</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/32cFh7SOhTM/software.html" />
   <updated>2012-02-20T00:00:00-08:00</updated>
   <id>http://dyang.github.com/others/2012/02/20/software</id>
   <content type="html">&lt;p&gt;在我看来，做软件、做网站和做人没什么太大的区别。有着什么样的性格，大体就会做出什么样的软件或者网站来：&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;居住空间紧张、内心倍感压抑者，比较适合来做 &lt;a href="/images/Sina.jpg"&gt;这个&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
	&lt;li&gt;追求经济利益、做事不择手段者，比较适合来做 &lt;a href="/images/Xiazaiba.jpg"&gt;这个&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
	&lt;li&gt;外观看似时髦、实则毫无美感者，比较适合来做 &lt;a href="/images/UUSee.jpg"&gt;这个&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
	&lt;li&gt;自我感觉良好、喜欢搔首弄姿者，比较适合来做 &lt;a href="/images/360.jpg"&gt;这个&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然，上面这些说起来还都只是小打小闹，心有多大、舞台就有多大，人家都挺知足的，唯独：&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;一直奋起直追、始终不得要领者，比较适合来做 &lt;a href="/images/windows8logo.jpg"&gt;这个&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;随便说说，请勿当真。:P&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/32cFh7SOhTM" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/others/2012/02/20/software.html</feedburner:origLink></entry>
 
 <entry>
   <title>简单</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/yXAMuP7hUYs/simplicity.html" />
   <updated>2012-02-14T00:00:00-08:00</updated>
   <id>http://dyang.github.com/others/2012/02/14/simplicity</id>
   <content type="html">&lt;p&gt;我喜欢简单。&lt;/p&gt;
&lt;p&gt;我喜欢简单的生活。在我的家里没有电视，你也看不到任何在墙上悬挂的装饰品，没有画框，也没有相片，只有白色的墙面。在我的 &lt;a href="https://twitter.com/derekhyang/status/158875459105456128" style="color:darkblue;"&gt;工作室&lt;/a&gt; 里，只有一张没有抽屉的桦木工作台、一把舒服的人体工学工作椅、一对宜家的比利书柜、一个宜家的储物架和一个可供客人临时住宿用的单人沙发床。我的工作电脑是一台两年半以前买的15寸Mac Book Pro，没有iMac，没有Mac Mini，也没有外接的大显示器，甚至没有外接的鼠标和键盘，因为我不需要它们。&lt;/p&gt;
&lt;p&gt;我喜欢简单的饮食。我是一名素食者，这一方面是因为我不认可杀戮和伤害，另一方面则是因为我完全可以在素食中获取我身体所需要的营养元素。对我来说，自己亲手做的一顿简单但营养全面的素食要远胜过一桌山珍海味。在山珍海味的背后，是商人的利益、食客的欲望和小动物们无声的泪。&lt;/p&gt;
&lt;p&gt;简单并不等于低效。我不需要动力强劲的iMac来开发软件。对我来说，机器的快与慢并不会影响我写出让自己满意的代码；我也不需要宽大的显示器来做Photoshop，对我来说，虽然偶尔15寸的显示器会显得空间紧张了些，但也仅仅是偶尔。我的创意、我的技术不会因为显示空间增大了而增长。这些设备也许会提升我的工作效率，但制约我前进的主要因素并不是效率或者速度，而是创意、眼界和品味。我有我自己习惯的节奏，我喜欢慢慢地雕琢 &lt;a href="http://bit.ly/xWkrYX" style="color:darkblue;"&gt;我的作品&lt;/a&gt; ，我不喜欢风风火火地去追赶一个个可供商榷的目标。&lt;/p&gt;
&lt;p&gt;简单也并不等于无趣，家里的一切家具的颜色和式样都是我精心挑选和搭配的。我会在一个房间的白色主色调中加入少量的黑色来制造一种视觉上的对比，我还会在黑白之间点缀些许红色来引导视线的焦点。我会在线条略显复杂、轮廓不甚清晰的就餐区域下面铺上地毯、从而把繁杂收纳起来，制造一种视觉上的稳定。我会小心地把地毯的某个边缘和旁边沙发的某个边缘对齐，从而让整个区域在动态之中仍然拥有最基本的齐整……&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/yXAMuP7hUYs" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/others/2012/02/14/simplicity.html</feedburner:origLink></entry>
 
 <entry>
   <title>Veggie in China</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/zL8OvPiWhAI/veggie-in-china.html" />
   <updated>2012-02-10T00:00:00-08:00</updated>
   <id>http://dyang.github.com/others/2012/02/10/veggie-in-china</id>
   <content type="html">&lt;p&gt;&lt;img src="http://f.cl.ly/items/35410I1r0v3N062l1q0j/AppIcon.png" alt="" /&gt; &lt;br /&gt;
我和朋友合作开发的iPhone软件 &lt;a href="http://veggieinchina.com" style="color:darkblue;"&gt;Veggie in China&lt;/a&gt; 今天正式发布了！ :D&lt;/p&gt;
&lt;p&gt;这个软件的创意来自于我在ThoughtWorks工和时和一些素食外国同事外出就餐的经历。由于那些素食外国同事不会中文，所以我和几个中国同事就常常需要帮他们做翻译。他们的饮食习惯与中国传统的饮食习惯之间的巨大差异（以及由此带来的困惑和不便）给我留下了深刻的印象。&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;a href="http://veggieinchina.com" style="color:darkblue;"&gt;Veggie in China&lt;/a&gt; :P&lt;/p&gt;
&lt;p&gt;我希望它能帮助到那些在中国的外国素食者，我希望他们不再因为自己的很有意义但却鲜有人理解的饮食习惯而感到困惑。&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/zL8OvPiWhAI" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/others/2012/02/10/veggie-in-china.html</feedburner:origLink></entry>
 
 <entry>
   <title>新变化</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/JxHjsuvocPY/changes.html" />
   <updated>2012-02-10T00:00:00-08:00</updated>
   <id>http://dyang.github.com/others/2012/02/10/changes</id>
   <content type="html">&lt;p&gt;一不小心，居然有一年没写博客了。:P&lt;/p&gt;
&lt;p&gt;过去这一年对我和我的家庭来说变化实在太大：&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;我和妻子从上海来到了瑞典，在一个空气清新、景致怡人的海边小城开始了全新的生活&lt;/li&gt;
	&lt;li&gt;我离开了Autodesk，开始全心做自己喜欢的app :D&lt;/li&gt;
	&lt;li&gt;开始学和中文一样有着美妙韵律的瑞典语&lt;/li&gt;
	&lt;li&gt;成为了vegetarian&lt;/li&gt;
	&lt;li&gt;和妻子去了葡萄牙旅游&lt;/li&gt;
	&lt;li&gt;在丹麦看了我十几年来一直不变最爱的乐队Dream Theater的演唱会&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后，还有一个有趣的变化暂时藏起来，过几个月再告诉大家。:P&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/JxHjsuvocPY" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/others/2012/02/10/changes.html</feedburner:origLink></entry>
 
 <entry>
   <title>很贴心的提示</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/ZtRobmoLMAA/move-app.html" />
   <updated>2011-01-16T00:00:00-08:00</updated>
   <id>http://dyang.github.com/mac/2011/01/16/move-app</id>
   <content type="html">&lt;p&gt;在把Mac OS X升级到10.6.6版本以后，当我从Download目录下直接启动程序时，系统给出了下面这个很贴心的新提示：&lt;/p&gt;
&lt;p&gt;&lt;img src="/images/littleSnapper.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;如此体贴，我想不遵守最佳实践都难。&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/ZtRobmoLMAA" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/mac/2011/01/16/move-app.html</feedburner:origLink></entry>
 
 <entry>
   <title>Bisect Max Steps Counter</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/qfr4nJBdG90/bisect.html" />
   <updated>2010-08-08T00:00:00-07:00</updated>
   <id>http://dyang.github.com/groovy/code/2010/08/08/bisect</id>
   <content type="html">&lt;p&gt;星期五下午，我需要通过bisect的方法来定位系统中的一个bug。一次次地更新、编译和测试很耗时也很无聊，所以我决定用新学的Groovy计算bisect所需的最大次数来打发等待时间。&lt;/p&gt;
&lt;p&gt;以下是测试：&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;
class BisectTest extends GroovyTestCase {
	void testMaxSteps() {
		def bisect = new bisectStepsCounter()
		assertEquals 0, bisect.maxSteps(1, 2)
		assertEquals 1, bisect.maxSteps(1, 3)
		assertEquals 2, bisect.maxSteps(1, 4)
		assertEquals 3, bisect.maxSteps(1, 7)
		assertEquals 4, bisect.maxSteps(1, 10)
	}
}
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;下面是实现代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
class BisectStepsCounter {
	def maxSteps(int good, int bad, int stepsSoFar = 0) {
		if (bad - good &amp;lt;= 1) return stepsSoFar;

		stepsSoFar += 1
		int middle = (bad + good)/2
		Math.max(maxSteps(good, middle, stepsSoFar), maxSteps(middle, bad, stepsSoFar))
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;应用这段逻辑，我算了一下自己的实际需要：bisectCounter.maxSteps(167932, 168223)，结果是9。唉，看来我还得再找点什么事情继续打发时间……&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/qfr4nJBdG90" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/groovy/code/2010/08/08/bisect.html</feedburner:origLink></entry>
 
 <entry>
   <title>Groovy中基于正则表达式的模式匹配</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/MjYKaYFr8gY/pattern-matching.html" />
   <updated>2010-07-26T00:00:00-07:00</updated>
   <id>http://dyang.github.com/groovy/code/2010/07/26/pattern-matching</id>
   <content type="html">&lt;p&gt;在写 &lt;a href="http://github.com/dyang/OCUnitReportConverter"&gt;OCUnitReportConverter&lt;/a&gt; 的时候，我发现Groovy对用正则表达式进行模式匹配支持得非常棒。比如说下面这段代码，遍历 &lt;a href="http://www.sente.ch/software/ocunit/"&gt;OCUnit&lt;/a&gt; 输出的每一行文本，提取其中的信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
def parse(String ocunitOutput) {
	def expSuite = /^Test Suite '([^']+)' started at (.*)/
	def expTestCount = /^Executed ([\d]+) tests, with ([\d]+) failure[s]? \([\d]+ unexpected\) in [\S]+ \(([^\)]+)\).*/
	def expTestCase = /^Test Case '-[\S]+ ([^\]]+)\]' (passed|failed) \(([\S]*) seconds\).*/
	def expTestFailure = /(.+): error: -\[[^]]+\] : (.*)/

    ocunitOutput.split(LINE_BREAK).each {
		switch (it) {
			case ~expSuite:
				def matcher = Matcher.getLastMatcher()
				testSuites &amp;lt;&amp;lt; new TestSuite(name: matcher[0][1], timestamp: matcher[0][2])
				break
			case ~expTestCount:
				// capture test count data...
			case ~expTestFailure:
				// capture test failure data...
			case ~expTestCase:
				// capture test case data...
		}
	// ...
}
&lt;/pre&gt;&lt;p&gt;&lt;/code&gt;&lt;/p&gt;
&lt;div style="float:left"&gt;&lt;iframe src="http://rcm.amazon.com/e/cm?t=lostinmalmo-20&amp;o=1&amp;p=8&amp;l=as1&amp;asins=1934356093&amp;ref=tf_til&amp;fc1=000000&amp;IS2=1&amp;lt1=_blank&amp;m=amazon&amp;lc1=0000FF&amp;bc1=FFFFFF&amp;bg1=FFFFFF&amp;npa=1&amp;f=ifr" style="width:120px;height:200px;" scrolling="no" marginwidth="0" marginheight="0" frameborder="0"&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p&gt;同样的逻辑如果在Java里处理就比较麻烦了。由于Java中switch只支持byte、short、char、int(以及它们的包装类）和枚举类型，因此要么写一堆if-else，要么就得动用strategy等面向对象手段来掩盖语言层面的不足了，写起来总感觉不如Groovy那样顺畅自然。&lt;/p&gt;
&lt;p&gt;值得一提的是，上面代码中的Matcher.getLastMatcher方法来自GDK的扩展，用来提供上一次正则表达式匹配的结果（比如获取其中的组信息），这个方法格外适用于switch-case这种无法直接获取正则匹配结果的场景。&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/MjYKaYFr8gY" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/groovy/code/2010/07/26/pattern-matching.html</feedburner:origLink></entry>
 
 <entry>
   <title>在InvokeMethod中调用println导致StackOverflow</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/G0nHxUgcibg/invokemethod.html" />
   <updated>2010-07-21T00:00:00-07:00</updated>
   <id>http://dyang.github.com/groovy/code/2010/07/21/invokemethod</id>
   <content type="html">&lt;p&gt;最近一段时间在摆弄Groovy。&lt;br /&gt;
Groovy提供了多种用于实现AOP的方法，其中之一是让对象实现GroovyInterceptable接口，然后通过实现InvokeMethod方法来对方法调用进行拦截处理。举例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
class MyClass implements GroovyInterceptable {
	def invokeMethod(String name, args) {
		System.out.println("Intercepted ${name}")
		def methodToCall = MyClass.metaClass.getMetaMethod(name, args)
		methodToCall?.invoke(this, args)
	}

	String greet(String name) {
		"Hello ${name}"
	}
}

println new MyClass().greet("dyang")
&lt;/pre&gt;&lt;p&gt;&lt;/code&gt;&lt;br /&gt;
运行该脚本，Groovy会打印出以下输出:&lt;/p&gt;
&lt;pre class="terminal"&gt;
Intercepted greet
Hello dyang
&lt;/pre&gt;
&lt;p&gt;这是对的。不过如果你把InvokeMethod中的System.out.println改成println并再次运行该脚本，那么有趣的事就发生了——Groovy会提示出错StackOverflowError。这是为什么呢？&lt;/p&gt;
&lt;p&gt;原来，Groovy对JDK做了扩充，其中就包括对java.lang.Object类增加了println方法。由于MyClass最终继承于java.lang.Object，因此当我们在MyClass实例上调用println时，Groovy同样会对该调用进行拦截，从而导致InvokeMethod会被执行，并进而导致了StackOverflowError的最终产生。&lt;/p&gt;
&lt;p&gt;同理，如果我们在MyClass实例上调用java.lang.Object中的其它扩展方法（例如dump），invokeMethod也会被调用，因此我们在实现invokeMethod时一定要格外小心。&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/G0nHxUgcibg" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/groovy/code/2010/07/21/invokemethod.html</feedburner:origLink></entry>
 
 <entry>
   <title>用Doskey在Windows下实现命令别名（alias）</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/Uo1MSpbEEas/alias-revised.html" />
   <updated>2010-04-24T00:00:00-07:00</updated>
   <id>http://dyang.github.com/shell/2010/04/24/alias-revised</id>
   <content type="html">&lt;p&gt;在Windows命令行下使用Doskey相关命令可以实现类似于Linux shell中的命令别名（alias）功能。&lt;/p&gt;
&lt;p&gt;举个例子，假如你和我一样每次想查看当前目录下的文件的时候总是先敲ls的话（Windows会温柔地提醒你ls命令不存在），那么你可以用下面的命令来创建一个从ls到dir的别名：&lt;/p&gt;
&lt;pre class="terminal"&gt;doskey ls=dir&lt;/pre&gt;
&lt;p&gt;然后在同一个session里再敲ls命令就会执行dir了。&lt;/p&gt;
&lt;p&gt;如果你想再把命令搞得复杂点，比如说让它支持输入参数，那么也是有办法的。下面这条命令是我在工作时常用的：&lt;/p&gt;
&lt;pre class="terminal"&gt;doskey record=Connectivity.VaultPro.exe /record=Record\$1.autoscr /recordOpts=$2&lt;/pre&gt;
&lt;p&gt;在使用的时候，我就可以敲入类似下面的的命令了：&lt;/p&gt;
&lt;pre class="terminal"&gt;record test GuiDump&lt;/pre&gt;
&lt;p&gt;Doskey会帮我把这条简化了的命令展开成：&lt;/p&gt;
&lt;pre class="terminal"&gt;record=Connectivity.VaultPro.exe /record=Record\test.autoscr /recordOpts=GuiDump&lt;/pre&gt;
&lt;p&gt;当然，这种直接通过敲入doskey命令来设定别名的方法有个缺陷，就是这些别名的生命周期仅限于你敲入时所打开的命令行session，一旦session关闭那么这些别名就失效了。如果这些命令是你一直常用的，那么可以考虑用下面方法把常用别名保存在文件里：&lt;/p&gt;
&lt;pre class="terminal"&gt;doskey /macros:all &amp;gt; %userprofile%\macros.mac&lt;/pre&gt;
&lt;p&gt;这条命令的含义是把当前session已经设置好的所有别名都打印出来，并且通过管道的方式重定向到%userprofile%\macros.mac文件里。打开该文件，你会看到此前你定义的那些别名都是在的。&lt;/p&gt;
&lt;p&gt;有了这个文件，下次再打开一个新的命令行session的时候我们只需要把该文件重新加载一遍就好了，doskey也提供了相应的命令：&lt;/p&gt;
&lt;pre class="terminal"&gt;doskey /macrofile=%userprofile%\macros.mac&lt;/pre&gt;
&lt;p&gt;如果你像我一样主要使用 &lt;a href="http://sourceforge.net/projects/console"&gt;Console2&lt;/a&gt; 命令环境的话，那么在Console2配置中的Shell一项里把启动命令设成下面这样就一劳永逸了：&lt;/p&gt;
&lt;pre class="terminal"&gt;cmd /k doskey /macrofile=%userprofile%\macros.mac&lt;/pre&gt;
&lt;p&gt;有了这个文件，以后如果想增加、删除、修改别名的话直接打开它编辑即可。为此，我写了个别名来自动打开它：&lt;/p&gt;
&lt;pre class="terminal"&gt;alias=gvim "%userprofile%\macros.mac"&lt;/pre&gt;
&lt;p&gt;在编译好并且保存以后，可以通过以下命令来刷新当前session：&lt;/p&gt;
&lt;pre class="terminal"&gt;reload=doskey /macrofile=%userprofile%\macros.mac&lt;/pre&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/Uo1MSpbEEas" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/shell/2010/04/24/alias-revised.html</feedburner:origLink></entry>
 
 <entry>
   <title>最近的一些胡思乱想</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/B1UFmZ365eU/some-thoughts.html" />
   <updated>2010-04-13T00:00:00-07:00</updated>
   <id>http://dyang.github.com/agile/thoughts/2010/04/13/some-thoughts</id>
   <content type="html">&lt;p&gt;1. 让一个人失去创造力的必杀技就是让他每天从早忙到晚。时间都花在急急忙忙地动手做事情上了，自然就没空去找问题想办法了。&lt;/p&gt;
&lt;p&gt;此必杀技也适用于团队。&lt;/p&gt;
&lt;p&gt;2. 环境对人的影响真不容低估。几个月前加入新环境时，看到一个超过三五百行的类我还会惊叫起来，而现在，我已经学会“因地制宜”地抑制自己喊叫的冲动了。也许有一天，我还会觉得眼前的代码看上去还不错。&lt;/p&gt;
&lt;p&gt;如果那一天真的来了，我宁愿去死。&lt;/p&gt;
&lt;p&gt;3. 一个人如果长时间在某一方面不进步，其原因往往并不是他不懂，而是他懂了一点。这一点足够他做出很多看似合理实则错误的判断。&lt;/p&gt;
&lt;p&gt;顺便说一下，这一点在试图实施敏捷却又最终失败的团队身上反映得淋漓尽致。&lt;/p&gt;
&lt;p&gt;4. 经验是个很有趣的东西，一定要看仔细了，否则很容易被它蒙骗。比如说“五年开发经验”，从某种角度看来，它的实际含义有时候却是“一年开发经验重复了四年”。&lt;/p&gt;
&lt;p&gt;白毛汗？&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/B1UFmZ365eU" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/agile/thoughts/2010/04/13/some-thoughts.html</feedburner:origLink></entry>
 
 <entry>
   <title>懂了再去定制</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/KbdCb7GfDkI/customization.html" />
   <updated>2010-02-09T00:00:00-08:00</updated>
   <id>http://dyang.github.com/agile/2010/02/09/customization</id>
   <content type="html">&lt;div style="float:right"&gt;&lt;iframe src="http://rcm.amazon.com/e/cm?lt1=_blank&amp;bc1=FFFFFF&amp;IS2=1&amp;npa=1&amp;bg1=FFFFFF&amp;fc1=000000&amp;lc1=0000FF&amp;t=lostinmalmo-20&amp;o=1&amp;p=8&amp;l=as1&amp;m=amazon&amp;f=ifr&amp;ref=tf_til&amp;asins=0130676349" style="width:120px;height:240px;" scrolling="no" marginwidth="0" marginheight="0" frameborder="0"&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p&gt;在实施一种新的管理方法（比如scrum）的时候，我们常听到这样一种声音：&lt;/p&gt;
&lt;p&gt;&lt;em&gt;我们项目的情况比较特殊，没必要严格遵照要求，我们需要定制……&lt;/em&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/Shu_ha_ri"&gt;因为你很可能还不懂自己究竟在做什么。&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;一个让我至今难忘的例子：几年前，我的老板置疑scrum：scrum不是说可以每个迭代结束都能产生潜在可发布的产品吗，我们为什么做不到？&lt;/p&gt;
&lt;p&gt;答案是：我们一开始觉得sprint review和demo没必要，所以＂定制＂掉了。没有目标，自然也就不会有结果。&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/KbdCb7GfDkI" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/agile/2010/02/09/customization.html</feedburner:origLink></entry>
 
 <entry>
   <title>再见ThoughtWorks！</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/jEA7bmmST5w/bye-thoughtworks.html" />
   <updated>2009-12-01T00:00:00-08:00</updated>
   <id>http://dyang.github.com/ci/agile/2009/12/01/bye-thoughtworks</id>
   <content type="html">&lt;p&gt;已经很长时间没有写blog了。最近的几个月时间里我的工作和生活都发生了较大的变化：因为家庭原因，我离开了生活了六年之久的北京，来到了上海和妻子团聚;同时，我也因此而离开了 &lt;a href="http://www.thoughtworks.com"&gt;ThoughtWorks&lt;/a&gt; ，加入了设计软件公司 &lt;a href="http://www.autodesk.com"&gt;Autodesk&lt;/a&gt; 。&lt;/p&gt;
&lt;p&gt;回首过去的几年时间，我能很清晰地感觉到自己对软件开发的认识不断地发生着有趣的变化：&lt;/p&gt;
&lt;h3&gt;六年前&lt;/h3&gt;
&lt;ul&gt;
	&lt;li&gt;眼中只有C#/.&lt;span class="caps"&gt;NET&lt;/span&gt;/Windows，“外面&amp;quot;的世界？连想都没有想过&lt;/li&gt;
	&lt;li&gt;对版本控制工具几乎毫不了解，顶多也只是在朋友的推荐下用用VSS。依靠文件拷贝来备份，不知丢过几次源代码&lt;/li&gt;
	&lt;li&gt;长长的方法 :P&lt;/li&gt;
	&lt;li&gt;if-else/switch case&lt;/li&gt;
	&lt;li&gt;在面向对象的幌子下写着并不纯粹的面向过程代码，自己却浑然不知&lt;/li&gt;
	&lt;li&gt;界面和业务逻辑代码混在一起&lt;/li&gt;
	&lt;li&gt;项目完成==软件功能开发完成&lt;/li&gt;
	&lt;li&gt;对微软崇尚备至，做梦都想加入微软&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;三年前&lt;/h3&gt;
&lt;ul&gt;
	&lt;li&gt;C#/.&lt;span class="caps"&gt;NET&lt;/span&gt;/Windows，但已经开始不满现状，希望具备多平台、多语言的开发能力（如果你手里只有锤子，那么你会希望所有的问题都是钉子）&lt;/li&gt;
	&lt;li&gt;面向对象很简单，不就是为主要的业务对象建几个模吗？&lt;/li&gt;
	&lt;li&gt;所有的项目里都会大量地写Helper/Manager&lt;/li&gt;
	&lt;li&gt;真正的业务对象仅仅是一些没有实质行为的纯数据载体&lt;/li&gt;
	&lt;li&gt;很在乎自己用什么设计模式，最常用的却是Singleton和Factory&lt;/li&gt;
	&lt;li&gt;（几乎）没有封装&lt;/li&gt;
	&lt;li&gt;以能够做出复杂的软件设计为荣&lt;/li&gt;
	&lt;li&gt;带了好几个项目，但所有的项目都解释不了一个问题──为什么软件的质量永远无法保证？
	&lt;ul&gt;
		&lt;li&gt;开始思考为什么软件写到一定程度以后就会很难维护&lt;/li&gt;
		&lt;li&gt;开始思考为什么软件的质量只能通过大规模反复测试和加班改bug来解决&lt;/li&gt;
		&lt;li&gt;开始思考为什么单元测试看似无法应用到实际的项目中&lt;/li&gt;
		&lt;li&gt;“这个建议很好，但是我们的项目进度很紧，所以……”&lt;/li&gt;
		&lt;li&gt;“测试会增加开发人员的工作量……”&lt;/li&gt;
		&lt;li&gt;开始疯狂地寻找解决办法，开始疯狂地自学TDD、单元测试、持续集成这些敏捷软件的实践&lt;/li&gt;
		&lt;li&gt;结果发现仅靠自学是永远不可能真正领悟到敏捷的实践&lt;/li&gt;
		&lt;li&gt;决定去ThoughtWorks取经&lt;/li&gt;
	&lt;/ul&gt;&lt;/li&gt;
	&lt;li&gt;版本控制==Subversion&lt;/li&gt;
	&lt;li&gt;软件完成==开发完成+通过测试及客户验收+团队成长&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;现在&lt;/h3&gt;
&lt;ul&gt;
	&lt;li&gt;面向对象很难
	&lt;ul&gt;
		&lt;li&gt;封装！封装！&lt;/li&gt;
		&lt;li&gt;在多个项目上或好或坏地实践了面向对象设计，但到现在还感觉自己仍是面向对象的门外汉&lt;/li&gt;
		&lt;li&gt;一切皆是模式，一切皆非模式&lt;/li&gt;
		&lt;li&gt;对Singleton有一种疯狂地抵制，两年半里从来没写过一个Singleton，而且预计将来也很难&lt;/li&gt;
		&lt;li&gt;面向对象不是唯一的解决方案，在一些特定领域里（比发并发处理）其它的设计思想（比如函数式编程）更具竞争力&lt;/li&gt;
	&lt;/ul&gt;&lt;/li&gt;
	&lt;li&gt;以能够做出简单而有效的软件设计为荣&lt;/li&gt;
	&lt;li&gt;经历了多个成功的敏捷开发项目的实战，亲身体验了什么叫做有秩序、高质量、低成本的软件开发
	&lt;ul&gt;
		&lt;li&gt;充足的测试、良好的封装、有效的沟通是提高软件可维护性的有效手段（全世界都知道，但只有少数团队真正用心在做，sigh……）&lt;/li&gt;
		&lt;li&gt;近三年之内的加班次数超不过十次&lt;/li&gt;
		&lt;li&gt;软件发布阶段团队却无事可做，只好提前开发下一版本&lt;/li&gt;
		&lt;li&gt;项目进展的从容不迫使我有了大量的业余时间投入到开源世界&lt;/li&gt;
		&lt;li&gt;“这个建议很好，我们来试一下，这样就能进一步减缓项目进度的压力”&lt;/li&gt;
		&lt;li&gt;有效的测试会帮助开发人员提供软件质量的反馈，帮助整个团队提升软件质量&lt;/li&gt;
	&lt;/ul&gt;&lt;/li&gt;
	&lt;li&gt;写代码前不写测试就会极度不安&lt;/li&gt;
	&lt;li&gt;离开IntelliJ就不会写Java，离开Resharper就不会写C#&lt;/li&gt;
	&lt;li&gt;开发、办公、日常操作平台从Windows转到了Linux和Mac，开发平台从.NET转到了JVM&lt;/li&gt;
	&lt;li&gt;习惯于和同事们以微软为反例来相互取笑 ;)&lt;/li&gt;
	&lt;li&gt;自动化！自动化！甚至连发布blog都是如此&lt;/li&gt;
	&lt;li&gt;对版本控制和持续集成有一种宗教般的热情&lt;/li&gt;
	&lt;li&gt;版本控制==Git、Mercurial&lt;/li&gt;
	&lt;li&gt;软件完成==开发完成+通过测试及客户验收+团队成长+产品维护+分享（写文章、blog，总结出通用框架、工具，演讲……）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;多年以后&lt;/h3&gt;
&lt;p&gt;我不知道三年、五年、十年以后这份清单会变成什么样子，但是有一点是可以肯定的：在ThoughtWorks的这段经历使我更关注问题本质、更注重解决方案的实效性，并且不轻易屈服于事物的表面现象而努力寻找并尝试改变。ThoughtWorks教会了我用敏锐的眼光去观察身边日复一日的软件开发活动，在平淡之中追寻软件开发的乐趣，在惯性思维的种种借口之下取得突破。&lt;/p&gt;
&lt;p&gt;再见，ThoughtWorks！&lt;/p&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/jEA7bmmST5w" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/ci/agile/2009/12/01/bye-thoughtworks.html</feedburner:origLink></entry>
 
 <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/tdul2DcazfY/inline_field.html" />
   <updated>2009-04-22T00:00:00-07:00</updated>
   <id>http://dyang.github.com/refactoring/code/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;div&gt;&lt;iframe src="http://rcm.amazon.com/e/cm?t=lostinmalmo-20&amp;o=1&amp;p=8&amp;l=as1&amp;asins=0201485672&amp;ref=tf_til&amp;fc1=000000&amp;IS2=1&amp;lt1=_blank&amp;m=amazon&amp;lc1=0000FF&amp;bc1=FFFFFF&amp;bg1=FFFFFF&amp;npa=1&amp;f=ifr" style="width:120px;height:240px;" scrolling="no" marginwidth="0" marginheight="0" frameborder="0"&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/dyang/~4/tdul2DcazfY" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/refactoring/code/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;div style="float:right"&gt;&lt;iframe src="http://rcm.amazon.com/e/cm?t=lostinmalmo-20&amp;o=1&amp;p=8&amp;l=as1&amp;asins=0321601912&amp;ref=tf_til&amp;fc1=000000&amp;IS2=1&amp;lt1=_blank&amp;m=amazon&amp;lc1=0000FF&amp;bc1=FFFFFF&amp;bg1=FFFFFF&amp;npa=1&amp;f=ifr" style="width:120px;height:200px;" scrolling="no" marginwidth="0" marginheight="0" frameborder="0"&gt;&lt;/iframe&gt;&lt;br&gt;&lt;iframe src="http://rcm.amazon.com/e/cm?t=lostinmalmo-20&amp;o=1&amp;p=8&amp;l=as1&amp;asins=0131177052&amp;ref=tf_til&amp;fc1=000000&amp;IS2=1&amp;lt1=_blank&amp;m=amazon&amp;lc1=0000FF&amp;bc1=FFFFFF&amp;bg1=FFFFFF&amp;npa=1&amp;f=ifr" style="width:120px;height:200px;" scrolling="no" marginwidth="0" marginheight="0" frameborder="0"&gt;&lt;/iframe&gt;&lt;br /&gt;
&lt;br&gt;&lt;iframe src="http://rcm.amazon.com/e/cm?t=lostinmalmo-20&amp;o=1&amp;p=8&amp;l=as1&amp;asins=0321503627&amp;ref=tf_til&amp;fc1=000000&amp;IS2=1&amp;lt1=_blank&amp;m=amazon&amp;lc1=0000FF&amp;bc1=FFFFFF&amp;bg1=FFFFFF&amp;npa=1&amp;f=ifr" style="width:120px;height:200px;" scrolling="no" marginwidth="0" marginheight="0" frameborder="0"&gt;&lt;/iframe&gt;&lt;/div&gt;
&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/J0utqRHQ0KU/testing_vm.html" />
   <updated>2009-03-16T00:00:00-07:00</updated>
   <id>http://dyang.github.com/scm/agile/2009/03/16/testing_vm</id>
   <content type="html">&lt;div style="float:right"&gt;&lt;iframe src="http://rcm.amazon.com/e/cm?t=lostinmalmo-20&amp;o=1&amp;p=8&amp;l=as1&amp;asins=0596800673&amp;ref=tf_til&amp;fc1=000000&amp;IS2=1&amp;lt1=_blank&amp;m=amazon&amp;lc1=0000FF&amp;bc1=FFFFFF&amp;bg1=FFFFFF&amp;npa=1&amp;f=ifr" style="width:120px;height:200px;" scrolling="no" marginwidth="0" marginheight="0" frameborder="0"&gt;&lt;/iframe&gt;&lt;/div&gt;
&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;div style="float:left"&gt;&lt;iframe src="http://rcm.amazon.com/e/cm?t=lostinmalmo-20&amp;o=1&amp;p=8&amp;l=as1&amp;asins=1590594959&amp;ref=tf_til&amp;fc1=000000&amp;IS2=1&amp;lt1=_blank&amp;m=amazon&amp;lc1=0000FF&amp;bc1=FFFFFF&amp;bg1=FFFFFF&amp;npa=1&amp;f=ifr" style="width:120px;height:200px;" scrolling="no" marginwidth="0" marginheight="0" frameborder="0"&gt;&lt;/iframe&gt;&lt;/div&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/J0utqRHQ0KU" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/scm/agile/2009/03/16/testing_vm.html</feedburner:origLink></entry>
 
 <entry>
   <title>持续集成是一种思维方式</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/DULTpoJ0tWQ/continuous-integration.html" />
   <updated>2009-03-07T00:00:00-08:00</updated>
   <id>http://dyang.github.com/ci/agile/2009/03/07/continuous-integration</id>
   <content type="html">&lt;div&gt;
&lt;div style="float:right"&gt;&lt;iframe src="http://rcm.amazon.com/e/cm?lt1=_blank&amp;bc1=FFFFFF&amp;IS2=1&amp;npa=1&amp;bg1=FFFFFF&amp;fc1=000000&amp;lc1=0000FF&amp;t=lostinmalmo-20&amp;o=1&amp;p=8&amp;l=as1&amp;m=amazon&amp;f=ifr&amp;ref=qf_sp_asin_til&amp;asins=0321601912" style="width:120px;height:240px;" scrolling="no" marginwidth="0" marginheight="0" frameborder="0"&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;div&gt;&lt;i&gt;&lt;strong&gt;2011-6-8更新&lt;/strong&gt;：我在Cruise项目上的同事及产品经理Jez Humble对Cruise团队在持续集成、持续发布方面的多年经验做了精妙的总结，著成了Continuous Delivery一书。大家如果想真正了解持续集成是如何在真实项目中成功应用的，那么请一定不要错过这本书！（这本书也可以在Kindle上阅读）&lt;/i&gt;&lt;br /&gt;
&lt;br /&gt;
&lt;iframe src="https://ws.amazon.com/widgets/q?_encoding=UTF8&amp;tag=lostinmalmo-20&amp;asin=B003YMNVC0&amp;size=small&amp;ServiceVersion=20061125&amp;TemplateId=8012" style="width:157px;height:19px;" frameborder="0" scrolling="no"&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;/div&gt;
&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;div style="float:right"&gt;&lt;iframe src="http://rcm.amazon.com/e/cm?lt1=_blank&amp;bc1=FFFFFF&amp;IS2=1&amp;npa=1&amp;bg1=FFFFFF&amp;fc1=000000&amp;lc1=0000FF&amp;t=lostinmalmo-20&amp;o=1&amp;p=8&amp;l=as1&amp;m=amazon&amp;f=ifr&amp;ref=qf_sp_asin_til&amp;asins=0321601912" style="width:120px;height:240px;" scrolling="no" marginwidth="0" marginheight="0" frameborder="0"&gt;&lt;/iframe&gt;&lt;/div&gt;&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/DULTpoJ0tWQ" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/ci/agile/2009/03/07/continuous-integration.html</feedburner:origLink></entry>
 
 <entry>
   <title>用visitor来封装实现细节</title>
   <link href="http://feedproxy.google.com/~r/dyang/~3/lHUGPpP2r54/encapsulation_with_visitor.html" />
   <updated>2009-03-06T00:00:00-08:00</updated>
   <id>http://dyang.github.com/refactoring/code/2009/03/06/encapsulation_with_visitor</id>
   <content type="html">&lt;p&gt;前几天我写过一篇 &lt;a href="/code/refactoring/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/lHUGPpP2r54" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/refactoring/code/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="/refactoring/code/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/lLVVUogxgz4/blog-automation.html" />
   <updated>2008-12-30T00:00:00-08:00</updated>
   <id>http://dyang.github.com/refactoring/code/2008/12/30/blog-automation</id>
   <content type="html">&lt;p&gt;昨天我在一篇 &lt;a href="/refactoring/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/lLVVUogxgz4" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/refactoring/code/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;div style="float:right"&gt;&lt;iframe src="http://rcm.amazon.com/e/cm?lt1=_blank&amp;bc1=FFFFFF&amp;IS2=1&amp;npa=1&amp;bg1=FFFFFF&amp;fc1=000000&amp;lc1=0000FF&amp;t=lostinmalmo-20&amp;o=1&amp;p=8&amp;l=as1&amp;m=amazon&amp;f=ifr&amp;ref=tf_til&amp;asins=0321213351" style="width:120px;height:200px;" scrolling="no" marginwidth="0" marginheight="0" frameborder="0"&gt;&lt;/iframe&gt;&lt;/div&gt;
&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;div style="float:left"&gt;&lt;iframe src="http://rcm.amazon.com/e/cm?t=lostinmalmo-20&amp;o=1&amp;p=8&amp;l=as1&amp;asins=0596800673&amp;ref=tf_til&amp;fc1=000000&amp;IS2=1&amp;lt1=_blank&amp;m=amazon&amp;lc1=0000FF&amp;bc1=FFFFFF&amp;bg1=FFFFFF&amp;npa=1&amp;f=ifr" style="width:120px;height:200px;" scrolling="no" marginwidth="0" marginheight="0" frameborder="0"&gt;&lt;/iframe&gt;&lt;/div&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;div style="float:right"&gt;&lt;iframe src="http://rcm.amazon.com/e/cm?t=lostinmalmo-20&amp;o=1&amp;p=8&amp;l=as1&amp;asins=0321601912&amp;ref=tf_til&amp;fc1=000000&amp;IS2=1&amp;lt1=_blank&amp;m=amazon&amp;lc1=0000FF&amp;bc1=FFFFFF&amp;bg1=FFFFFF&amp;npa=1&amp;f=ifr" style="width:120px;height:200px;" scrolling="no" marginwidth="0" marginheight="0" frameborder="0"&gt;&lt;/iframe&gt;&lt;/div&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/6mRk0kOzp4U/alias.html" />
   <updated>2008-07-27T00:00:00-07:00</updated>
   <id>http://dyang.github.com/linux/code/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/6mRk0kOzp4U" height="1" width="1"/&gt;</content>
 <feedburner:origLink>http://dyang.github.com/linux/code/2008/07/27/alias.html</feedburner:origLink></entry>
 
 
</feed>
