Morning@Weblog

5/31/2005

Basic Test Pattern

Filed under: — site admin @ 11:21 pm

这里列出的是出现于TDD一书中的两个有关于测试的基本模式:

When you begin writing tests, you will discover a common pattern (Bill Wake coined the phrase 3A for this):
1. Arrange—create some objects
2. Act—stimulate them
3. Assert—check the results

这个模式看起来很简单,以至于似乎不值一提。这里主要的问题在于,对一组测试来说,Arrange是否可以“共享”。而一般认为,这里的Performance是次要的,因为比起测试间依赖导致的严重后果,Performance的影响就显得弱了许多。

前阵子在BJUG的maillist里讨论过DbC(Design by Contract)的问题。如Raimundo所说,这里的Test Case其实是在检查postcondition和invarient。而至于precondition则需要有被测代码自己来保障了。不过适当结合DbC到或许是一个值得一试的事情,至少可以把对precondition的检验放到代码之外。

a common pattern—one test can be simple if and only if another test is in place and running correctly.

这条模式比较有意思,讲的是,当你想要简化一个test的时候,也就是要对原有test做出修改的时候,首先要有其他的测试对这一修改做出保障。

5/30/2005

软件开发成本的“守恒”规律

Filed under: — site admin @ 11:27 am

一方面,借助于某一系列的具体技术或者工具,软件开发的成本也许可以在某个局部领域里得到大幅度的降低。另一方面,总体而言,成本依然没有减少,无非是从一处转移到了另一处,从一种形式的消耗转化为另一种形式的消耗。这一点到是酷似物理学领域里的“能量守恒定律”。这里可以举出很多例子:

比如framework,它部分的将软件成本转移到了framework的开发上,从而使上层应用者得以轻松享用framework开发者的辛劳付出。但是不要忘记,任何framework都有其适用的领域,只有在其擅长的范围内,效用才能得以彰显。而一旦超越其能力所及,那么,越俎代庖只会事与愿违,古老的MFC便是一个例子。

再比如TDD,作为一种以个体经验为主导的敏捷方法实践,它在为XP团队带来编写高质量代码保障的同时,也为TDDer们学习和掌握其实践精要无形中设置了障碍。也就是说,这种效益的总体提升,是建立在TDD的正确实践基础之上的。因为TDD浓重的经验主义色彩,使得成功者的经验很难轻松把握和简单复制。因此,对于一个实际团队而言,TDD本身的掌握成本,以及对项目构成的风险,也该计入账上。此外,TDD的适用范围也是一个值得思考的问题。至少,对于许多并未采用TDD的现存系统的再工程(reengineering)而言,中途引入TDD在短期内并不会看到任何作用。而很多情况下,我们面对的多是遗留系统(至少从我个人经历而言是如此),而非白手起家,从零开始。最近市面上有一本书,叫做《Object-Oriented Reengineering Patterns》,讲的就是对遗留系统的再工程问题。

这里还有一个来自于.net的例子,这是最近从孟岩那里听来的,有关于DataGrid的抱怨。DataGrid作为RAD开发工具的一个必要组件,为基于数据库应用的快速搭建提供了便利,但是与数据源过于紧密的绑定,导致了许多基于.net技术的应用系统没有层次概念。而一般认为,一个分层系统所带来的松耦合,可以使系统更具延展性,以适应多变的需求。由此,为了使.net应用也变得层次分明,为了使domain model能够贯穿于从后到前的每一个层面,就不得不花费大量精力来做与DataGrid的适配。这个反面例子,到是让我更加相信RAD工具的局限性了。看起来,像do more with less这样的口号,也并非如听上去那么悦耳,搞不好就是一个hype。

5/29/2005

重构,错误,测试

Filed under: — site admin @ 10:56 pm

Made a mistake in a refactoring and chose to forge ahead, writing another test to isolate the problem.

这句话摘自Kent Beck的TDD一书。重构与测试一直以来都被认为是休戚与共,息息相关的。这一点也贯穿于TDD实践的始终。在我看来,上面这句话的指导意义在于:
1) 如果将软件开发中的重构行为比作排布与时间轴上的若干活动序列,那么TDD不只是孤立的讨论某个时间切片上的测试技法,TDD同时也为从一个重构活动到另一个重构活动的“迁移”提供了指导。当重构发生错误时,请适时的引入测试来隔离该错误。我以为,除了isolate the problem,这里还有catch and track the problem的意味。
2) 将重构时发生错误作为一个适时加入测试用例的契机。以往,我觉得对于一个“遗留”系统,加入测试用例的最好时机是后期测试出现bug的时候,所谓“亡羊补牢”。而事实上,开发过程中间的每个环节也存在补写测试用例的时机。这里便是一个恰到好处的引入点。

下面这段话,从反面进一步说明了重构与测试的关系。与上一句到是有种“遥相呼应”的感觉。

Write the tests you wish you had. If you don’t, you will eventually break something while refactoring. Then you’ll get bad feelings about refactoring and stop doing it so much.

5/27/2005

基于JavaScript的Web组件化——从一个按钮开始

Filed under: — site admin @ 9:59 am

以前系统的JSP页面中,有很多地方都要出现图片式按钮,该按钮实际上是一个table。

<table border="0″ cellspacing="0″ cellpadding="0″>
 <tr>
  <td><img src="images/left_btn.gif” width="6″ height="25″></td>
  <td nowrap background="images/mid_btn.gif” class="btntext”
    onmouseover="this.className=’btntexthighlight’;”
    onmouseout="this.className=’btntext’;">
   <a href="doSomething.do">提交</a>
  </td>
  <td><img src="images/right_btn.gif” width="6″ height="25″></td>
 </tr>
</table>

每当我们需要按钮的时候,都要引用这样一段冗长的html代码。实际上,如果将之封装成一个Web按钮组件,然后在需要的时候调用并传入必要的参数,可以省去相当的重复劳动,并且也不易出错。

采用JavaScript技术,基本的思路是,我们先在原来的JSP页面中按钮出现的地方定义一些placeholder,然后传入必要的参数,比如:

<div id="button” name="添 加” action="javascript:document.addItemForm.submit();"/>

在一个公共的js文件中,利用JavaScritp将这些placeholder替换成真正的html输出。

function replaceDivTag() {
 aTags=document.getElementsByTagName("DIV");
 for(var i=0;i<aTags.length;i++)
  with(aTags[i]){
   if(id=="button") {
    // replace the div with table
    aTags[i].innerHTML=’<table border=0 cellspacing=0 cellpadding=0>…’
   }
  }
}

接下来的问题是,如何让该函数得到调用。我们可以将其作为onload事件的处理函数,这样,在每次页面加载时,id为"button"的div都将被预先置换为正确的html输出。如此,一个JSP页面看起来要比以前清爽了许多。你所做的全部事情是:
1) 引入公共的js文件
2) 在按钮所在位置定义div,并给出正确的参数

5/26/2005

Refuse Duplication —— JavaScript

Filed under: — site admin @ 3:23 pm

既然对于Web层的诸多页面冗余,我们提出了重新整理的想法,那么这种思想也必然应该是贯彻始终的,换言之,在开发过程中应该“绷紧这根弦”,敏感的不要放过任何有可能改善重复的机会。对于JSP页面,我们可以在确定框架之后,采用include机制,而对于JSP页面以外的其他元素呢?比如JavaScritp,比如CSS。

一个典型的例子是用JavaScript实现的鼠标悬浮于文本输入框之上时控件风格的改变。以前的做法是,要在每一个页面的首部先定义一段JavaScript代码:

function mouseOverInput() {
  this.className = “focus";
}
function mouseOutInput() {
 this.className = “";
}

这里的focus定义于一个外部的css文件中。然后,在文本输入框出现的地方,我们需要加上这么一句:

<input type="text” onmouseover="mouseOverInput();” onmouseout="mouseOutInput();” />

随着时间的推移,duplication不断重现,最后就会发现,有的页面中只有function定义,而没有对function的引用。事实上,上述功能在“要求所有文本输入框都遵循该规则”的前提下,完全可以有更为优雅而不致错误的做法。我们可以将下列JavaScript代码作为公共部分独立于所有页面:

/// general.js
// set onload handler
if(document.all)
 window.onload=addEvent;
function addEvent(){
  aTags = document.getElementsByTagName("INPUT");
  for (var i=0; i<atags .length; i++)
  with(aTags[i]) {
   if (type!="text")
    continue;
   // set onmouseout handler for text box
   onmouseout = function() {
    className = ‘’;
   }
   // set onmouseover handler for text box
   onmouseover = function() {
    className = ‘focus’;
   }
  }
}

这样,引用该js文件的页面,其中包含的所有文本输入框无须额外设置,都将自动获得前述界面效果。

<input type="text">

因而也不用担心漏写onmouseover和onmouseout消息响应函数的调用了。除了需要在文件首部包含一行引入该js文件的JavaScript代码外,页面内部对其他事情一概不知。

上述解决方法还有一个缺陷,假如我们希望在页面加载时做一些自定义的初始化操作,我们也许会在页面里定义自己的myInit函数,然后将其设置为onload消息的处理函数。比如:

<script src="general.js"></script>
<script>
function myInit() {
 alert(’init’);
}
</script>
<body onload="myInit">
</body> >

此时,general.js中的addEvent函数将不起作用。为了解决这个问题,我们可以做一个简单的约定:在general.js中定义一个dummy myInit,它什么事情都不做,在addEvent中会将其作为callback函数进行调用。

function addEvent(){
 // …
 myInit();
}
function myInit() {}

如果我们需要在本页面内定义自己的初始化行为,则可以在引入general.js之后再行定制myInit。并且,我们不用在body标记中声明onload属性了。当然,如果你希望强行“注销”addEvent的功能,也可以这么做:

<body onload="">
</body>

5/18/2005

ibatis使用心得——返回Map的Map

Filed under: — site admin @ 10:56 pm

一般而言,ibatis的sql map是通过JavaBean的属性与数据库表字段的映射来完成一些数据库存取的。而有些场合下,比如数据表本身较为简单,我们并不希望为此单独构造一个JavaBean而“污染”对象系统。此时,我们可以利用Map作为返回结果来代替JavaBean对象。见下面的sql map片段:

<resultMap id="getItemsResult” class="java.util.HashMap">
<result property="itemName” column="item_name” />
<result property="itemValue” column="item_value” />
</resultMap>
<select id="getItems” resultMap="getItemsResult">
select item_name, item_value from item_table
</select>

这里显式指定了一个resultMap,利用SqlMapClient的queryForList将会返回一个HashMap List,每个List元素都将是一个Map对象。

有时候,我们并不希望返回的是Object List,而希望返回某种形式的Map。无须更改sql map,我们可以利用SqlMapClient的queryForMap达到这一目的。此时,需要指定Map的key和value,见下面的代码片段:

getSqlMapClientTemplate().queryForMap("getItems", null, “itemName", “itemValue");

该函数的返回结果将是一个HashMap Map。这里,指定了key和value分别为itemName和itemValue,根据sql map中的定义,实际对应于item_name和item_value字段。也可以定义key为某个字段,而value为整个HashMap对象(或者JavaBean对象):

getSqlMapClientTemplate().queryForMap("getItems", null, “itemName");

此外,sql map还有提供一种隐式的resultMap(详见reference),但是经过试验,这种隐式resultMap在返回HashMap Map的时候,并没有得到正确结果,但显式的resultMap声明是没有问题的。

5/17/2005

一次TDD实践经历

Filed under: — site admin @ 11:50 am

昨天在重构一位同事的代码时,小试了一下TDD的开发方式。在看过Kent的TDD之后,做这样的实践,感觉效果还是不错的。进行的时候颇有点像Money Example的味道。

因为要将原来散布于JSP页面中的部分代码逻辑移出,因此我引入了一个新类。在未及代码实现之前,先写了Test Case,然后一点点充实,Test Case与实现类的规模同步增长。

不过,过程中也再次体会到了一些问题:

我在某个Servlet类中加入了一段代码,而正是这段代码中的一个小疏忽,使我为此调试了4个小时之久,这期间包含了反复的部署和启停服务器。试想,如果将这段代码移入POJO进行单独的测试,相信很快就可以在我真正关注的范围之内通过assert来暴露问题,而不是一直等到UI表现上的异常反映,才促使我去通过debug的方式来逐步排查。实际上,我们的时间往往就是在这样不知不觉的过程中“浪费”掉的,虽然看起来从出现错误到排查错误的过程很自然很合理。

不过,当我调试完错误后,试图将这段代码从Servlet移出时,确发现了绊脚石:代码中存在对Singleton的调用,该Singleton又间接包含了对数据库的访问。这使我最终放弃了努力。当然,这么做还有另外两个理由:相信debug之后的代码不会再有问题;时间紧迫。

由此,也可以再次给我一个深刻的启示,即:如果要真正高效而方便的完成测试编写,一定记住两点:
Programme to Interface
Test Plain Object

培养起主观上使代码易于测试的意识并不容易,因为它涉及定势开发习惯的改变。如果问我为什么开始的时候要在Servlet类中加入那段代码,我也无法回答,只能归咎于惯常思维。

5/16/2005

一种工具、技术、方法论是否被广为接受的两个要点

Filed under: — site admin @ 10:21 pm

Write the tests you wish you had. If you don’t, you will eventually break something while refactoring. Then you’ll get bad feelings about refactoring and stop doing it so much.

一种工具、技术,甚或是方法论,能否被大家广为接纳与传布,有两点至关重要:其一是自身简洁直观便于掌握,其二是恰如其分的方式方法。如果达不到前者,那么工具、技术、方法论的实际价值就值得怀疑。反之,则是个人认识与修为的欠缺,而不能盲目的一概否定。比如上面的反面教材,正是由于没有适时的引入测试,才致于对重构产生怀疑与恐惧,并最终止步不前,重回老路。而导致这一结果的真正原因恰在于错误的实践方式,并非重构之过。

很多时候,失败的软件过程实践往往就是因为团队成员觉得难于施行,在遇到一系列挫折之后,最终选择了放弃,甚至产生了偏见,即使他原本很想尝试新鲜事物。应该从上述两点来考察失败的原因。是方法论本身的问题,还是自身理解的偏差。

一件最近发生的让我很沮丧的事情:在我充分认识沟通的重要之后,却无奈于我与另一位同事的相隔距离十分的远(他的办公地点在与我们部门相对的另一个部门,分属两个办公场所,中间一条过道相隔),于是,我希望借助于远程桌面,但是对方的Win XP(SP2)操作系统似乎很顽固,致使我最终选择放弃。由此带来的影响,我估计沟通起码减少一半。

5/15/2005

The rhythm of TDD

Filed under: — site admin @ 3:07 pm

Kent Beck在他的TDD一书讲到有关TDD的“节奏”的问题:

The rhythm of test-driven development
1. Quickly add a test
2. Run all tests and see the new one fail
3. Make a little change
4. Run all tests and see them all succeed
5. Refactor to remove duplication

很喜欢rhythm这个词。实际上,如Kent所述,编写Test Case有如跳“韵律操”,节奏快慢全然在乎于自己的掌控。而对于节奏的把握,恰是因人而异的,即使同一个人于不同时段,亦有不同的表现。以往曾听人抱怨于TDD的“琐碎”:一个Money Example,就这么点内容,要分作这么多步序,要构筑一个大系统岂不累死人了。并且,这种夸张的“微观”行为难道不会影响实践者的“大局关”吗?难道不会使其偏题于千里之外吗?殊不知,Kent作为教练,在传授TDD技艺的时候,自然是要循循善诱的讲解分解动作的。但是作为实践者的我们,并非要拘泥于老师所教的“节奏”,所谓授人以渔,这才是老师的真正本意。何况Kent在书中不止一次的重申了这一点,看看下面这些散布于TDD中的零星陈述,是不是觉得软件的实现过程也很具“韵律”呢?

When everything is going smoothly and you know what to type, use Obvious Implementation—type in the real implementation. As soon as you get an unexpected red bar, back up, shift to Fake It—return a constant and gradually replace constants with variables until you have the real code. When confidence is back, go back to obvious implementations.

This is the kind of tuning you will be doing constantly with TDD. Are the teenytiny steps feeling restrictive? Take bigger steps. Are you feeling a little unsure? Take smaller steps. TDD is a steering process—a little this way, a little that way. There is no right step size, now and forever.

It is not necessary to work in such tiny steps as these. Once you’ve mastered TDD, you will be able to work in much bigger leaps of functionality between test cases. However, to master TDD you need to be able to work in such tiny steps when they are called for.

You will likely end up with about the same number of lines of test code as model code when TDDing. For TDD to make economic sense, either you will have to be able to write twice as many lines per day as before, or write half as many lines for the same functionality. You’ll have to measure and see what effect TDD has on your own practice. Be sure to factor debugging, integrating, and explaining time into your metrics, though.

Powered by WordPress