你的位置:首页 > Java教程

[Java教程][Effective JavaScript 笔记]第57条:使用结构类型设计灵活的接口


想象创建wiki的库。wiki网站包含用户可以交互式地创建、删除和修改的内容。许多wiki都以简单、基于文本标记语言创建内容为特色。通常,这些标记语言只提供了HTML可用功能的一个子集,但是却有一个更简单、更清晰的源格式。例如,环绕星号的文本被格式化为粗体,环绕下划线的被格式化为带有下划线的文本,环绕斜杠的被格式化为斜体。用记可以输入如下格式:

this sentence contains a *bold phrase* within it.this sentence contains a _underlined phrase_ within it.this sentence contains a /italicized phrase/ within it.

该网站会以下面的形式将内容呈现给wiki读者
this sentence contains a bold phrase within it.
this sentence contains a underlined phrase within it.
this sentence contains a italicized phrase within it.
一个灵活的wiki库应该给应用程序编写者提供可供选择的标记语言。
要给应用程序编写者提供备选的标记语言,需要将提取用户创建的标记源文本内容的功能与其他功能分离。其他的wiki功能包括账记管理、修订历史记录和内容存储等。其余的应用程序可以通过一套文档完善的属性和方法的接口与提取出的功能交互。通过针对接口完备API的严格编程,并忽略这些方法的实现细节,从而不论选择使用哪个源格式,应用程序都能正常运行。

更加深入地了解需要什么样的wiki内容提取接口。wiki库必须能提取元数据,如页面标题和作者信息等,并能将页面内容格式化为HTML呈现给wiki读者。可以将wiki中的每一个页面表示为提供了通过getTitle、getAuthor和toHTML页面方法获取这些数据的一个对象。

接下来,该wiki库需要提供创建一个自定义wiki格式化器的应用程序的方法,以及一些针对流行标记格式的内置格式化器。例如,应用程序的编写者可能希望使用MediaWiki格式化器(这是维基百科所使用的格式)。

var app=new Wiki(Wiki.formats.MEDIAWIKI);

该wiki库将该格式化函数存储在wiki实例对象的内部

function Wiki(format){  this.format=format;}

每当读者想要查看页面时,应用程序都会检索出其源文本并使用内部的格式化器将源文本渲染为HTML页面。

Wiki.prototype.displayPage=function(source){  var page=this.format(source);  var title=page.getTitle();  var author=page.getAuthor();  var output=page.toHTML();  //...}

类似Wiki.formats.MEDIAWIKI的格式化器是如何实现的?熟悉基于类编程的程序员可能倾向于创建一个Page的基类,该Page类表示用户创建的内容,每个Page的子类实现不同的格式。MediaWiki格式化可能实现为一个继承Page的MWPage类,而MEDIAWIKI则是返回MWPage实例的“工厂函数”。

function MWPage(source){  Page.call(this,source);  //...}MWPage.prototype=Object.create(Page.prototype);MWPage.prototype.getTitle=function(){};MWPage.prototype.getAuthor=function(){};MWPage.prototype.toHTML=function(){};wiki.formats.MEDIAWIKI=function(source){  return new MWPage(source);}

这里由于MWPage类需要自己实现所有wiki应用程序需要的getTitle,getAuthor,toHTML方法,Page基类并没有起到什么作用。而且displayPage方法并不需要关心页面对象的继承体系。它只需要实现如何显示页面的相关方法。wiki格式的实现很自由,任何能完成功能的代码都可以。
使用简单对象字面量构建MediaWiki页面格式的接口实现通常已经足够了。

Wiki.formats.MEDIAWIKI=function(source){  //....  return {    getTitle:function(){},    getAuthor:function(){},    toHTML:function(){}  }};

继承有时会导致比它解决的更多的问题。当几个不同的wiki格式共享不相重叠的功能集时,继承的问题就出现了。没有任何继承结构才是对的。例如:

Format A:*bold*,[link],/italics/Format B:**bold**,[[link]],*italics*Format A:**bold**,[link],*italics*

我们想要实现各个部分的功能来识别每种不同类型的输入,然而功能的混合和匹配并没有映射到任何清晰的A,B和C之间的继承层次关系。正确的做法是分别实现每种输入匹配的函数,然后根据每种格式的需要混合和匹配功能。

注意消除Page基类,这里不需要用任何东西来替代它。任何人希望实现一个新的自定义格式都可以,而不需要在某处“注册”它。只要displayPage方法结构正确,具有预期行为的getTitle,getAuthor,toHTML方法,那么它就适用任何js对象。

这种接口有时称为结构类型或鸭子类型。任何对象只要具有预期的结构就属于该类型(看起来像只鸭子,或叫声像只鸭子)。在js中这是一种优雅、轻量的编程模式,因为不需要编写显示的声明。一个调用某个对象方法的函数能够与任何实现了相同接口的对象一起工作。当然你在API的文档中列出对象接口的预期结构。这样接口实现者便会知道哪些属性和方法是必需的以及库和应用程序期望的行为是什么。
灵活的结构类型的另一个好处是有利于单元测试。wiki库可能期望嵌入一个HTML服务器对象来实现wiki网站的网络功能。如果想要在没有连接网络的情况下测试wiki网站的交互时序,那么可以实现一个mock对象来模拟HTTP服务器的行为。这些行为是遵照预定的脚本而不是真实的接触网络。这种方式提供了与虚拟服务器重复交互的行为,而不是依赖不可预知的网络行为。这使用测试组件与服务器的交互行为成为可能。

提示

  • 使用结构类型(也称为鸭子类型)来设计灵活的对象接口

  • 结构接口更灵活、更轻量,所以应该避免使用继承

  • 针对单元测试,使用mock对象即接口的替代实现来提供可复检的行为