我们将要使用的主要工具是重构(refactoring)。重构的目的不是增加新的功能,而是重写代码使得它们更加清晰。更加清晰的代码本身就是一个令人满意的结果,但是除此之外,它还可以带来其他引人注目的好处,应该能够吸引那些在底线上苦苦挣扎、如履薄冰的开发人员。
通常,如果代码经过了良好的重构,那么为它增加、修改或者去掉某些功能就会更加容易。这是因为代码很容易理解。相反,如果代码没有经过良好的重构,经常会出现,即使每一件事情都符合了当前的需求,但是开发团队仍然对于代码为何能够工作缺乏信心[1]。
需求的变化总是要求在很短时间内就能实现,这已经是大多数专业编程工作中的一种常态。重构可以保持代码清晰,使它易于维护,允许你毫无畏惧地面对和实现需求的变化。
在第2章的例子中,当将JavaScript、HTML和样式表分别移到单独的文件中时,我们实际上已经在做一些基本的重构工作了。然而,得到的JavaScript代码大约有120行,底层功能(例如向服务器发起请求)和专门处理列表对象的代码混在一起。随着项目渐渐变大,这一单独的JavaScript文件(在这个例子中,也是一个单独的样式表文件)很快就会变得难以维护。我们正在追求的目标是,创建小的、易读的、易修改的代码块,用来解决特定的问题,也就是我们常说的职责分离(separation of responsibilities)。
重构的另一个动机是识别出通用的解决方案,并且按照这种特定的模式来重新组织代码。这样做本身同样可以使得代码更加令人满意,而且还可以带来非常实际的好处。我们下面就来考虑一下这个问题。
3.1.1 模式:创造通用的词汇表
遵循成熟模式的代码,更有可能得到满意的结果,这仅仅是因为在此之前别人已经做过这件事。与之相关的很多问题都已经经过了其他人的深思熟虑,并且如我们希望的那样,已经解决了。如果我们运气好,可能其他人已经开发出了解决这类特定问题的可重用框架。
这种行事方式有时候也称作设计模式(design pattern)。模式的概念诞生于20世纪70年代,当时用来描述建筑规划问题的解决方案,近十年来,软件开发领域借用了这概念。服务器端的Java开发有很强的设计模式文化,微软最近也开始在.NET框架中大力推动设计模式的应用。这个术语总是令人产生一种难以亲近的学院派的味道。而且,为了令老板和客户听起来印象深刻,设计模式总是被人有意滥用。尽管如此,从根本上来说,设计模式仅仅是用来描述在软件设计中解决特定问题的一种可重复的方法。值得注意的是,设计模式为抽象的技术解决方案命名,使得它们更加便于讨论和容易理解。
设计模式之所以对重构如此重要,是因为它使我们能够简洁地描述意图达到的目标。“我们将这些执行用户操作的代码封装在一个对象中,这样就可以在需要的时候撤销这个操作”这样的说法,实在太拗口了,而且在重写代码的时候,要记住这样一个冗长的目标实在是有点困难。如果说“我们正在向代码中引入Command模式”,这种表述既更加准确,也更加便于讨论。
如果你是一位服务器端的Java开发者,或者是一位任何类型的系统架构师,可能会感到疑惑,我们说的这些有何新意?如果你是来自于Web设计或者新媒体世界[2],可能会认为我们是有着某种控制欲怪癖的家伙,因为我们宁可画图也不去编写一行真正的代码。无论你来自哪个阵营,都可能在疑惑,这些玩意儿与Ajax有何关系?我们的简短回答是:“大有关系”。我们下面就来探讨一下忙于工作的Ajax程序员可以从重构中得到哪些好处。
3.1.2 重构与Ajax
我们前面已经提到,Ajax应用很可能使用更多的JavaScript代码,而这些代码倾向于在浏览器中运行更长的时间。
在传统的Web应用中,复杂的代码运行在服务器上。在这些地方,设计模式常常应用在PHP、Java或.NET代码中。对于Ajax来说,我们需要考察如何在客户端代码中应用相同的技术。
甚至就连说JavaScript比Java和C#那样具有严格结构的语言更加需要良好的组织,都是大有争议的。不考虑它与C语言很相似的语法,JavaScript其实更接近于Ruby、Python,甚至是Common Lisp这样的语言,而不是Java或C#。JavaScript语言极具灵活性,有极大的空间允许开发者开发个人风格或者习惯用法。对于技艺纯熟的开发者来说,这是很棒的特征;而对于水平一般的开发者来说,它所能提供的安全保障实在是太少了。Java和C#这样的企业语言为由水平一般的开发者组成并且人员变动频繁的团队提供了良好的支持,然而JavaScript却没有提供这样的支持。
创建混乱的、艰深难懂的JavaScript代码的危险相对来说要高得多,随着代码从简单的Web页面上的小技巧发展到Ajax应用,这些危险的真实性会逐渐显现,说不定什么时候就会跳出来狠狠咬你一口。出于这个原因,我强烈提倡在Ajax开发中使用重构,其必要性比在Java或C#这类“安全的”编程语言中要大得多,因为在这类语言中设计模式早已经枝繁叶茂了。
3.1.3 保持均衡
继续前进之前,有必要再强调的一点是,重构和设计模式不过只是工具,只应该应用在确实有用的地方。如果过度使用,将会导致所谓的“分析瘫痪”(paralysis by analysis)状态,为了增加设计的灵活性和架构的适应性,以便应对可能永远也不会出现的未来的需求,导致应用的实现被这些不确定的因素所阻碍,迟迟无法得到可以运行的应用。
设计模式专家Erich Gamma在最近的一次访谈中(参见本章“资源”一节)对这种情况作了很好的总结。他谈到曾经有位读者请他帮忙,这位读者在自己的项目应用中只用到了《设计模式》一书中所描述的23个设计模式中的21个。这就好比一个开发者试图在他写的所有代码中避免使用整数、字符串和数组一样,设计模式只在一些特定的情形中才是有用的。
Gamma推荐将重构作为引入设计模式的最佳方式。第一次写代码的时候,先以最简单的方式来完成工作;然后当你遇到一些常见的问题时,引入模式来解决这些问题。如果你已经编写了大量的代码,或者负责维护大量别人编写的混乱代码,你可能已经体验过一种远离人群,孤独地沉浸在代码之中的生活。幸运的是,即便到了这个时刻,仍然有可能引入设计模式来改善代码的质量。在下面一小节,我们将拿第2章中开发的“粗糙但可用”的代码开刀,看看重构可以为它做些什么。
3.1.4 重构实战
重构听起来似乎是个好主意,但是头脑更加实际的开发者希望在花钱购买之前,至少要看到它确实会起作用。我们现在花一些时间,在上一章中那个包含了核心Ajax功能的例子(代码清单2-11)中应用一些重构技术。为了翻新那段代码的结构,我们定义了一个sendRequest()函数,触发一个发到服务器的请求。sendRequest()委派initHttpRequest()函数查找适当的XMLHttpRequest对象,并且以硬编码的方式为它分配了一个onReadyState()回调函数来处理服务器的响应。XMLHttpRequest对象定义为全局变量,使得回调函数可以找到它的引用。回调函数随后询问请求对象的状态,并且生成一些调试信息。
代码清单2-11的代码做了我们需要它做的事情,但是有些难以重用。通常,当我们发送请求到服务器时,也想要解析服务器的响应,并且根据结果做一些与应用密切相关的事情。为了在当前代码中插入定制的业务逻辑,我们需要修改onReadyState()函数这一段。
全局变量的存在也是一个容易出问题的地方。如果想要同时向服务器发起多个调用,我们必须能够为每个调用指定不同的回调函数。如果我们正在获取一个需要更新的资源列表,同时又要丢弃另外一个资源列表,无论如何我们不能将它们搞混!
在面向对象编程中,这类问题的标准解决方案是将必需的功能封装在对象中。要实现这一点,JavaScript语言支持的OO编码风格已经绰绰有余。我们将这个对象叫做ContentLoader,因为它用来从服务器加载内容。那么,这个对象看起来应该是个什么样子呢?理想情况下,我们应该能够创建它,给它传递一个表示请求的发送地址的URL。我们也应该能够给它传递两个到自定义回调函数的引用,一个在文档成功加载时调用,另一个在发生了错误时调用。对于这个对象的调用也许看起来像是这样:
var loader=new net.ContentLoader(‘mydata.xml’,parseMyData);
其中parseMyDate是文档成功加载时调用的回调函数。代码清单3-1展示了ContentLoader对象的实现代码。这其中涉及了一些新的概念,稍后我们就来讨论。
代码清单3-1 ContentLoader对象


这段代码中值得注意的第一件事是我们定义了一个全局变量net
,然后将所有其他的引用都附加在它的上面。这样做可以将变量名发生冲突的风险降到最小,并且使得所有与网络请求相关的代码都位于一个地方。
我们为对象提供了一个构造函数
。它有三个参数,只有前两个是必需的。第三个参数是错误处理函数,我们检查它是否为null值[3],如果需要就进行默认的错误处理。可以给函数传递数量可变的参数,这种能力对于OO程序员来说可能感觉会很奇怪。另外,可以将函数当作头等的引用来传递。这种能力也容易使人迷惑,不过,这些都是JavaScript的基本特征。我们在附录B中会更详细地讨论这些JavaScript语言的特征。
我们将代码清单2-11中的initXMLHttpRequest()
和sendRequest()
函数的很大一部分都移到了对象内部。我们也重新命名了这些函数以反映出它们稍微扩大的作用域,它现在称作loadXMLDoc
。我们使用同样的技术来查找XMLHttpRequest对象,并且初始化一个请求,不过这个对象的用户无需关注这一点。onReadyState回调函数
很大程度上也与代码清单2-11中的代码类似。我们将对调试控制台的调用替换成了对onload和onerror函数的调用。这里的语法看起来有点奇怪,我们来更仔细地检查一下。onload和onerror是Function对象,而Function.call()是这个对象的方法。Function.call()的第一个参数成为这个函数的上下文,也就是说,在函数的内部可以使用this关键字来引用它。
编写一个回调处理函数,将它传递给ContentLoader是非常简单的。如果我们需要引用任何ContentLoader的属性,例如XMLHttpRequest或者url,可以简单地使用this来做到,例如:

构建这样一个必需的“基础架构”(plumbing)需要理解一些JavaScript的怪癖,但是一旦这个对象写好了,最终用户不必再担心它。
这种状况通常是一次优秀重构的标记。我们将困难的代码隐藏在对象内部,对外只展示出容易使用的部分。最终用户减少了很多不必要的困难,与此同时,专家又可以在一个地方专心负责维护那些困难的代码。修改只需要做一次,就可以影响到整个代码库。
我们已经讨论了重构的基础知识,并且展示了在实际工作中它如何给我们带来好处。在下一节中,我们将考察Ajax编程中一些更加常见的问题,并且看看如何使用重构来解决这些问题。在这个过程中,我们将会发现一些有用的技巧,这些技巧在后续的章节中重用或者应用到你自己的项目中。







