如何优化JavaScript脚本的性能

作者:ShiningRay @ Nirvana Studio

随着网络的发展,网速和机器速度的提高,越来越多的网站用到了丰富客户端技术。而现在Ajax则是最为流行的一种方式。JavaScript是一种解释型语言,所以能无法达到和C/Java之类的水平,限制了它能在客户端所做的事情,为了能改进他的性能,我想基于我以前给JavaScript做过的很多测试来谈谈自己的经验,希望能帮助大家改进自己的JavaScript脚本性能。

语言层次方面

循环

循环是很常用的一个控制结构,大部分东西要依靠它来完成,在JavaScript中,我们可以使用for(;;),while(),for(in)三种循环,事实上,这三种循环中for(in)的效率极差,因为他需要查询散列键,只要可以就应该尽量少用。for(;;)while循环的性能应该说基本(平时使用时)等价。

而事实上,如何使用这两个循环,则有很大讲究。我在测试中有些很有意思的情况,见附录。最后得出的结论是:

  • 如果是循环变量递增或递减,不要单独对循环变量赋值,应该在它最后一次读取的时候使用嵌套的++--操作符。

  • 如果要与数组的长度作比较,应该事先把数组的length属性放入一个局部变量中,减少查询次数。

举例,假设arr是一个数组,最佳的遍历元素方式为:

或者,如果无所谓顺序的话:

局部变量和全局变量

局部变量的速度要比全局变量的访问速度更快,因为全局变量其实是全局对象的成员,而局部变量是放在函数的栈当中的。

不使用Eval

使用eval相当于在运行时再次调用解释引擎对内容进行运行,需要消耗大量时间。这时候使用JavaScript所支持的闭包可以实现函数模版(关于闭包的内容请参考函数式编程的有关内容)

减少对象查找

因为JavaScript的解释性,所以a.b.c.d.e,需要进行至少4次查询操作,先检查a再检查a中的b,再检查b中的c,如此往下。所以如果这样的表达式重复出现,只要可能,应该尽量少出现这样的表达式,可以利用局部变量,把它放入一个临时的地方进行查询。

这一点可以和循环结合起来,因为我们常常要根据字符串、数组的长度进行循环,而通常这个长度是不变的,比如每次查询a.length,就要额外进行一个操作,而预先把var
len=a.length
,则就少了一次查询。

字符串连接

如果是追加字符串,最好使用s+=anotherStr操作,而不是要使用s=s+anotherStr

如果要连接多个字符串,应该少使用+=,如

应该写成

而如果是收集字符串,比如多次对同一个字符串进行+=操作的话,最好使用一个缓存。怎么用呢?使用JavaScript数组来收集,最后使用join方法连接起来,如下

类型转换

类型转换是大家常犯的错误,因为JavaScript是动态类型语言,你不能指定变量的类型。

1.
把数字转换成字符串,应用"" + 1,虽然看起来比较丑一点,但事实上这个效率是最高的,性能上来说:

("" + ) > String() > .toString() > new String()

这条其实和下面的“直接量”有点类似,尽量使用编译时就能使用的内部操作要比运行时使用的用户操作要快。

String()属于内部函数,所以速度很快,而.toString()要查询原型中的函数,所以速度逊色一些,new String()用于返回一个精确的副本。

2.
浮点数转换成整型,这个更容易出错,很多人喜欢使用parseInt(),其实parseInt()是用于将字符串转换成数字,而不是浮点数和整型之间的转换,我们应该使用Math.floor()或者Math.round()

另外,和第二节的对象查找中的问题不一样,Math是内部对象,所以Math.floor()其实并没有多少查询方法和调用的时间,速度是最快的。

3.
对于自定义的对象,如果定义了toString()方法来进行类型转换的话,推荐显式调用toString(),因为内部的操作在尝试所有可能性之后,会尝试对象的toString()方法尝试能否转化为String,所以直接调用这个方法效率会更高

使用直接量

其实这个影响倒比较小,可以忽略。什么叫使用直接量,比如,JavaScript支持使用[param,param,param,...]来直接表达一个数组,以往我们都使用new Array(param,param,...),使用前者是引擎直接解释的,后者要调用一个Array内部构造器,所以要略微快一点点。

同样,var foo = {}的方式也比var foo = new Object();快,var reg = /../;要比var reg=new RegExp()快。

字符串遍历操作

对字符串进行循环操作,譬如替换、查找,应使用正则表达式,因为本身JavaScript的循环速度就比较慢,而正则表达式的操作是用C写成的语言的API,性能很好。

高级对象

自定义高级对象和DateRegExp对象在构造时都会消耗大量时间。如果可以复用,应采用缓存的方式。

DOM相关

插入HTML

很多人喜欢在JavaScript中使用document.write来给页面生成内容。事实上这样的效率较低,如果需要直接插入HTML,可以找一个容器元素,比如指定一个div或者span,并设置他们的innerHTML来将自己的HTML代码插入到页面中。

对象查询

使用[""]查询要比.items()更快,这和前面的减少对象查找的思路是一样的,调用.items()增加了一次查询和函数的调用。

创建DOM节点

通常我们可能会使用字符串直接写HTML来创建节点,其实这样做

  1. 无法保证代码的有效性

  2. 字符串操作效率低

所以应该是用document.createElement()方法,而如果文档中存在现成的样板节点,应该是用cloneNode()方法,因为使用createElement()方法之后,你需要设置多次元素的属性,使用cloneNode()则可以减少属性的设置次数——同样如果需要创建很多元素,应该先准备一个样板节点。

定时器

如果针对的是不断运行的代码,不应该使用setTimeout,而应该是用setIntervalsetTimeout每次要重新设置一个定时器。

其他

脚本引擎

据我测试Microsoft的JScript的效率较Mozilla的Spidermonkey要差很多,无论是执行速度还是内存管理上,因为JScript现在基本也不更新了。但SpiderMonkey不能使用ActiveXObject

文件优化

文件优化也是一个很有效的手段,删除所有的空格和注释,把代码放入一行内,可以加快下载的速度,注意,是下载的速度而不是解析的速度,如果是本地,注释和空格并不会影响解释和执行速度。

总结

本文总结了我在JavaScript编程中所找到的提高JavaScript运行性能的一些方法,其实这些经验都基于几条原则:

  1. 直接拿手头现成的东西比较快,如局部变量比全局变量快,直接量比运行时构造对象快等等。

  2. 尽可能少地减少执行次数,比如先缓存需要多次查询的。

  3. 尽可能使用语言内置的功能,比如串链接。

  4. 尽可能使用系统提供的API,因为这些API是编译好的二进制代码,执行效率很高

同时,一些基本的算法上的优化,同样可以用在JavaScript中,比如运算结构的调整,这里就不再赘述了。但是由于JavaScript是解释型的,一般不会在运行时对字节码进行优化,所以这些优化仍然是很重要的。

当然,其实这里的一些技巧同样使用在其他的一些解释型语言中,大家也可以进行参考。

参考

附录1

由于是以前做过的测试,测试代码已经不全,我补充了一部分如下:

附录2

代码1:

代码2:

代码3:

在firefox下测试这两段代码,结果是代码2优于代码1和3,而代码1一般优于代码3,有时会被代码3超过;而在IE
6.0下,测试压力较大的时候(如测试10000次以上)代码2和3则有时候优于代码1,有时候就会远远落后代码1,而在测试压力较小(如5000次),则代码2>代码3>代码1。

代码4:

代码5:

上面两段代码在Firefox和IE下测试结果都是性能接近的。

代码6:

代码7:

代码8:

代码9:

这四段代码在Firefox下6和8的性能接近,7和9的性能接近,而6,
8 < 7, 9;

最后我们来看一下空循环

代码10:

代码11:

最后的测试出现了神奇的结果,Firefox下代码10所花的时间与代码11所花的大约是24:1。所以它不具备参考价值,于是我没有放在一开始给大家看。

FIT 简介

Fit: Framework for Integrated Test

优秀的软件需要协作和沟通,在开发过程中,客户如何才能知道他们的程序员如何正确处理了事情呢?而程序员又如何知道客户究竟想的是什么呢?测试人员如何才能知道什么是正确的而什么又是错误的?让这些小组能有效和精确地交流应该是团队创建伟大软件的一个目标。

FIT就是一个用于增强交流和协作的工具。FIT创建了一个在客户和程序员之间的反馈循环。FIT让客户和测试人员可以使用诸如Microsoft Office之类的工具来给出程序应当如何表现的例子——而无需成为直接编码的程序员。FIT自动针对实际的程序检测那些例子,这样就在业务世界和软件工程世界之间建立了一个简单而且有效的桥梁。

有了FIT,客户可以通过将他们的主题相关的专业知识和想象引入实际的工作中,给开发过程直接提供更多的指导。客户所获得的更多关于目前产品开发中正在发生的东西的可见性,能让他们随时控制项目避免偏离目标。

==FIT如何工作==

FIT通过读取由类似MicrosoftWord之类的工具生成的HTML文件中的表格来进行工作。每一个表格通过一个程序员写的“装置”来进行解释。装置将通过运行实际的程序来检测表格中的例子。

在这个例子中,团队要建立一个产品用于计算职工的薪水。团队已经一起创建了一个包含一些如何计算小时薪水例子的FIT文档。

表格包含了例子。第一行告诉了FIT如何读取表格。第二行给出了例子的头,剩下的行给出了例子。例如,在第一个表格中的例子说:“如果某人工作了40个工时同时没有休息时,那么就付给$20每小时的薪水,然后他的总薪水是$800。”

(虽然这个例子只使用了一个简单的表格格式来展示计算的结果,其实还有很多不同的表格格式可以使用。)

FIT自动针对软件检验表格中的例子。在我们这个例子中,对前两个测试案例,软件给出了正确的解,所以FIT把表格单元标示为绿色。在最后一个测试案例中,软件给出了错误的解,所以FIT把这个表格单元标记为红色。这个软件说职员的总薪酬是$1,040,而期望的值是$1,360。


为了能让FIT能使用这个表格工作,团队的程序员创建了一个“装置”来告诉FIT如何和他们的软件交流。情况类似下面:

程序员使用了一个ColumnFixture来将表格中的列映射到装置中的变量和方法上。前三列,提供了信息,对应于装置中的变量。最后一列,包含期望的值,对应于装置中的Pay()方法。要计算答案,程序员使用他们程序中的WeeklyTimesheet类。

==总结==

FIT给予了客户和程序员一个关于软件的精确交流的方法。客户所给的具体的例子让程序员能深刻理解将要构建的产品。程序员的对于装置的工作和软件可以让客户给出不同的例子进行试验来获取对于软件如何真正工作更深入的了解。这样通过一起工作,整个团队可以学会更多关于产品的内容并产生更好的结果。

为什么Flickr不进行自动化测试?

原文:为什么Flickr不进行自动化测试? — Carlos Villela @ 8:00 pm

翻译:ShiningRay

Sam Newman 惊讶于(中文版) Flickr没有使用很多自动化测试,因为“作为一个姿态鲜明和成功的应用,我只能假设自动化测试还存在一个更加成熟的途径”。

事实上,当看到的时候我也有点惊讶,不过我想我知道他们为什么不真正十分关心测试的原因,我的反应是他们不仅仅只是一群过时.COM时代的普通牛仔,在一边把代码仍在墙上一边看看堵住了什么——这些人常常被一般的敏捷人(agilist,还和拳击手pugilist押韵?)在会议和论文上批评他们没有测试自己的代码并被告知这样做更好。也许有某些原因可以不去麻烦测试,我在这里要尝试在这里解释他们,当然我明白这篇贴子可能有点过激。首先,让我们提出在一个新的项目阶段开始后,两种不同的场景,其中数字代表某种在给定的期间内的想象的成本单元:

场景 A B
开发 10 10
自动化测试 8 0
维护 2 10

假设其他条件均相同,同时代码是由合格的开发者——当然Flickr的家伙们肯定都是——很好地书写的,这样的表格看起来似乎可信:其中A使用了自动化测试来发现错误,节省了维护的时间,而B单独把时间花费在维护上,产生了同样的结果(至少在这个阶段)。

A中,公司需要预付钱来建立可用的测试套件并运行,这样他们就不用对未来的维护多花费这笔钱。现在飞过来了叉子和火把,并且要置我于死地,但是这是一个糟糕的财政决定,除非写自动测试的成本比进行所有的维护的成本要低,但后者的成本可能会扩散。然而这个成本不低的风险,至少在我的经验里,看起来比大多数公司所愿意接受的要高的多,因此我们从多数——如果不是全部的——敏捷人那里听到关于这个方式的好的结果。

现在再把这小段话引用一下:我是一个自动化测试的爱好者因为我见过维护软件有多烦人 ,在几个月都不停做这事,消耗尽了我最后一点出现在我可怜精神中的愉快感。我则更偏好写全新的代码,即使代码没有在最终的产品环境中用到,就像测试代码一样,而不是经历另一个令人沮丧的一天不停地在调试器上一步步移动。 我确定Sam和大多数正在阅读本文的人都是同一条船上的。

B,在我的理论中,就是Flickr所正在做的:较少的预期投入和随着时间流逝的更多的维护成本。
作为一个开始阶段(嗯,不管怎样,在他们被Yahoo收购之前),这很有意义:整个启动阶段就是让你可以去做更有风险的事情,同时某种程度上他们臆测自动地测试任何东西除了小部分重要的(冒烟测试?)并不如立刻把代码颁布出门,且快速,并强制听取用户的反馈并作出反应来得更加重要。这也许要求对对细节和承诺的关注保持一种疯狂的关注程度,这是很稀罕的,不过我要补充一点,这却是我所总结他们成功的原因的一个重要的部分。

正如Joel Spolsky所写的,大概三年前,针对一个类似的情况:

你需要某种经济模型来决定如何花费你有限的资源。你不可能通过说 “压力测试是一个没脑子的东西”或者“服务器也许还可以撑一下。”之类的话来作出可靠的理智的决定。这些都是感情用事的脑子的大便,而非分析。同时在长期的运行中,我们科学家将会获胜。

所以,我不确定谁是正确的,这里:我知道我所参与的大部分项目,在没有经过自动化测试和大量的测试覆盖率保持工作的情况下,本应该完全失败的(某些事实上就是)。但是再说,我从来没有进行过任何像Flickr这样的起始工作。

Web 2.0需要测试

原文: Web 2.0 needs testing 作者:Sam Newman

翻译:ShiningRay @ Nirvana Studio

我带着很多怀疑,耐着性子看完了Cal Henderson的高度富有娱乐性的(而且推荐大家观看)
构建Flickr作坊》,
从中听说自动化测试在他们的优先级列表中并不是特别地高
(有趣的旁白——Cal还认为面向对象编程是“神经错乱的”,所有的“健全”的代码则在“一超大函数”编程和OO之间的空间中存在)。
“测试Web应用”Cal说,“很困难”。其实这是一个普遍的误解。像任何其他应用的类型一样,只要你针对测试性设计了结构,测试是很容易的。然而,同时如果你采用了针对Web应用的测试而制作的测试工具(FITFITnesse, 以及Selenium都是很好的例子),那么即使那些没有带着测试的概念写的应用都没有借口说不利用他们进行开发。

但是现在放在我们面前的是Flickr这样一个姿态鲜明成功的应用,我只能假设自动化测试还存在一个更加成熟的途径。这种对自动化测试的放任自由的方式是不是很普遍呢?

让我们看看这另一条道路。如果你没有自动化测试,你要么手工进行测试(这意味着更长的发布时间)要么干脆不测试。如果你要着眼于交付有质量的产品,这也许意味着你最终还是要做很多手工测试,而这些测试本来可以自动化地更快完成的,甚至只要你想,可以在
在每一次的代码提交中完成

Web的架构产生了一个及早的快速发布、同时有一个经常发布的过程这样的想法。有了Web,你所有要关心的最终目标环境仅仅是你的服务器和客户端的浏览器。推出一个发行版真的可以像Flickr使用的“一点部署”(One-click Deployment)。有了一个成熟的测试框架,如果有必要的话,还可以办到一天之内多次直接给你的用户发布。

测试也许看起来不怎么好玩——但是一旦你明白了它意味着你可以更频繁地对你的用户发布更酷的功能,同时你可以在修复Bug上花费更少的时间,那么我们都可以去更加喜欢书写测试了。

Smalltalk不好吗?

原文:http://www.cincomsmalltalk.com/blog/blogView?showComments=true&entry=3295067130
by James Robertson
翻译:ShiningRay

我收到了一封邮件,看到了一个语言趋势的帖子的链接——这个帖子说大约在1995年,Smalltalk有一个很好的及正派的用途,但是在2005却被大家扔进了墙角。他问到:

那么为什么会导致这种情况呢?这个更大的故事的背后:对于Smalltalk的否定。我很感兴趣的是,为什么Smalltalk会从他的顶点——1995年OO语言的首选落到到它现在作为一个强大却没人用的平台的地步。这是否也是Java的未来呢?如果我能更好的理解Smalltalk的衰落(更不用提Objective C了),我可以更好地预期Java和其他语言的未来。

好吧,有两件事情影响了Smalltalk的使用——ParcPlace-Digitalk(也就是后来的ObjectShare)的行为和IBM的行为。先让我说一下PPD/OBJS。ParcPlace在1995年是一个快速成长的公司,但那在与Digitalk合并之前。之后这个公司就逐渐显现出一些组织上的劳损——管理小组充其量只是次好的(他们所做的与Digitalk合并的决定就很好的说明了这个问题)。

那次合并消耗了接下去的18个月和大量资金。公司不仅仅花了18个月尝试将VisualWorks和VSE合并,他还:

  • 烧掉了很多钱却带来了几个没有效果的新东西
  • 在尝试代码合并的过程中没有发布任何VW或者是VSE的新版本
  • 由于没有去管理合并本身的事宜,结果导致了在工程和咨询小组之间致命的冲突。

不用说了,这种事情根本不能给客户群带来任何信息。这已经够坏了——然后财政问题的谣言就传开了,然后再1997年的动态年管理层发布了VSE的EOL(不包括任何迁移策略)的声明,VW/VSE的代码合并终止,以及一个新的Java策略。然后VSE的客户群就乱套了,已经对不存在的新发布很紧张,质问在新的Java的关注下,到底这个产品还能获得多少关注。

一切就这样进行直到新的管理团队进入并把公司名称更改为了ObjectShare(在原来管理层的疯狂的收购行为中一个被培养出来的公司)。事情一团糟,Java产品砰一下砸了出来,VW也爬了出来。新的管理层尝试扶持股票价格但是未能成功,情况又继续下滑(这一次又没有对顾客和前景带来任何信心)。最后,Cincom在1999年收购了Smalltalk业务(从此事情在我们这边开始有了转机)。

。下面,你要看一下Java的介绍和IBM的举动。在90年代中期的时候,IBM有一个很成功的Smalltalk产品,而且他们所有的开发环境都是基于它的——从VisualAge Smalltalk产生了一个C++、Cobol、同时最后,一个Java工具集。IBM的这些东西做的都很好,并且已经设法(由于PPD刚刚抛弃了他们)。然而,Java得到了很多绯闻,而且IBM对于构建VA架构基础花了很多他们自己的钱。提醒一下,这部分是我的推测,不过我认为发生了这些事情:

  • IBM发现Java在快速流行起来。
  • 他们认识到直接骑在Sun的Java上要比维护他们自己的一套基于VAST的开发工具要便宜得多

后一个观点最后因为Sun免费发布Java而成真。同时这两个举动都帮助了Java惊人地增长,我一点也不确定他们是否帮助了Sun一点点。这是另外一个故事了——在Smalltalk的世界中,IBM逐渐降低了他们对Smalltalk的投资(最后以他们把这个技术推给Instantiations而告终,今年),同时开始快速书写他们的Java故事。这也没有给Smalltalk用户或者是对Smalltalk的期望带来任何信息——特别是当IBM的销售人员们开始干劲十足地推进他们的Java生产线和驳倒Smalltalk的时候。

总之——ParcPlace(以及他后来的公司,PPD/OBJS)自己把自己玩完了,破坏了它原有的客户群,并让潜在的用户对Smalltalk的信息也降低了。同时,IBM开始从Smalltalk转移至Java上——也产生了同样的效果。在90年代中期其他的Smalltalk提供商还过于微小无法影响这种不断成长的观点病毒——Smalltalk正在消亡。

而现在不同了——从1999末开始,Cincom勤奋地研究Smalltalk,我们也拥有一个快速增长的、并且盈利的业务。同时还有很多其他的Smalltalk方言,包括现在要称为VA Smalltalk的。Smalltalk在当前并不是主流,但是它还是被很多期望一个有生产力的候选工具的公司所使用。

简介延续“Continuation”

作者: Denys Duchier

翻译: Shining Ray @ Nirvana Studio

写本文旨在回应comp.lang.python新闻组中关于延续的讨论。


对于call/cc(call with current continuation)的情结和关于他的操作解释粗糙的细节内容,至今一直掩盖了延续的简洁和优雅。在本文中,我想用两个方式来纠正这个问题:

  • 首先用一个简单且直观的方式展示延续的概念。
  • 第二通过提供_可运行的_Python代码,来描述如何使用延续而不用call/cc来实现搜索引擎。

我将展示:

  1. 一个针对命题公式确定性检查工具。
  2. 一个基本的带回溯和裁剪的prolog引擎。

我希望这能帮助那些对于这个论题有些迷惑的人搞清这个问题。我还想指出上面讲到的两个程序是我在今天晚上花了几个小时写的,他们应该可以给你关于下面将要讲述的技术的能力的一些衡量尺度。


1. 简化延续

习惯上一般来说,一个函数返回一个值是这样的:

这隐含了它的值返回的地方。而延续的想法是通过添加一个延续参数来明确要返回的地方。函数不“返回”值,而是通过把值作为一个参数传递给延续“继续”处理值。在基于延续的程序中,以上函数foo变成了:

从这个角度看,函数从不使用返回“return”。取代的是它“继续”。正因为这个原因,延续有时候会被描述为“带参数的goto”。

上面描述的想法是。更精确地说,它是CPS(Continuation Passing Style,延续传送风格)的一个初步代码转换。基本的想法是给每个函数添加一个额外的“延续”参数,并进一步转化函数体以便不用返回他的值,而是把值传送给这个额外的延续参数。

在foo函数的例子中已经大体描述了这个概念。然而,更确切的,要注意CPS转换同时展开了所有的非lambda算子的嵌套表达式(换句话说,就是他显式地连接了所有子表达式的计算)。让我们看一个例子:

在延续传送的观点中,甚至像*+之类的基本操作符都要带一个额外的延续参数。我们通过以下定义进行模拟:

现在,CPS可以把上面的baz函数转换成:

换句话说,2*x的计算现在使用了一个延续来接收结果v并使用它来计算 v+y并最终把这个结果传给总的延续c

当在这个上下文中理解了call/cc之后,它就不再神秘了。它只是一个特殊的手段,能让我们的双手去接触那个不可见的由CPS引入的额外的延续参数,并让我们像程序中其他函数值那样使用它。思考call/cc(f),其中f需要接受当前延续作为参数。其实说到底,call/cc(f)可以通过CPS转换成call_cc(f,c),而c就是CPS所引进的延续参数同时call_cc可以按照以下方式定义:

即,f的普通参数和由CPS引入的额外的延续参数都是当前的延续c

还有一些细节,不过以上部分是基础。

CPS转换是很多针对函数式语言的编译器的基础。它的缺点是它引入了很多lambda算子(即闭包),同时,必要的是,编译器要尽可能多得把他们优化出去。在这方面,Scheme的Rabbit编译器的Steele,T的Orbit编译器的Kelsey etal. 和SML/NJ编译器的Apple有着很多的研究。有一个优点是,如果lambda表达式是你唯一的控制结构而且你把他们优化到了极限,那么你同时也优化了所有的控制结构。

然而应该注意的是,也正如很多人已经注意到的,由于编译器的工作恰是常常要把CPS引入的东西删除,所以有些人对将CPS转换作为编译过程基础的价值提出了质疑。

2. 将延续作为一个普通的编程技术

你可能已经注意到一些人对于不可思议难懂的延续的应用极其狂热。但事实上还是有着很多“非不可思议”的延续的应用,他们也不要求call/cc的存在。你可以在Python中书写延续传递程序,或者在任何支持闭包的部分形式和有自动垃圾收集的语言中写。

我最了解的应用程序是关注于“搜索”。它和正在进行的关于迭代子的讨论有很大的关系。很多年以前,我从我以前的导师Drew McDermott那里学习了这个技术,我会在下面讲述它。然而,我得指出,和Tim的特性记述相反,生成器(generator,在Drew的观点上)不需要以“类栈方式”进行表现;尽管很少有提出不这样用的 :-)

这个概念是通过传递两个延续来进行搜索:

  1. 一个成功延续,用于进一步进行搜索
  2. 一个失败延续,用于回溯到第一个更早的选择点

用Python来表达它,常常是以下面的形式出现:

“info”是在搜索中传送的一些信息。“yes”是成功延续,“no”是失败延续。“yes”以当前的“info”状态和当前的失败延续作为参数。而“no”则没有参数。

如果self.check(info)为真,则Foo对象满足了搜索条件。

一个有两个Foo“one”和“two”属性的类Baz。定义一个Baz对象来满足搜索条件,只要“one”属性满足它或者“two”属性满足(换句话说就是一个Baz对象是一种析取式)。我们通过调用“one”属性的搜索方法来表达,同时还给他传递一个尝试“two”属性的失败延续。

很明显,在上面的内容中,由于Python缺乏对于闭包的真正支持,使得要按照函数式语言中简洁和优雅的写法书写有些困难。

3. 检查命题公式的确定新

命题逻辑的公式如下:

它表示

p,q,r 是可以被赋以真值得命题变量。你可以证明不管你给p,q,r赋什么值,上面这个公式总是为真的。看一个更加简单的公式会更清楚,如(p | !p)也就是“p或非p”。这样一个公式是所谓的“确定的”:它总是为真,无论你如何解释他的变量。

下面的Python程序实现了针对命题公式的确定性检验工具,它使用了前面描述的延续传送风格。这个程序仅仅只是要描述一个例子而已。对于这种人物有更加有效的方法。然而,我相信它可以很好地表达关于使用延续传送实现搜索的一个通用的想法。

两个程序(确定性检查工具和前面提到的prolog引擎)都可以在下面的URL中获得:

Io语言

“动态语言”现在是一个很时髦的术语,常用来表示那些编程语言十分地具有弹性而且很合适“敏捷”开发环境。在这种趋势背后有一些很有趣和很强大的语言,Io就是其中一个十分重要的语言。

Io最早发布在2002年,它混合了Lua、Lisp、Smalltalk和其他一些语言的某些方面的一个小语言。而起初的大想法是要把Io作为一个完整的可视化编程语言的基础,就像Self一样。

概览

Io属于“解释型”语言家族(像Perl、Python等等),这种语言的代码并不是通过编译而至直接解释并运行的。和很多“解释型”语言一样,Io也是高度动态的:一个程序可以迅速地生成并解释新的代码。同时,Io通过采用了Lisp的“代码即数据”的模型让这个特性向前迈进了一步,它可以让任何Io程序访问和处理它自身在内存中的对象模型。毫无疑问,这也让Io成为一个自省的语言。

并发编程在很多应用中都是一个新兴的被关注的方面,尤其是大多数用户界面和Web应用。Io从Act1中获得了灵感并使用了“参与者”(Actor)的概念。参与者介于线程和延续之间,它可以让你更容易编写你的并发应用,而原有的基于线程或者是进程编程的负担则不复存在。

简介清晰在Io的语法和他的对象模型中是显而易见的。语法可读性很好,而且看上去很好地结合了Lisp和Smalltalk。Io的对象模型是基于原型的。一个基于原型的对象模型不使用类来表示泛化和特化,取代的是创建特定的对象来用作“模型”或是“原型”并通过他们来创建新的对象。在这种编程范型中,对象是从原型中克隆出来的而不是从一个类实例化得来的。

对象模型是基于消息的,这表示方法调用甚至是变量访问都是通过发送动态的消息来完成的,而不是“硬布线”的(像C++和Java那样)。同时,Io对象响应消息的方式可以在运行时进行改变,这也是一个很典型的“动态”特性。

Io的足迹很小可以很方便地嵌入C或者是C++应用中。这方面它最有趣的特点之一是他的Objective-C的桥接器,这在集成上达到了一个令人惊奇的程度,在Io中使用你的Objective-C对象几乎不要写任何粘合代码。

Io分析

长处

Io的主要的优点来自他简洁的设计:它是一个可以很快学会的语言因为他有一个简单而且一致的语法、语义和API。由于Io的足迹小,所以它十分合适嵌入式编程。Io的另一个长处是它的速度。它能在性能上胜过很多其他解释型语言,这让他成为密集工作的一个理想选择。

Io的另一个长处——从一个嵌入的角度来看是很有意思的——是你可以重新定义几乎任何运行机制。Io语法中的任何东西都已经转化成了消息,而消息是可以在运行时重新定义的。从这方面来看,实际上你就可以改变里面任何东西来满足你的需求。

同时,Io对于并发编程的解决方案对于Web应用和GUI脚本编程也极具吸引力。

弱点

尽管Io有很多有趣和先进的特性,然而它还是十分年轻。到目前为止它还没有什么值得称赞的开发工具诸如文档生成器或者是代码检查器等等,而且在解释其上面仍然有一些问题。尽管如此,随着Io社群的不断壮大,更多的代码被制作共享,这个“青少年问题”会最终消失。

范例程序

参考

从Trails和Firebird开始

作者:Chris
翻译:ShiningRay @ Nirvana Studio

0. 前言

在Trails的首页上有一段Trails的指导视频,它比本文说得更为详细。你可以看看它,另外根据Firebird设置一节中的内容,设置一下和Firebird相关的东西。

1. 什么是Trails?

Trails是一个领域驱动开发框架,它使用了Hibernate、Spring和Tapestry。其中,Hibernate 被用作数据访问层,Tapestry用来给用户显示数据。而Spring 则是把Hibernate和Tapestry连接在一起。

Trails自带了几乎所以必需的jar文件——你只需要安装一下Firebird的数据库驱动,它可以在 http://firebirdsql.sf.net上下载。

1.1 Hibernate

Hibernate是一个O/RM工具。O/RM的意思是:对象关系映射。O/RM可以让你把 java 对象映射到后台数据库中。Hibernate使用了XDoclet来指明映射信息所需的信息,以便在数据库中存储复杂的对象。而XDoclet使用了javadoc注释来告诉Hibernate如何映射对象。例如:

[code lang=”java”]/**
* @hibernate.class table=”PERSON”
*/

public class Person {
}
[/code]

这段代码会告诉Hibernate要将一个指定的对象(在这里是Person)映射到PERSON表。

Hibernate 的主页是:www.hibernate.org.

1.2 Tapestry

Tapestry 是(简而言之)一个Web框架。更确切地说,Taspetry是一个基于组件的Web框架,它将表示和逻辑清晰地分离开来了。

在Tapestry中,一个页面和一个.html文件相关,这个文件负责这个页面的外观,和一个Java类,它负责对.html文件提供数据,还有一个.
page或者是一个.jwc文件(这由你是要作为一个真正的页面还是一个单独的组件),这两个负责把前两个层次连接起来。

组件是通过ognl语言来访问的,形式如:

jwcid 指明了使用的组件——这里是一个PageLink,它是一个HTML表现中的<a></a>标签的一个组件。page属性指明了你要链接的页面的名称。

Tapestry 的主页在jakarta.apache.org/tapestry

2. 安装Trails。

首先从trails.dev.java.net下载Trails.

当前的版本是0.5.1。先把下载的文件解压缩(在https://trails.dev.java.net上有一个很棒的演示,教你如何利用Trails来写应用程序)。

解压缩trails,并进入新建的文件夹。更改build.properties文件以匹配你的tomcat路径,
然后做以下这些事情:

通过运行下面的指令来创建trail.jar(我在我的机器上必须这样做)

3. 写一个新的应用程序

3.1 创建一个新的trail应用:

这是在问你,把工程的根目录放在哪里(在MacOS X上我输入的是/Users/chris/Desktop/projects——Windows用户可能要输入诸如c:\path\to\new\project的路径)

输入工程名称

然后,一个目录包括所有必须的jar文件就被创建好了。

3.2 启动

3.3 创建一个新的 “Java Project”

  • 输入工程名称
  • 选择 “Create project at external location”(在外部位置创建工程) 并指向新创建的文件夹。

  • 点击 “next”
  • 点击 “add folders”
  • 选择 “src”,然后点击OK

  • 点击 “finish”

新创建的工程就会被导入到Eclipse中。

4. 设置

4.1 改变tomcat的主目录

  • 打开Eclipse中的open build.properties 文件并将tomcat.home改称你的tomcat的目录。

4.2 更改数据库驱动:

  • 打开文件 context/WEB-INF/hibernate.properties 并把其中的设置改为::

4.3 在主机上创建一个新数据库(这里是127.0.0.1)

并且在 FIREBIRD_HOME/aliases.conf 中添加一个别名,这样写:

在生产系统中,你还要为以后要连接的用户更改用户名和密码 :-)

4.4 安装Firebird数据库驱动

firebird.sf.net下载Firebird的JCA-JDBC驱动,解压缩并把jar文件安装到CATALINE_HOME/common/lib下(详细内容参见 JayBird FAQ)。

5. 开始编码

5.1 创建一个领域对象(Domain Object)

领域对象可以简单地说成是POJO(Plain Old Java Objects),它是Hibernate用来在数据库中存放数据的。

  • 在Eclipse中选择src文件夹并点击 File -> New -> Class(文件->新建->类)
  • 输入一个包名(de.test.data)
  • 在Name输入框中输入类名:Person
  • 点击 “finish”

成功新建并打开了一个类。

5.2 告诉Hibernate要使用的表格:

把表格名称作为XDoclet的hibernate.classabove标签添加到类的说明中。XDoclet任务以 @ 符号开始,后面跟着一个名空间(在这里是hibernate)和一个动作,指明要做什么(更多信息参见hibernate.sf.net)。

XDoclet标签 @hibernate.class table=”PERSON” 将告诉Hibernate把这个对象中的数据存储在一个叫做PERSON的表中。

表格的名称无需和类名相匹配——你也可以指定一个MY_PERSON作为名称 :-)。

5.3 创建属性:

  • 先输入以下内容:

  • 在“Outline”(大纲)中选择PersonID并右键点击它。
  • 选择Source -> Generate Getters and Setters
  • 两个都选中并点击OK

现在已经有了getter和setter例程,而且已经添加了注释。然后我们添加下面的注释:

这将告诉Hibernate使用表格的PERSON_ID列来存储表格的主键,同时通过使用一个生成器来自动新建这个字段的值。
键将由SQ_PERSON_ID生成器来生成。

在大纲中选择name并且右键点击它(选择 source
-> generate getters and setters,选择Name和SurName然后点OK :-) )

这次,我们不再需要主键——我们仅仅需要一个字段来存放姓名。所以我们继续并添加以下javadoc注释:

[code lang=”java”] /**
* @hibernate.property column=”LAST_NAME” not-null=”true”
*/
public String getName() {
return Name;
}
[/code]

以及surname属性

由名称属性而来的getter上的列是必须出现的,因为我们要覆盖默认的行为。Hibernate会假设一个叫“NAME”的字段——但这在Firebird中是一个关键字,所以我们必须另外选择一个名称。

记住:Hibernate标记必须总是设置在getter上——而不是setter。

如果需要,我们可以通过在Hibernate的XDoclet标记中添加not-null=”true”属性来设置一个null值——默认值为false。

5.4 创建查询表

现在提示有一个错误——是LSalutation。

这时把你的鼠标移到LSaltutation上并且按下Ctrl+1(在Apple Macintosh上你要按Command + 1)。然后会出现以下对话框: 

输入要创建的文件的包名,点击“finish”,然后做以下几步动作:

  • 给他一个ID
  • 个它一个名称
  • 生成getter和setter
  • 并且指明给表格设置的类名,以及ID和属性字段

现在,又是另一个对象可以被Hibernate映射了。对于Hibernate,这已经可以用了,但对于Trails,还少个东西:

  • 一个 equals() 和一个 toString() 方法

toString() 方法是用来在列表视图中能正确显示属性,而equals方法是用来标示对象的。所以只要添加以下代码到LSalutation类中:

返回到Person.java中并且为Saltuation属性创建getter和setter。

由于我们要给salutations使用另一个表中的数据,所以这里我们要用另一个属性。所以这里我们要用一个多对一的关系。也就是,我们要把javadoc注释改成:

现在Hibernate已经了解如何引用salutation类了。

这就是我们要写的全部代码了。 :-)

6. 构建应用程序:

打开build.xml文件,右件点击war目标并选择 ant -> build

这将调用ant来构建.war文件(Trails的指南说,使用deploy目标可以部署程序——这不能在我的机器上运行)。然后我把project.war文件复制到TOMCAT_HOME/webapps中去。

7. 访问应用程序:

打开浏览器并输入127.0.0.1:8080/project_name

现在,享受一下浏览和输入数据的乐趣吧 :-)

无任何担保——Chris

SmaCC 指南

这是一个用于演示一些SmaCC(Smalltalk编译器的编译器)的简要指南。在这个例子中,我们会逐步开发一个简易的计算器。

如果你已经做过这种东西,你可以先 载入代码 。你载入了代码之后,你需要打开SmaCC解释器生成器。在VisualWorks 和 VisualAge中,它在Tools菜单下。Dolphin的在一个额外的工具目录中。它会打开一个类似下面的窗口:

SmaCC Window

我们第一个计算器相对比较简单。它只要能读取两个数字并把它们相加。开始之前,我们首先要告诉扫描程序如何辨认一个数字。数字由一个或多个数字打头,后面可能还有一个小数点加上0或者更多的数字。扫描程序对这个标记的定义是:

把这行代码输入界面上的scanner标签页中。让我们逐个看每一个部分:

<number>

指出记号的名字。在<>中的名称必须是合法的Smalltalk变量名。

:

分隔记号名称和记号定义。

[0-9]

匹配任何一个在’0’到’9’(一个数字)范围中的字符。

+

匹配前面的表达式一次或多次。在这种情况下,我们要匹配一个或多个数字。

( … )

标示子表达式组。

\.

匹配 ‘.’ 字符(. 在正则表达式中有特殊的含义,使用 \ 来转义)。

*

匹配前一个表达式零次或多次。

?

匹配前面的表达式零次或一次。(也就是,前面表达式是可选的)。

;

终止一个记号说明。

我们不想去关心我们语言中的空白符,所以我们需要定义什么是空白符并且忽略它,输入下面的记号说明:

\s 会匹配任何空白字符(空格、制表符、换行、回车等等)。然后我们怎么告诉扫描程序去忽略它呢?
如果你看一下SmaCCScanner类,你会发现一个叫做’whitespace’的方法。如果一个扫描程序有一个方法的名称和某个标记一样,那么一旦扫描程序匹配了这类标记就会调用这个方法。正如你所见,whitespace方法会吃掉空白符。同样还有一个’comment’方法会作类似的处理。

说到我们的语法,现在让我们来定义它吧。在Parser表格中输入以下语法说明:

这基本上指出了一个表达式可以是一个数字或者是另一个表达式加上一个数字。

我们现在应该可以编译一个分析器了。切换到Compile标签页。你要输入扫描器和分析器的类的名称。这里我们相应使用CalculatorScanner 和CalculatorParser。当类名输完之后,我们就准备编译分析器了。点击 ‘Compile LARLR(1)’
按钮(你应该总是点这个按钮除非你知道你要做什么。一般来说,他会生成比另一个选项更小的分析器)。这时就会生成新的CalculatorScanner和CalculatorParser的Smalltalk类同时会编译这两个类中的一些方法。所有的SmaCC编译出来的方法会按照”generated-*”的格式。你不可以更改这些方法因为每次你重新编译他们都会被覆盖。

不管SmaCC何时创建新类,这些类都会被放到默认的应用程序/包中。如果你使用的是VisualAge,你要确保默认应用程序是开放的版本而且SmaCCRuntion应用程序已经安装(prereq)。

如果你已经生成了扫描器和分析器类,你可以通过类名旁边的”…”按钮来载入他们的定义。如果在出现的对话框中你回答”Yes”,那么在Scanner/Parser标签页中的文本就会被替换为上一次编译过的定义(假设”Generate definition comments”在上一次编译中被选中了)。

现在我们要测试我们的分析器了。进入“test”面板,输入“ 3 + 4”(不要加双引号),并且点击“parse”按钮;你会看到分析器正确分析了它。如果你点击“Parse and Inspect”你会看到一个检视器(inspector),里面有一个包含了被解析的记号的顺序集合(OrderedCollection)。这是因为我们没有指明当分析器在解析的时候要怎么处理记号。你也可以是如一个不正确的内容。例如尝试解析“3 + + 4”或者“3 + a”。应该会出现一个错误信息。

现在我们要定义当我们分析我们的表达式的时候要产生的动作。当前的情况是,我们的分析器仅仅验证表达式是一些相加的数字。一般来说你要创建一些结果来表示你已经解析了什么内容(比如,一个棵分析树)。然而,在这个情况下,我们不关心结构,我们只关心结果(表达式的值)。在我们的例子中,你需要把语法定义改成如下:

括号中的文本是Smalltalk代码,当应用规则的时候,就会执行这个代码。有一个数字的字符串会被替换成相应的解析节点。在第一个Expression的规则中,’1’会被替换成匹配Expression的ParseNode同时’3’会被替换成匹配Number的ParseNode。在规则中的第二个东西是’+’记号。因为我们已经知道它是什么了,所以我们对他不感兴趣。编译新的分析器。现在当你从Test面板中执行’Parse and Inspect’时,你应该看到这个结果:7。

前面的代码有一个问题是如果你需要更改一个规则,你可能也要跟着修改规则内的代码。例如,假设你你在规则的开头添加了一个新的记号,那么你就要更改所有在Smalltalk代码中的引用了。我们可以通过使用命名表达式来减少这种问题。在规则的每个部分后面,我们可以指明它的名称。名称是通过单引号来标明的,它同样必须是一个合法的Smalltalk变量名。象下面这个:

它和前面解析的语言的结果是一样的,但它同时让你更容易维护的你分析器。让我们现在扩展我们的语言并加入减法功能。这里是新的语法:

你编译了这个代码之后,’3 + 4 – 2’就会返回’5’了。下面,再让我们加入乘法和减法:

这时我们遇到一个问题.如果你计算” 2 + 3 * 4“,最后的结果将是 20。这个问题是因为在标准的数学中,乘法比加法有更高的优先级。我们的语法是严格按照从左到右的方式运算的。这个问题一般的解决方法是定义加法的非终结符来强制计算的顺序。这个解决方法的语法类似:

这时候如果你编译这个语法,你会看到” 2 + 3 * 4 “的计算结果是14,正如我们所期望的那样。现在,正如你可以想象的,当优先级规则的数量增加时,语法也越来越复杂(例如,C语言)。我们可以使用歧义语法和优先级规则来简化这种情况。这里有一段使用优先级来限制计算顺序的一段语法:

注意我们更改了语法所以操作符两边都是Expression。我们在语法顶部添加的两行表示“+”和“-”是从左至右运算的而且优先级相同,同时他们的优先级比“*”和“/”低。类似的,第二行表示“*”和“/”有同样的优先级。这个形式的语法通常更加直观,特别是当有很多优先级要处理的时候。我们再来一个例子,现在加入指数运算和括号:

当你编译了这个语法之后,你就可以正确计算” 3 + 4 * 5 ^ 2 ^ 2“得到2503了。由于这个指数操作是右结合的,所以这个表达式是象这样计算的3 + (4 * (5 ^ (2 ^ 2)))。我们也可以计算带括号的表达式。例如,计算 ” (3 + 4) * (5 – 2) ^ 3 “将得到189。

为什么继承是有害的?

通过把具体的基类转变成接口来改进你的代码

作者:Allen Holub  翻译:ShiningRay @ Nirvana Studio

摘要

大多数优秀的设计师避免出现继承(extends描述的关系),就像躲避瘟疫似的。你的代码80%应该完全以接口的方式来书写,而不是继承具体的基类。其实,Gang of Four 这本关于设计模式的书(以下简称GoF)很大程度上关于如何把类继承转变成接口实现。本文将叙述为什么设计师们会有这种古怪的信条。(2,300 words;2003 年 8月 1日)

译注:本文其实已经有人翻译,当时没有具体了解就开始翻译了,如果另一位译者看到这篇文章,希望不要理解为我抄袭的。

extends关键字是很有害的;也许不仅仅是在Charles Mason的级别上,还坏到了只要可能都应该避免的程度。GoF中详细讨论了把类继承(extends)如何转变成接口实现(implements)。

优秀的设计师的大部分代码都是根据接口写的,而不是根据具体的基类。本文将会讲述为什么设计师们会有这种古怪的癖好,同时也将介绍一些基于接口的编程基础。

接口 VS 类

我曾经参加了一个Java用户小组会议,那次刚好是James Gosling(Java的发明者)作特邀演讲人。在那次难忘的Q&A对话(提问)上,一个人问他:“如果你可以重新将Java搞一遍,你会做哪些修改?”“我会去掉类,”他回答道。在笑声渐渐消失之后,他解释了真正的问题不是类的本质,而是类继承(extends关系)。接口实现(implements关系)却是完美的。只要有可能,你们就应该避免类继承。

弹性的丧失

为什么你应该避免类继承?第一个问题是明确的使用具体类的名称会把你框在特定的实现中,让以后的更改会十分困难。

当代,敏捷开发方法学的核心是设计和开发同步。你在完全详细描述程序之前,就开始编写代码了。这种技术完全违背了传统的理念——设计应该在编程之前完成——但是很多成功的项目已经证实了,用这个方法,你可以比传统流水线作业更快速地开发高质量的代码(同时付出很有效)。然而,在并行开发的核心是,弹性的概念。你必须以这种方式来写你的代码,以便你可以尽可能以无痛的方式加入新发现的需求到现有的代码中。

你只要实现确实需要的特性,而不是实现那些可能需要的特性,但要用一种可以适应变化的方法。如果你没有这种弹性,并行开发明显是不行的。

接口编程正是这个弹性接口的核心。要了解为什么,先让我们看看如果你不使用接口会发生什么。考虑以下代码:

现在假设一个紧急的新需求,需要进行更快速的查找,已经暴露出来了,这样LinkedList就达不到要求了,你就要把它换成HashSet。在现有的代码中,因为你必须同时修改f()还有g()(它用一个LinkedList作为参数),因此更改不是局限在一处的,还有一切传列表给g()的地方。

现在把代码改成这样:

现在我们要把链表改成哈希表就只把new LinkedList()改成new HashSet()。就完成了。不需要更改其他的地方。

另外一个例子,比较一下代码:

以及:

g2()方法现在遍历Collection的派生对象以及你从Map中得到键和值。事实上,你可以写一个不断产生数据的迭代子而不是遍历一个集合。你可以写很多不同的迭代子,比如可以从测试台中或者一个文件中不断给出信息。这就是这里最重要的弹性所在。

耦合

关于类继承的一个更加关键的问题是耦合——程序中不期望的一个部分对另一个部分的依赖。全局变量提供了一个经典的例子来说明为什么强耦合会造成很多问题。例如,如果你更改了全局变量的类型,所有使用这个变量的函数(也就是,对这个变量有耦合)就会受到影响,这样所有这样的代码必须被检查、修改和重新测试。此外,所有使用这个变量的函数也会通过这个变量产生耦合。也就是,一个函数可能会不正确地更改了这个变量从而造成了其他函数的行为,如果变量的值在某些特殊的时间被更改的话。这个问题在多线程的程序中特别突出。

作为一个设计者,你应该力争做到最低的耦合度。当然你不可能完全消除耦合,因为一个类的对象调用另一个对象就是一种松散耦合的形式。你不可能写出一个一点耦合都没有的程序。尽管这样,你可以通过绝对服从OO的原则(最重要的是一个对象的实现细节应该对使用它的对象是隐藏的)来相当可观地最小化耦合。例如,一个对象的实例变量(非常量的成员字段),总是应该为私有private。这没有任何例外的情况(你可以偶尔很有效地使用protected方法,但是protected实例变量是相当讨厌的)同样的原因,你也绝不能使用 set/get 函数——他们只是另一种让字段变成公共的稍复杂方式而已。(虽然返回处理过的对象而不是一个基本类型的值的访问函数在某些情况下还是合理的,如果返回的对象的类是设计中的一个关键的抽象的话。)

这里我不是在卖弄学问。我发现了一个OO方式的严格性、快速代码开发、和简单的代码维护之间的直接的相关性。无论什么时候我违反了一个核心的OO原则比如隐藏实现细节,我只能结束代码的修改(通常是因为这个代码不可能进行调试)。我没有时间重写程序,所以我只能遵循这些规则。我关注的是完全实际的内容——我对为了设计而设计没有兴趣。

脆基类问题

现在,我们把耦合的概念应用到继承上。在一个使用实现-继承系统中,派生类对基类有十分紧密的耦合,同时这个闭合的连接是不受欢迎的。设计师们因此给这种行为起了一个绰号——“脆基类问题”。基类是被认为十分脆弱的,因为你可以通过一个表面上十分安全的方法修改一个基类,但这个新的行为,当被派生类继承的时候,可能会造成派生类运行出错。你不能简单孤立地通过检查基类的方法来判断你对基类的改变是不是安全;你也必须查看(并测试)所有的派生类。此外,你必须检查所有同时使用了基类和派生类对象的代码,因为这些代码可能会被新的行为所破坏。对关键的基类的小小的改变都会导致整个程序无法运行。

我们来一起检验这个脆基类和基类耦合这两个问题。下面的类扩展了Java的ArrayList类,来模拟栈的行为:

甚至像这样简单的一个类,都存在着问题。思考一下如果用户利用继承直接使用ArrayListclear()方法来把所有的元素都从栈中弹出去,会发生什么:

这个代码可以成功地编译,但是由于基类并不知道任何关于栈指针的信息,Stack对象现在处在一个不确定的状态。下面再调用push()会把新的条目放到索引2种(栈指针stack_pointer当前的值),这样栈看上去就有三个元素了——但底下两个已经被垃圾收集了。(Java类库中的Stack类就是这种问题,所以不要用)

对于不需要的方法继承,一种解决方式是,对于Stack要重写所有ArrayList的可能修改数组状态的方法,这样覆盖的函数可以正确处理栈指针或者抛出一个异常。(removeRange()方法是一个较好的抛出异常的候选。)

这个方法有两个缺点。第一,如果你覆盖所有的东西,基类就实际上成为了一个接口,而不是一个类。如果你不使用任何继承的方法,类继承就毫无意义。 第二,也是更为重要的一点,你并不希望一个栈能支持所有ArrayList的方法。比如,那个讨厌的removeRange()方法没什么用处。实现一个没用的方法的唯一合理的方式,就是让他抛出一个异常,这样他就不可能被调用了。这个方法却将一个编译时错误变成了运行时错误。这并不好。如果方式只是没有被声明,那么编译器会直接扔出一个“未找到方法”的错误。如果这个方法存在但是他抛出异常,你就不会发现错误直到程序运行的时候。

一个更好的解决方法是封装一个数据结构而不使用继承。这下面是一个Stack的改进过的新版本:

目前为止还不错,但是还要考虑到脆基类的问题。让我们假设你想创建一个Stack的变体,可以跟踪运行一段时间之后栈出现过的最大值。一种可能的实现如下:

新的类运行得很好,至少目前这样。但不幸的是,代码暴露了push_many()方法是通过调用push()来完成它的任务的。首先,这个细节看起来还不算一个糟糕的选择。他简化了代码,同时你可以获得派生类的push()版本,即使当Monitorable_stack是通过一个Stack类型的引用也能完成,所以,high_water_mark的更新是正确的。

某一天,有个人也许会运行一个测试工具并且发现了Stack还不够快,而且他要被频繁地使用。你可以重写一个不使用ArrayListStack,由此改进Stack的性能。下面是最新的版本:

注意push_many()不再是重复调用push(),而是采用了块传送。新版本的Stack运行很好;事实上,它要比原先的版本更 。但很不幸,派生类 Monitorable_stack 就不能再正常工作了,因为如果调用的是push_many()那么他不能正确跟踪栈的使用情况了(派生类的push()版本不再被继承了的push_many()所调用,所以他不会再更新high_water_mark)。现在Stack就是一个脆基类。正如上面显示的,事实上不可能仅仅靠小心就能消除这类问题。

值得注意的是如果你使用接口继承,就不会有这种问题,因为不会继承任何功能,就不会产生不良影响。如果Stack是一个接口,同时通过Simple_stackMonitorable_stack来实现,那么代码就会更加强壮。

在表0.1种,我提供了一个基于接口的解决方案。这个方法和类继承的方法有相等的弹性:你可以根据Stack抽象来写你的代码而不用担心你要具体处理那种类型的栈。由于这两种实现都必须提供公共接口中的所有方法,要出现问题也很难。我也有且仅有一次从写相同的基类代码中获益,因为我使用了封装而不是派生。从负面,我必须通过一个封装类中的细小的访问器方法去访问默认的实现。(例如Monitorable_Stack.push(...)(41行)必须要调用Simple_stack的中等价的方法。)程序员总是抱怨写这种一行就完了的代码,但是仅仅就写这么额外的一行的代价就可以消除潜在的巨大Bug。

表 0.1. 使用接口消除脆基类

框架(Frameworks)

关于脆基类的讨论如果不提到基于框架的编程,就不会是完整的讨论。像MFC(微软基础类库)这种框架已经成为一种建立类库的流行手段。虽然MFC正在急流勇退,但MFC的结构已经深深扎根在无数微软的车间——这里面的程序员都认为微软的方法就是最好的方法。

一个基于框架的系统,一般都是以半成品类的库作为起始,这些半成品的类不会完成所有事情,而是要依赖于派生类提供未完成的功能。Java中的一个典型的例子就是Componentpaint()方法,它其实只算一个占位符;而派生类则必须提供真正的版本。

你可以,但是一个整个类框架都依赖于基于派生的自定义是极其脆弱的。基类也很脆弱。当我用MFC编程的时候,每次微软发布一个新版本的MFC,我都必须重写自己的应用程序。代码会经常编译但接下来却不能正确工作因为一些基类的方法改变了。

所有的Java包都可以即开即用(Out of box)且运行良好。你无需扩展任何东西来让他们执行功能。 即开即用的结构比基于派生的框架要好。它更容易维护和使用,并且即使Sun提供的类改变了他的实现也不会让你的代码处于危险中。

脆基类的总结

一般来说,最好能避免继承具体基类和extends关系,而使用接口和implements关系。凭我的经验,代码中最少有80%应该是完全用接口的方式来写。比如,我从来不引用一个HashMap ;我通常会用指向Map接口的引用。(这里的“接口”是广义的。一个 InputStream也算一个有效的接口,你可以看看是如何使用它的,虽然他在Java中是描述为一个抽象类。)

你加入越多的抽象,弹性就会越好。在今天的商业环境中,通常在程序开发时需求就在不断变化,弹性是十分必要的。 此外,大多数敏捷开发方法(比如Crystal方法和极限编程)完全不能正常运作,除非代码是先用抽象写得。

如果你仔细研究Gang of Four的模式,你就会发现他们中很多都是提供了各种消除类继承的方法,而偏重于使用接口,这也是大多数模式的一个共性。我们一开始就要明白一个重要的事实:模式是被发现的,而不是被发明的。当你看了那些写得好的、易于维护可以很好运行的代码,模式自然就会浮现出来。这告诉我们这么多优秀的代码都会以各种方式避免类继承。

本文是从我即将发表的书中节选出来的,书暂时命名为《 Holub on Patterns: Learning Design Patterns by Looking at Code 》,将会在今年秋季通过Apress (www.apress.com)出版。

关于作者

Allen Holub 从1979年开始从事计算机产业的工作。他目前是一个顾问,通过对行政人员提供建议、培训以及设计、编程服务,来帮助企业不需要再软件上浪费钱。他撰写了8本书籍,包括 Taming Java Threads (Apress, 2000) 和 Compiler Design in C (Pearson Higher Education, 1990), 并且在加州大学伯克利分校教学。请在他的网站 ( http://www.holub.com ) 查询更多关于他的信息。


资源