﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>C++博客-eXile 的专栏-随笔分类-编程与设计</title><link>http://www.cppblog.com/eXile/category/7020.html</link><description /><language>zh-cn</language><lastBuildDate>Sun, 20 Jul 2008 19:51:46 GMT</lastBuildDate><pubDate>Sun, 20 Jul 2008 19:51:46 GMT</pubDate><ttl>60</ttl><item><title>纯真IP数据库格式详解(zt)</title><link>http://www.cppblog.com/eXile/archive/2008/07/20/56679.html</link><dc:creator>eXile</dc:creator><author>eXile</author><pubDate>Sun, 20 Jul 2008 05:46:00 GMT</pubDate><guid>http://www.cppblog.com/eXile/archive/2008/07/20/56679.html</guid><wfw:comment>http://www.cppblog.com/eXile/comments/56679.html</wfw:comment><comments>http://www.cppblog.com/eXile/archive/2008/07/20/56679.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/eXile/comments/commentRss/56679.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/eXile/services/trackbacks/56679.html</trackback:ping><description><![CDATA[转自<a  href="http://lumaqq.linuxsir.org/article/qqwry_format_detail.html">http://lumaqq.linuxsir.org/article/qqwry_format_detail.html</a> <br><br>
<center>
<h1>纯真IP数据库格式详解</h1>
</center>
<center></center>
<blockquote><strong>摘要</strong> <br>网络上的IP数据库以纯真版的最为流行，LumaQQ也采用了纯真版IP数据库做为IP查询功能的
基础。不过关于其格式的文档却非常之少，后来终于在网上找到了一份文档，得以了解其内幕，不过那份文档寥寥数语，也是颇为耐心才读明白。在这里我重写一
份，以此做为LumaQQ开发者文档的一部分，我想还是必要的。本文详细介绍了纯真IP数据库的格式，并且给出了一些Demo以供参考。
<p><strong>Luma, 清华大学 <br></strong>修改日期： 2005/01/14 </p>
<p>Note: 在此感谢纯真IP数据库作者金狐和那唯一一份文档的作者。</p>
<strong>修改历史:</strong> <br>2005-01-14 修改了原来一些表达不清和错误的地方 </blockquote>
<hr width="100%">
<p>自从有了IP数据库这种东西，QQ外挂的显示IP功能也随之而生，本人见识颇窄，是否还有其他应用不得而知，不过，IP数据库确实是个不错的东西。
如今网络上最流行的IP数据库我想应该是纯真版的（说错了也不要扁我），迄今为止其IP记录条数已经接近30000，对于有些IP甚至能精确到楼层，不亦
快哉。2004年4、5月间，正逢LumaQQ破土动工，为了加上这个人人都喜欢，但是好像人人都不知道为什么喜欢的显IP功能，我也采用了纯真版IP数
据库，它的优点是记录多，查询速度快，它只用一个文件QQWry.dat就包含了所有记录，方便嵌入到其他程序中，也方便升级。</p>
<h3>基本结构</h3>
<p>QQWry.dat文件在结构上分为3块：文件头，记录区，索引区。一般我们要查找IP时，先在索引区查找记录偏移，然后再到记录区读出信息。由于
记录区的记录是不定长的，所以直接在记录区中搜索是不可能的。由于记录数比较多，如果我们遍历索引区也会是有点慢的，一般来说，我们可以用二分查找法搜索
索引区，其速度比遍历索引区快若干数量级。图1是QQWry.dat的文件结构图。</p>
<center><img  src="http://lumaqq.linuxsir.org/article/images/1/qqwry_dat_overview.gif" twffan="done" border="0"> <br><br>图1. QQWry.dat文件结构</center>
<p>要注意的是，QQWry.dat里面全部采用了little-endian字节序</p>
<h3>一. 了解文件头</h3>
<p>QQWry.dat的文件头只有8个字节，其结构非常简单，首四个字节是第一条索引的绝对偏移，后四个字节是最后一条索引的绝对偏移。</p>
<h3>二. 了解记录区</h3>
<p>每条IP记录都由国家和地区名组成，国家地区在这里并不是太确切，因为可能会查出来&#8220;清华大学计算机系&#8221;之类的，这里清华大学就成了国家名了，所以
这个国家地区名和IP数据库制作的时候有关系。所以记录的格式有点像QName，有一个全局部分和局部部分组成，我们这里还是沿用国家名和地区名的说法。</p>
<p>于是我们想象着一条记录的格式应该是:
[IP地址][国家名][地区名]，当然，这个没有什么问题，但是这只是最简单的情况。很显然，国家名和地区名可能会有很多的重复，如果每条记录都保存一
个完整的名称拷贝是非常不理想的，所以我们就需要重定向以节省空间。所以为了得到一个国家名或者地区名，我们就有了两个可能：第一就是直接的字符串表示的
国家名，第二就是一个4字节的结构，第一个字节表明了重定向的模式，后面3个字节是国家名或者地区名的实际偏移位置。对于国家名来说，情况还可能更复杂
些，因为这样的重定向最多可能有两次。</p>
<p>那么什么是重定向模式？根据上面所说，一条记录的格式是[IP地址][国家记录][地区记录]，如果国家记录是重定向的话，那么地区记录是有可能没有的，于是就有了两种情况，我管他叫做模式1和模式2。我们对这些格式的情况举图说明：</p>
<center><img  src="http://lumaqq.linuxsir.org/article/images/1/ip_record_1.gif" twffan="done" border="0"> <br><br>图2. IP记录的最简单形式</center>
<p>图2表示了最简单的IP记录格式，我想没有什么可以解释的</p>
<center><img  src="http://lumaqq.linuxsir.org/article/images/1/ip_record_2.gif" twffan="done" border="0"> <br><br>图3. 重定向模式1</center>
<p>图3演示了重定向模式1的情况。我们看到在模式1的情况下，地区记录也跟着国家记录走了，在IP地址之后只剩下了国家记录的4字节，后面3个字节构成了一个指针，指向了实际的国家名，然后又跟着地址名。模式1的标识字节是0x01。</p>
<center><img  src="http://lumaqq.linuxsir.org/article/images/1/ip_record_3.gif" twffan="done" border="0"> <br><br>图4. 重定向模式2</center>
<p>图4演示了重定向模式2的情况。我们看到了在模式2的情况下（其标识字节是0x02），地区记录没有跟着国家记录走，因此在国家记录之后4个字节之
后还是有地区记录。我想你已经明白了模式1和模式2的区别，即：模式1的国家记录后面不会再有地区记录，模式2的国家记录后会有地区记录。下面我们来看一
下更复杂的情况。</p>
<center><img  src="http://lumaqq.linuxsir.org/article/images/1/ip_record_5.gif" twffan="done" border="0"> <br><br>图5. 混和情况1</center>
<p>图5演示了当国家记录为模式1的时候可能出现的更复杂情况，在这种情况下，重定向指向的位置仍然是个重定向，不过第二次重定向为模式2。大家不用担
心，没有模式3了，这个重定向也最多只有两次，并且如果发生了第二次重定向，则其一定为模式2，而且这种情况只会发生在国家记录上，对于地区记录，模式1
和模式2是一样的，地区记录也不会发生2次重定向。不过，这个图还可以更复杂，如图7：</p>
<center><img  src="http://lumaqq.linuxsir.org/article/images/1/ip_record_6.gif" twffan="done" border="0"> <br><br>图6. 混和情况2</center>
<p>图6是模式1下最复杂的混和情况，不过我想应该也很好理解，只不过地区记录也来重定向而已，有一点我要提醒你，如果重定向的地址是0，则表示未知的地区名。</p>
<p>所以我们总结如下：一条IP记录由[IP地址][国家记录][地区记录]组成，对于国家记录，可以有三种表示方式：字符串形式，重定向模式1和重定
向模式2。对于地区记录，可以有两种表示方式：字符串形式和重定向，另外有一条规则：重定向模式1的国家记录后不能跟地区记录。按照这个总结，在这些方式
中合理组合，就构成了IP记录的所有可能情况。</p>
<h3>设计的理由</h3>
<p>在我们继续去了解索引区的结构之前，我们先来了解一下为何记录区的结构要如此设计。我想你可能想到了答案：字符串重用。没错，在这种结构下，对于一
个国家名和地区名，我只需要保存其一次就可以了。我们举例说明，为了表示方便，我们用小写字母代表IP记录，C表示国家名，A表示地区名： </p>
<ol>
    <li>有两条记录a(C1, A1), b(C2, A2)，如果C1 = C2, A1 = A2，那么我们就可以使用图3显示的结构来实现重用 </li>
    <li>有三条记录a(C1, A1), b(C2, A2), c(C3, A3)，如果C1 = C2, A2 = A3，现在我们想存储记录b，那么我们可以用图6的结构来实现重用 </li>
    <li>有两条记录a(C1, A1), b(C2, A2)，如果C1 = C2，现在我们想存储记录b，那么我们可以采用模式2表示C2，用字符串表示A2 </li>
</ol>
<p>你可以举出更多的情况，你也会发现在这种结构下，不同的字符串只需要存储一次。</p>
<h3>了解索引区</h3>
<p>在"了解文件头"部分，我们说明了文件头实际上是两个指针，分别指向了第一条索引和最后一条索引的绝对偏移。如图8所示：</p>
<center><img  src="http://lumaqq.linuxsir.org/article/images/1/header_to_index.gif" twffan="done" border="0"> <br><br>图8. 文件头指向索引区图示</center>
<p>实在是很简单，不是吗？从文件头你就可以定位到索引区，然后你就可以开始搜索IP了！每条索引长度为7个字节，前4个字节是起始IP地址，后三个字
节就指向了IP记录。这里有些概念需要说明一下，什么是起始IP，那么有没有结束IP？ 假设有这么一条记录：166.111.0.0 -
166.111.255.255，那么166.111.0.0就是起始IP，166.111.255.255就是结束IP，结束IP就是IP记录中的那头
4个字节，这下你应该就清楚了吧。于是乎，每条索引配合一条记录，构成了一个IP范围，如果你要查找166.111.138.138所在的位置，你就会发
现166.111.138.138落在了166.111.0.0 - 166.111.255.255
这个范围内，那么你就可以顺着这条索引去读取国家和地区名了。那么我们给出一个最详细的图解吧：</p>
<center><img  src="http://lumaqq.linuxsir.org/article/images/1/overall_format.gif" twffan="done" border="0"> <br><br>图9. 文件详细结构</center>
<p>现在一切都清楚了是不是？也许还有一点你不清楚，QQWry.dat的版本信息存在哪里呢？
答案是：最后一条IP记录实际上就是版本信息，最后一条记录显示出来就是这样：255.255.255.0 255.255.255.255 纯真网络
2004年6月25日IP数据。OK，到现在你应该全部清楚了。</p>
<h3>Demo</h3>
<p>下一步：我给出一个读取IP记录的程序片断，此片断摘录自LumaQQ源文件edu.tsinghua.lumaqq.IPSeeker.java，如果你有兴趣，可以下载源代码详细看看。</p>
<pre> /**<br>  * 给定一个ip国家地区记录的偏移，返回一个IPLocation结构<br>  * @param offset 国家记录的起始偏移<br>  * @return IPLocation对象<br>  */<br> private IPLocation getIPLocation(long offset) {<br>  try {<br>   // 跳过4字节ip<br>   ipFile.seek(offset + 4);<br>   // 读取第一个字节判断是否标志字节<br>   byte b = ipFile.readByte();<br>   if(b == REDIRECT_MODE_1) {<br>    // 读取国家偏移<br>    long countryOffset = readLong3();<br>    // 跳转至偏移处<br>    ipFile.seek(countryOffset);<br>    // 再检查一次标志字节，因为这个时候这个地方仍然可能是个重定向<br>    b = ipFile.readByte();<br>    if(b == REDIRECT_MODE_2) {<br>     loc.country = readString(readLong3());<br>     ipFile.seek(countryOffset + 4);<br>    } else<br>     loc.country = readString(countryOffset);<br>    // 读取地区标志<br>    loc.area = readArea(ipFile.getFilePointer());<br>   } else if(b == REDIRECT_MODE_2) {<br>    loc.country = readString(readLong3());<br>    loc.area = readArea(offset + 8);<br>   } else {<br>    loc.country = readString(ipFile.getFilePointer() - 1);<br>    loc.area = readArea(ipFile.getFilePointer());<br>   }<br>   return loc;<br>  } catch (IOException e) {<br>   return null;<br>  }<br> } <br><br> /**<br>  * 从offset偏移开始解析后面的字节，读出一个地区名<br>  * @param offset 地区记录的起始偏移<br>  * @return 地区名字符串<br>  * @throws IOException 地区名字符串<br>  */<br> private String readArea(long offset) throws IOException {<br>  ipFile.seek(offset);<br>  byte b = ipFile.readByte();<br>  if(b == REDIRECT_MODE_1 || b == REDIRECT_MODE_2) {<br>   long areaOffset = readLong3(offset + 1);<br>   if(areaOffset == 0)<br>    return LumaQQ.getString("unknown.area");<br>   else<br>    return readString(areaOffset);<br>  } else<br>   return readString(offset);<br> }<br><br> /**<br>  * 从offset位置读取3个字节为一个long，因为java为big-endian格式，所以没办法<br>  * 用了这么一个函数来做转换<br>  * @param offset 整数的起始偏移<br>  * @return 读取的long值，返回-1表示读取文件失败<br>  */<br> private long readLong3(long offset) {<br>  long ret = 0;<br>  try {<br>   ipFile.seek(offset);<br>   ipFile.readFully(b3);<br>   ret |= (b3[0] &amp; 0xFF);<br>   ret |= ((b3[1] &lt;&lt; 8) &amp; 0xFF00);<br>   ret |= ((b3[2] &lt;&lt; 16) &amp; 0xFF0000);<br>   return ret;<br>  } catch (IOException e) {<br>   return -1;<br>  }<br> } <br> <br> /**<br>  * 从当前位置读取3个字节转换成long<br>  * @return 读取的long值，返回-1表示读取文件失败<br>  */<br> private long readLong3() {<br>  long ret = 0;<br>  try {<br>   ipFile.readFully(b3);<br>   ret |= (b3[0] &amp; 0xFF);<br>   ret |= ((b3[1] &lt;&lt; 8) &amp; 0xFF00);<br>   ret |= ((b3[2] &lt;&lt; 16) &amp; 0xFF0000);<br>   return ret;<br>  } catch (IOException e) {<br>   return -1;<br>  }<br> }<br><br> /**<br>  * 从offset偏移处读取一个以0结束的字符串<br>  * @param offset 字符串起始偏移<br>  * @return 读取的字符串，出错返回空字符串<br>  */<br> private String readString(long offset) {<br>  try {<br>   ipFile.seek(offset);<br>   int i;<br>   for(i = 0, buf[i] = ipFile.readByte(); buf[i] != 0; buf[++i] = ipFile.readByte());<br>   if(i != 0) <br>       return Utils.getString(buf, 0, i, "GBK");<br>  } catch (IOException e) {   <br>      log.error(e.getMessage());<br>  }<br>  return "";<br> }<br></pre>
<p>代码并不复杂，getIPLocation是主要方法，它检查国家记录格式，并针对字符串形式，模式1，模式2采用不同的代码，readArea则相对简单，因为只有字符串和重定向两种情况需要处理。</p>
<h3>总结</h3>
<p>纯真IP数据库的结构使得查找IP简单迅速，不过你想要编辑它却是比较麻烦的，我想应该需要专门的工具来生成QQWry.dat文件，由于其文件格式的限制，你要直接添加IP记录就不容易了。不过，能查到IP已经很开心了，希望纯真记录越来越多～。</p><img src ="http://www.cppblog.com/eXile/aggbug/56679.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/eXile/" target="_blank">eXile</a> 2008-07-20 13:46 <a href="http://www.cppblog.com/eXile/archive/2008/07/20/56679.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>单元测试[zt]</title><link>http://www.cppblog.com/eXile/archive/2008/04/29/48422.html</link><dc:creator>eXile</dc:creator><author>eXile</author><pubDate>Tue, 29 Apr 2008 05:39:00 GMT</pubDate><guid>http://www.cppblog.com/eXile/archive/2008/04/29/48422.html</guid><wfw:comment>http://www.cppblog.com/eXile/comments/48422.html</wfw:comment><comments>http://www.cppblog.com/eXile/archive/2008/04/29/48422.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/eXile/comments/commentRss/48422.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/eXile/services/trackbacks/48422.html</trackback:ping><description><![CDATA[<br>来源: WingFire On Toplanguange<br><br>1.单元测试库要尽量少地增加开发人员的负担。额外负担必须尽可能直白，傻瓜化。<br>市面上的许多讲到单元测试的书都是以XUnit为蓝本的，这导致CppUnit的接受程度颇高。CppUnit中规中矩，四平八稳，但不够犀利。个人认为boost.test最简单，只要一个BOOST_AUTO_TEST_CASE就可以开始了。CppUnit则要复杂一点，而这种复杂性是多余的，甚至是有害的。用CppUnit的时候，我看到有人为了共享测试代码，随便在test case里面加函数，然后复用，结果导致case不独立。boost.test倾向于不要建立.h文件，所以要复用不方便（或者，不习惯在Cpp中复用），反而不容易犯错误。<br>2.实施单元测试，必须能够让程序员看得到好处并尽快受益。新项目必须尽早引入单元测试，要早在正式编码之前。<br>想立刻让UT变得完美是不可能的，行政命令也不会有好结果。在推行单元测试的时候，教育很重要。必须让同事能理解单元测试为什么有效，如何工作，UT编写准则之类的问题。另外，在工作多年的程序员（对UT缺乏认识的）中推行单元测试，阻力更大。更要注意教育和反馈。最好的反馈就是帮助他们从单元测试中获益。例如，修改更轻松，思维更面向接口，bug更少，代码更容易理解等等。作为推动者，有义务去主动发现这些改善之处并积极地反馈给程序员。从而增强应用UT的信心和意愿。<br>3.必须充分自动化。<br>UT的任务之一是给代码编织一层细密的保护网。程序员应该认识到，单元测试是为自己服务的，所以，我们要的是完成任务而不是展示。能够自动地完成任务则是最好的。如果单元测试过多地干扰程序员的正常思考，就会招致更多的抵触（抵触总是存在的）或敷衍。敷衍是可怕的。我向来是把单元测试的运行作为build的一个步骤的。成功的单元测试不需要输出任何信息，最多在全部passs的时候给个OK就足够了。图形界面的测试工具在我看来也是鸡肋，新手的玩具而已。图形界面既不利于参数化运行，也不方便自动化，实在是降低开发效率的杀手。<br>4.不要追求完美的UT。<br>不是所有东西都很容易测试。UT要求被测试的东西可重现，可观测。 基本上，大部分的物理操作因为缺乏可重复性或可观察性，很难测试，例如database，GUI （注意，这不意味着在实现一个GUI库或db driver时就不能做UT了）。勉强UT全覆盖，既不现实，也不实惠。并且，这很可能让UT变得复杂，高成本，这是非常危险的和不值得的。我的主张是，很难测，那就不测，但要正确应对。我的做法是将难测的部分隔离到一些抽象层当中去。然后为这些抽象层写MockObject即可测试了。我曾经应用在数据库应用中，并很自然的得到一个良好的数据访问的抽象层，单元测试就只测了这个抽象层。而实际的数据库访问中的物理操作部分，则从单元测试中剥离出去。如果坚持分离物理操作和逻辑操作的话，这个剥离出去的部分一般很小很有限，也很容易测试。相反，如果不剥离，将导致单元测试的结果要依赖数据库的状态。这种额外的依赖性没什么好处。这里的关键是，必须让不可测的部分尽可能隔离，尽可能小，尽可能地将逻辑操作从物理操作中分离出来。被隔离部分所包含的逻辑操作仍然需要写UT。 
<img src ="http://www.cppblog.com/eXile/aggbug/48422.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/eXile/" target="_blank">eXile</a> 2008-04-29 13:39 <a href="http://www.cppblog.com/eXile/archive/2008/04/29/48422.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>设计Qt风格的C++API (zt)</title><link>http://www.cppblog.com/eXile/archive/2008/02/18/42857.html</link><dc:creator>eXile</dc:creator><author>eXile</author><pubDate>Sun, 17 Feb 2008 16:50:00 GMT</pubDate><guid>http://www.cppblog.com/eXile/archive/2008/02/18/42857.html</guid><wfw:comment>http://www.cppblog.com/eXile/comments/42857.html</wfw:comment><comments>http://www.cppblog.com/eXile/archive/2008/02/18/42857.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/eXile/comments/commentRss/42857.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/eXile/services/trackbacks/42857.html</trackback:ping><description><![CDATA[<h1>设计Qt风格的C++API</h1>
<strong>
<p>作者Matthias Ettrich，译者Googol Lee，原文地址在<a href="http://doc.trolltech.com/qq/qq13-apis.html"><font color=#004faf>这里</font></a>。 </p>
</strong>
<blockquote><strong>
<p>在奇趣（Trolltech），为了改进Qt的开发体验，我们做了大量的研究。这篇文章里，我打算分享一些我们的发现，以及一些我们在设计Qt4时用到的原则，并且展示如何把这些原则应用到你的代码里。 </p>
</strong>
<ul>
    <li><a href="file:///C:/Documents%20and%20Settings/wujq/Local%20Settings/Temp/Rar$EX00.547/设计Qt风格的C++API.html#SixCharacteristicsOfGoodApis"><font color=#004faf>好的API的六个特性</font></a>
    <li><a href="file:///C:/Documents%20and%20Settings/wujq/Local%20Settings/Temp/Rar$EX00.547/设计Qt风格的C++API.html#TheConvenienceTrap"><font color=#004faf>便利陷阱</font></a>
    <li><a href="file:///C:/Documents%20and%20Settings/wujq/Local%20Settings/Temp/Rar$EX00.547/设计Qt风格的C++API.html#TheBooleanParameterTrap"><font color=#004faf>布尔参数陷阱</font></a>
    <li><a href="file:///C:/Documents%20and%20Settings/wujq/Local%20Settings/Temp/Rar$EX00.547/设计Qt风格的C++API.html#StaticPolymorphism"><font color=#004faf>静态多态</font></a>
    <li><a href="file:///C:/Documents%20and%20Settings/wujq/Local%20Settings/Temp/Rar$EX00.547/设计Qt风格的C++API.html#TheArtOfNaming"><font color=#004faf>命名的艺术</font></a>
    <li><a href="file:///C:/Documents%20and%20Settings/wujq/Local%20Settings/Temp/Rar$EX00.547/设计Qt风格的C++API.html#PointersOrReferences"><font color=#004faf>指针还是引用？</font></a>
    <li><a href="file:///C:/Documents%20and%20Settings/wujq/Local%20Settings/Temp/Rar$EX00.547/设计Qt风格的C++API.html#CaseStudyQprogressbar"><font color=#004faf>例子：QProgressBar</font></a>
    <li><a href="file:///C:/Documents%20and%20Settings/wujq/Local%20Settings/Temp/Rar$EX00.547/设计Qt风格的C++API.html#HowToGetApisRight"><font color=#004faf>如何把API设计好</font></a> </li>
</ul>
</blockquote>
<p>设计应用程序接口，API，是很难的。这是一门和设计语言同样难的艺术。这里可以选择太多的原则，甚至有很多原则和其他原则有矛盾。 </p>
<p>现在，计算机科学教育把很大的力气放在算法和数据结构上，而很少关注设计语言和框架背后的原则。这让应用程序员完全没有准备去面对越来越重要的任务：创造可重用的组件。 </p>
<p>在面向对象语言普及之前，可重用的通用代码大部分是由库提供者写的，而不是应用程序员。在Qt的世界里，这种状况有了明显的改善。在任何时候，用Qt编程就是写新的组件。一个典型的Qt应用程序至少都会有几个在程序中反复使用的自定义组件。一般来说，同样的组件会成为其他应用程序的一部分。KDE，K桌面环境，走得更远，用许多追加的库来扩展Qt，实现了数百个附加类。（一般来说，一个类就是一个可重用组件，原文这里没有写清楚。） </p>
<p>但是，一个好的，高效的C++ API是由什么组成的呢？是好还是坏，取决于很多因素——比如，手头的工作和特定的目标群体。好的API有很多特性，一些特性是大家都想要的，而另一些则是针对特定问题域的。 </p>
<a name=SixCharacteristicsOfGoodApis>
<h2>好的API的六个特性</h2>
</a>
<p>API是面向程序员的，用来描述提供给最终用户的GUI是什么样子。API中的P带表程序员（Programmer），而不是程序（Program），用来强调API是给程序员用的，给人类的程序员用的。 </p>
<p>我们坚信API应该是最小化且完整的，拥有清晰且简单的语义，直觉化，容易记忆，并且引导人写出易读的代码。 </p>
<ul>
    <li><strong>最小化：</strong>最小化的API是指一个类尽可能只拥有最少的公开成员且尽可能只拥有最少的类。这个原则可以让API更简单易懂，更好记，更容易除错，且更容易改变。
    <li><strong>完整的：</strong>完整的API是指要提供所有期望的功能。这个可能与最小化原则相冲突。另外，如果一个成员函数属于一个不应该属于的类，很多潜在的使用者都会找不到这个函数。
    <li><strong>拥有清晰且简单的语义：</strong>就像其他设计工作一样，你必须遵守最小惊奇原则（the principle of least surprise）。让常见的任务简单易行。不常见的工作可行，但不会让用户过分关注。解决特殊问题时，不要让解决方案没有必要的过度通用。（比如，Qt3中的<a href="http://doc.trolltech.com/3.3/qmimesourcefactory.html"><font color=#004faf>QMimeSourceFactory</font></a>可以通过调用QImageLoader来实现不同的API。）
    <li><strong>直觉化：</strong>就像电脑上的其他东西一样，API必须是直觉化的。不同的经验和背景会导致在判断什么是直觉而什么不是时不同的感觉。如果一个中级用户不读文档就可以使用（a semi-experienced user gets away without reading the documentation，没懂这里的get away该怎么翻译），并且一个程序员不懂API就可以理解缩写的代码，这种API就是直觉化的。
    <li><strong>易于记忆：</strong>让API易于记忆，使用统一且精确的命名方法。使用可识别的模式和概念，并且避免缩写。
    <li><strong>引导易读的代码（Lead to readable code）：</strong>代码一经写就，会读（并且除错和修改）多次。易读的代码可能会花点时间来写，但是可以节省产品周期中的其他时间。 </li>
</ul>
<p>最后，记住，不同类型的用户会用到API的不同部分。虽然简单的实例化一个Qt类是非常直觉化的，让资深专家在试图子类化之前读一遍文档，是很合理的。 </p>
<a name=TheConvenienceTrap>
<h2>便利陷阱</h2>
</a>
<p>这是个常见的误解：更好的API，用更少的代码完成一件事。永远记住代码一次写就，之后需要不断的阅读并理解。比如： </p>
<pre>    <a href="http://doc.trolltech.com/3.3/qslider.html"><font color=#004faf>QSlider</font></a> *slider = new <a href="http://doc.trolltech.com/3.3/qslider.html"><font color=#004faf>QSlider</font></a>(12, 18, 3, 13, Qt::Vertical,
0, "volume");
</pre>
<p>远比下面那样难读（甚至难写）： </p>
<pre>    <a href="http://doc.trolltech.com/3.3/qslider.html"><font color=#004faf>QSlider</font></a> *slider = new <a href="http://doc.trolltech.com/3.3/qslider.html"><font color=#004faf>QSlider</font></a>(Qt::Vertical);
slider-&gt;setRange(12, 18);
slider-&gt;setPageStep(3);
slider-&gt;setValue(13);
slider-&gt;setObjectName("volume");
</pre>
<a name=TheBooleanParameterTrap>
<h2>布尔参数陷阱</h2>
</a>
<p>布尔参数通常会导致不易读的代码。更进一步，给一个已经存在的函数加入一个布尔参数，这常常是个错误。在Qt里，一个传统的例子是repaint()，这个函数带有一个布尔参数，来标识是否擦除背景（默认擦除）。这让代码通常写成： </p>
<pre>    widget-&gt;repaint(false);
</pre>
<p>初学者很容易把这句话理解成&#8220;别重画&#8221;！ </p>
<p>这样做是考虑到布尔参数可以减少一个函数，避免代码膨胀。事实上，这反而增加了代码量。有多少Qt用户真的记住了下面三行程序都是做什么的？ </p>
<pre>    widget-&gt;repaint();
widget-&gt;repaint(true);
widget-&gt;repaint(false);
</pre>
<p>一个好一些的API可能看起来是这样： </p>
<pre>    widget-&gt;repaint();
widget-&gt;repaintWithoutErasing();
</pre>
<p>在Qt4里，我们重新设计了widget，使得用户不再需要不重画背景的重画widget，来解决这个问题。Qt4原生支持双缓存，废掉了这个特性。 </p>
<p>这里还有一些例子： </p>
<pre>    widget-&gt;setSizePolicy(<a href="http://doc.trolltech.com/4.0/qsizepolicy.html"><font color=#004faf>QSizePolicy</font></a>::Fixed,
<a href="http://doc.trolltech.com/4.0/qsizepolicy.html"><font color=#004faf>QSizePolicy</font></a>::Expanding, true);
textEdit-&gt;insert("Where's Waldo?", true, true, false);
<a href="http://doc.trolltech.com/3.3/qregexp.html"><font color=#004faf>QRegExp</font></a> rx("moc_*.c??", false, true);
</pre>
<p>一个显而易见的解决方法是，使用枚举类型代替布尔参数。这正是我们在Qt4中<a href="http://doc.trolltech.com/3.3/qstring.html"><font color=#004faf>QString</font></a>大小写敏感时的处理方法。比较： </p>
<pre>    str.replace("%USER%", user, false);               // Qt 3
str.replace("%USER%", user, Qt::CaseInsensitive); // Qt 4
</pre>
<a name=StaticPolymorphism>
<h2>静态多态</h2>
</a>
<p>相似的类应该含有相似的API。在必要的时候——就是说，需要使用运行时多态的时候——这可以通过继承实现。但是多态依旧会发生在设计时期。比如，如果你用<a href="http://doc.trolltech.com/3.3/qlistbox.html"><font color=#004faf>QListBox</font></a>代替<a href="http://doc.trolltech.com/3.3/qcombobox.html"><font color=#004faf>QComboBox</font></a>，或者用<a href="http://doc.trolltech.com/4.0/qslider.html"><font color=#004faf>QSlider</font></a>代替<a href="http://doc.trolltech.com/4.0/qspinbox.html"><font color=#004faf>QSpinBox</font></a>，你会发现相似的API使这种替换非常容易。这就是我们所说的&#8220;静态多态&#8221;。 </p>
<p>静态多态也使API和程序模式更容易记忆。作为结论，一组相关类使用相似的API，有时要比给每个类提供完美的单独API，要好。 </p>
<p>（译注：C++ 0x将要引入的concept，就是静态多态的语法层实现。这个要比单独的函数名相似更强大且易用。） </p>
<a name=TheArtOfNaming>
<h2>命名的艺术</h2>
</a>
<p>命名，大概是设计API时唯一最重要的问题了。该怎么称呼这个类？成员函数该叫什么？ </p>
<h3>通用的命名规则</h3>
<p>一些规则通常对所有名字都是有用的。首先，就像我之前提到的，别用缩写。甚至很明显的缩写，比如&#8220;prev&#8221;表示&#8220;previous&#8221;从长远看也是不划算的，因为用户必须记住哪些词是缩写。 </p>
<p>如果API本身不一致，事情自然会变得很糟糕，比如， Qt3有activatePreviousWindow()和fetchPrev()。坚持&#8220;没有缩写&#8221;的规则更容易创建一致的API。 </p>
<p>另一个重要但更加微妙的规则是，在设计类的时候，必须尽力保证子类命名空间的干净。在Qt3里，没有很好的遵守这个规则。比如，拿<a href="http://doc.trolltech.com/3.3/qtoolbutton.html"><font color=#004faf>QToolButton</font></a>来举例。如果你在Qt3里，对一个<a href="http://doc.trolltech.com/3.3/qtoolbutton.html"><font color=#004faf>QToolButton</font></a>调用name()、caption()、text()或者textLabel()，你希望做什么呢？你可以在Qt Designer里拿QToolButton试试： </p>
<ul>
    <li>name属性继承自<a href="http://doc.trolltech.com/3.3/qobject.html"><font color=#004faf>QObject</font></a>，表示一个对象用于除错和测试的内部名字。
    <li>caption属性继承自<a href="http://doc.trolltech.com/3.3/qwidget.html"><font color=#004faf>QWidget</font></a>，表示窗口的标题，这个标题在视觉上对<a href="http://doc.trolltech.com/3.3/qtoolbutton.html"><font color=#004faf>QToolButton</font></a>没有任何意义，因为他们总是跟随父窗口而创建。
    <li>text属性继承自<a href="http://doc.trolltech.com/3.3/qbutton.html"><font color=#004faf>QButton</font></a>，一般情况下是按钮上现实的文字，除非useTextLabel为真。
    <li>textLabel在<a href="http://doc.trolltech.com/3.3/qtoolbutton.html"><font color=#004faf>QToolButton</font></a>里声明，并且在useTextLabel为真时显示在按钮上。 </li>
</ul>
<p>由于对可读性的关注，name在Qt4里被称作objectName，caption变成了windowsTitle，而在<a href="http://doc.trolltech.com/3.3/qtoolbutton.html"><font color=#004faf>QToolButton</font></a>里不再有单独的textLabel属性。 </p>
<h3>给类命名</h3>
<p>标识一组类而不是单独给每个类找个恰当的名字。比如，Qt4里所有模式感知项目的视图类（model-aware item view classes）都拥有-View的后缀（<a href="http://doc.trolltech.com/4.0/qlistview.html"><font color=#004faf>QListView</font></a>、<a href="http://doc.trolltech.com/4.0/qtableview.html"><font color=#004faf>QTableView</font></a>和<a href="http://doc.trolltech.com/4.0/qtreeview.html"><font color=#004faf>QTreeView</font></a>），并且对应基于项目的类都用后缀-Widget代替（<a href="http://doc.trolltech.com/4.0/qlistwidget.html"><font color=#004faf>QListWidget</font></a>、<a href="http://doc.trolltech.com/4.0/qtablewidget.html"><font color=#004faf>QTableWidget</font></a>和<a href="http://doc.trolltech.com/4.0/qtreewidget.html"><font color=#004faf>QTreeWidget</font></a>）。 </p>
<h3>给枚举类型及其值命名</h3>
<p>当声明枚举时，时刻记住，在C++（不像Java和C#）中，使用枚举值不需要类型信息。下面的例子演示了给枚举值起个太过常用的名字所引起的危害： </p>
<pre>    namespace Qt
{
enum Corner { TopLeft, BottomRight, ... };
enum CaseSensitivity { Insensitive, Sensitive };
...
};
tabWidget-&gt;setCornerWidget(widget, Qt::TopLeft);
str.indexOf("$(QTDIR)", Qt::Insensitive);
</pre>
<p>在最后一行，Insensitive是什么意思？一个用于命名枚举值的指导思想是，在每个枚举值里，至少重复一个枚举类型名中的元素： </p>
<pre>    namespace Qt
{
enum Corner { TopLeftCorner, BottomRightCorner, ... };
enum CaseSensitivity { CaseInsensitive,
CaseSensitive };
...
};
tabWidget-&gt;setCornerWidget(widget, Qt::TopLeftCorner);
str.indexOf("$(QTDIR)", Qt::CaseInsensitive);
</pre>
<p>当枚举值可以用&#8220;或&#8221;连接起来当作一个标志时，传统的做法是将&#8220;或&#8221;的结果作为一个int保存，这不是类型安全的。Qt4提供了一个模板类 <a href="http://doc.trolltech.com/4.0/qflags.html"><font color=#004faf>QFlags</font></a>&lt;T&gt;来实现类型安全，其中T是个枚举类型。为了方便使用，Qt为很多标志类名提供了typedef，所以你可以使用类型 Qt::Alignment代替<a href="http://doc.trolltech.com/4.0/qflags.html"><font color=#004faf>QFlags</font></a>&lt;Qt::AlignmentFlag&gt;。 </p>
<p>为了方便，我们给枚举类型单数的名字（这样表示枚举值一次只能有一个标志），而&#8220;标志&#8221;则使用复数名字。比如： </p>
<pre>    enum RectangleEdge { LeftEdge, RightEdge, ... };
typedef <a href="http://doc.trolltech.com/4.0/qflags.html"><font color=#004faf>QFlags</font></a>&lt;RectangleEdge&gt; RectangleEdges;
</pre>
<p>有些情况下，&#8220;标志&#8220;类使用了单数的名字。这时，枚举类使用-Flag做后缀： </p>
<pre>    enum AlignmentFlag { AlignLeft, AlignTop, ... };
typedef <a href="http://doc.trolltech.com/4.0/qflags.html"><font color=#004faf>QFlags</font></a>&lt;AlignmentFlag&gt; Alignment;
</pre>
<p>（这里为啥不是把&#8221;标志&#8220;类用-Flag做后缀，而是把枚举值做后缀呢？感觉有点混淆&#8230;&#8230;） </p>
<h3>给函数和参数命名</h3>
<p>给函数命名的一个规则是，名字要明确体现出这个函数是否有副作用。在Qt3，常数函数<a href="http://doc.trolltech.com/3.3/qstring.html"><font color=#004faf>QString</font></a>::simplifyWhiteSpace()违反了这个原则，因为它返回类一个<a href="http://doc.trolltech.com/3.3/qstring.html"><font color=#004faf>QString</font></a>实例，而不是像名字所提示的那样，更改了调用这个函数的实例本身。在Qt4，这个函数被重命名为<a href="http://doc.trolltech.com/3.3/qstring.html"><font color=#004faf>QString</font></a>::simplified()。 </p>
<p>参数名是程序员的重要信息来源，虽然在使用API时，并不直接展示在代码里。由于现代IDE在程序员写代码时可以自动显示参数名（就是自动感知或者自动补全之类的功能），值得花时间给头文件里声明的参数一个合适的名字，并且在文档中也使用相同的名字。 </p>
<h3>给布尔值设置函数（Setter）、提取函数（Getter）和属性命名</h3>
<p>给布尔属性的设置函数和提取函数一个合适的名字，总是非常痛苦的。提取函数应该叫做checked()还是isChecked()？scrollBarsEnabled()还是areScrollBarEnabled()? </p>
<p>在Qt4里，我们使用下列规则命名提取函数： </p>
<ul>
    <li>形容类的属性使用is-前缀。比如：
    <ul>
        <li><tt>isChecked()</tt>
        <li><tt>isDown()</tt>
        <li><tt>isEmpty()</tt>
        <li><tt>isMovingEnable()</tt> </li>
    </ul>
    另外，应用到复数名词的形容类属性没有前缀：
    <ul>
        <li><tt>scrollBarsEnabled()</tt>，而不是<tt>areScrollBarsEnabled()</tt> </li>
    </ul>
    <li>动词类的属性不使用前缀，且不使用第三人称（-s）：
    <ul>
        <li><tt>acceptDrops()</tt>，而不是<tt>acceptsDrops()</tt>
        <li><tt>allColumnsShowFocus()</tt> </li>
    </ul>
    <li>名词类的属性，通常没有前缀：
    <ul>
        <li><tt>autoCompletion()</tt>，而不是<tt>isAutoCompletion()</tt>
        <li><tt>boundaryChecking()</tt> </li>
    </ul>
    有时，没有前缀就会引起误解，这种情况使用前缀is-：
    <ul>
        <li><tt>isOpenGLAvailable()</tt>，而不是<tt>openGL()</tt>
        <li><tt>isDialog()</tt>，而不是<tt>dialog()</tt> </li>
    </ul>
    （通过调用dialogue()方法，正常情况下会期望返回一个<a href="http://doc.trolltech.com/4.0/qdialog.html"><font color=#004faf>QDialog</font></a>*的实例。） </li>
</ul>
<p>设置函数名字继承自提取函数名，只是移掉了所有前缀，并使用set-做前缀，比如：setDown()还有setScrollBarsEnabled()。属性的名字与提取函数相同，只是去掉了前缀。 </p>
<a name=PointersOrReferences>
<h2>指针还是引用？</h2>
</a>
<p>传出参数的最佳选择是什么，指针还是引用？ </p>
<pre>    void getHsv(int *h, int *s, int *v) const
void getHsv(int &amp;h, int &amp;s, int &amp;v) const
</pre>
<p>大部分C++书推荐在能用引用的地方就用引用，这是因为一般认为引用比指针更&#8220;安全且好用&#8221;。然而，在奇趣（Trolltech），我们倾向使用指针，因为这让代码更易读。比较： </p>
<pre>    color.getHsv(&amp;h, &amp;s, &amp;v);
color.getHsv(h, s, v);
</pre>
<p>只有第一行能清楚的说明，在函数调用后，h、s和v将有很大几率被改动。 </p>
<a name=CaseStudyQprogressbar>
<h2>例子：QProgressBar</h2>
</a>
<p>为了展示如何实际应用这些概念，我们将学习Qt3中的API <a href="http://doc.trolltech.com/3.3/qprogressbar.html"><font color=#004faf>QProgressBar</font></a>并和Qt4里相通的API做比较。在Qt3里： </p>
<pre>    class <a href="http://doc.trolltech.com/3.3/qprogressbar.html"><font color=#004faf>QProgressBar</font></a> : public <a href="http://doc.trolltech.com/3.3/qwidget.html"><font color=#004faf>QWidget</font></a>
{
...
public:
int totalSteps() const;
int progress() const;
const <a href="http://doc.trolltech.com/3.3/qstring.html"><font color=#004faf>QString</font></a> &amp;progressString() const;
bool percentageVisible() const;
void setPercentageVisible(bool);
void setCenterIndicator(bool on);
bool centerIndicator() const;
void setIndicatorFollowsStyle(bool);
bool indicatorFollowsStyle() const;
public slots:
void reset();
virtual void setTotalSteps(int totalSteps);
virtual void setProgress(int progress);
void setProgress(int progress, int totalSteps);
protected:
virtual bool setIndicator(<a href="http://doc.trolltech.com/3.3/qstring.html"><font color=#004faf>QString</font></a> &amp;progressStr,
int progress,
int totalSteps);
...
};
</pre>
<p>API相当复杂，且不统一。比如，仅从名字reset()并不能理解其作用，setTotalSteps()和setProgress()是紧耦合的。 </p>
<p>改进API的关键，是注意到<a href="http://doc.trolltech.com/3.3/qprogressbar.html"><font color=#004faf>QProgressBar</font></a>和Qt4的<a href="http://doc.trolltech.com/4.0/qabstractspinbox.html"><font color=#004faf>QAbstractSpinBox</font></a>类及其子类<a href="http://doc.trolltech.com/4.0/qspinbox.html"><font color=#004faf>QSpinBox</font></a>，<a href="http://doc.trolltech.com/4.0/qslider.html"><font color=#004faf>QSlider</font></a>和<a href="http://doc.trolltech.com/4.0/qdial.html"><font color=#004faf>QDial</font></a>很相似。解决方法？用minimum、maximum和value代替progress和totalSteps。加入alueChanged()信号。加入setRange()函数。 </p>
<p>之后观察progressString、percentage和indicator实际都指一个东西：在进度条上显示的文字。一般来说文字是百分比信息，但是也可以使用setIndicator()设为任意字符。下面是新的API： </p>
<pre>    virtual <a href="http://doc.trolltech.com/3.3/qstring.html"><font color=#004faf>QString</font></a> text() const;
void setTextVisible(bool visible);
bool isTextVisible() const;
</pre>
<p>默认的文字信息是百分比信息。文字信息可以藉由重新实现text()而改变。 </p>
<p>在Qt3 API中，setCenterIndicator()和setIndicatorFollowStyle()是两个影响对齐的函数。他们可以方便的由一个函数实现，setAlignment()： </p>
<pre>    void setAlignment(Qt::Alignment alignment);
</pre>
<p>如果程序员不调用setAlignment()，对齐方式基于当前的风格。对于基于Motif的风格，文字将居中显示；对其他风格，文字将靠在右边。 </p>
<p>这是改进后的<a href="http://doc.trolltech.com/3.3/qprogressbar.html"><font color=#004faf>QProgressBar</font></a> API： </p>
<pre>    class <a href="http://doc.trolltech.com/3.3/qprogressbar.html"><font color=#004faf>QProgressBar</font></a> : public <a href="http://doc.trolltech.com/3.3/qwidget.html"><font color=#004faf>QWidget</font></a>
{
...
public:
void setMinimum(int minimum);
int minimum() const;
void setMaximum(int maximum);
int maximum() const;
void setRange(int minimum, int maximum);
int value() const;
virtual <a href="http://doc.trolltech.com/3.3/qstring.html"><font color=#004faf>QString</font></a> text() const;
void setTextVisible(bool visible);
bool isTextVisible() const;
Qt::Alignment alignment() const;
void setAlignment(Qt::Alignment alignment);
public slots:
void reset();
void setValue(int value);
signals:
void valueChanged(int value);
...
};
</pre>
<a name=HowToGetApisRight>
<h2>如何把API设计好（原文是How to Get APIs Right，我总想成We do APIs right&#8230;&#8230;）</h2>
</a>
<p>API需要质量保证。第一个修订版不可能是正确的；你必须做测试。写些用例：看看那些使用了这些API的代码，并验证代码是否易读。 </p>
<p>其他的技巧包括让别的人分别在有文档和没有文档的情况下，使用这些API；或者为API类写文档（包括类的概述和独立的函数）。 </p>
<p>当你卡住时，写文档也是一种获得好名字的方法：仅仅是尝试把条目（类，函数，枚举值，等等呢个）写下来并且使用你写的第一句话作为灵感。如果你不能找到一个精确的名字，这常常说明这个条目不应该存在。如果所有前面的事情都失败了并且你确认这个概念的存在，发明一个新名字。毕竟，&#8220;widget&#8221;、 &#8220;event&#8221;、&#8220;focus&#8221;和&#8220;buddy&#8221;这些名字就是这么来的。 </p>
<img src ="http://www.cppblog.com/eXile/aggbug/42857.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/eXile/" target="_blank">eXile</a> 2008-02-18 00:50 <a href="http://www.cppblog.com/eXile/archive/2008/02/18/42857.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>测试驱动开发（ＴＤＤ）的顿悟</title><link>http://www.cppblog.com/eXile/archive/2008/01/23/41728.html</link><dc:creator>eXile</dc:creator><author>eXile</author><pubDate>Wed, 23 Jan 2008 09:23:00 GMT</pubDate><guid>http://www.cppblog.com/eXile/archive/2008/01/23/41728.html</guid><wfw:comment>http://www.cppblog.com/eXile/comments/41728.html</wfw:comment><comments>http://www.cppblog.com/eXile/archive/2008/01/23/41728.html#Feedback</comments><slash:comments>11</slash:comments><wfw:commentRss>http://www.cppblog.com/eXile/comments/commentRss/41728.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/eXile/services/trackbacks/41728.html</trackback:ping><description><![CDATA[　　对于测试驱动开发（ＴＤＤ），始终有一些迷惑，比如说，它的测试需要考虑完备性吗，需要考虑覆盖率吗？等等此类。今天从Javaeye中看到一句话，终于明白了。<br>　　&#8220;<span style="COLOR: red">什么是TDD？TDD就是把你的需求用测试给描述出来</span>。&#8221;<br>　　也就是说，ＴＤＤ中的测试和一般意义上的单元测试并不一样，尽管ＴＤＤ中的测试有时也作为单元测试来使用，但它们是两回事。（这里的<font style="COLOR: #000000" color=#ff0000>需求，指的不是客户需求，而是程序员的开发需求）。</font><br>　　使用ＴＤＤ时，首先写的是测试，这时相应代码还没有实现，那么测试什么东西呢？所以说，写测试的过程，同时也是<strong><span style="COLOR: red">设</span><span style="COLOR: red">计接口</span></strong>的过程。这和写单元测试的目的完全是不一样的。<br>　　ＴＤＤ还有一个额外的好处。大多数人都是懒的，不要指望所有的程序员在写完功能代码后，再去编写相应的单元测试。我觉得这个接口的实现没有问题，所以就不用测试。这种想法也很常见。所以一开始就写下测试，可以杜绝后患。 
<img src ="http://www.cppblog.com/eXile/aggbug/41728.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/eXile/" target="_blank">eXile</a> 2008-01-23 17:23 <a href="http://www.cppblog.com/eXile/archive/2008/01/23/41728.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>