javascript设计模式学习笔记二
2. 接口
接口是面向对象JavaScript程序员的工具箱中最有用的工具之一。GoF在《设计模式》一书中提出的可重用面向对象设计的第一条原则就说道:“针对接口而不是实现编程”,这个概念的基本性由此可见一斑。
问题在于,JavaScript中没有内置的创建或实现接口的方法。他也没有内置的方法可以用于判断一个对象是否实现了另一个对象相同的一套方法,这种对象很难互换使用。好在JavaScript有这出色的灵活性,因此添加这些特性并非难事。
先讨论在JavaScript中实现接口的各种方法,最后设计出一个可重用的类,用于检查对象是否具有必要的方法。
2.1 什么是接口
接口提供了一种用以说明一个对象应该具有哪些方法的手段。尽管它可以表明(或至少是暗示)这些方法的语义,但它并不规定这些方法应该如何实现。例如,如果一个接口包含一个名为setName的方法,那么你有理由认为这个方法的实现应该具有一个字符串参数,并且会把这个参数赋给一个name变量。
有了这个工具,你就能按这个对象提供的特性对它进行分组。例如,即使一批对象彼此存在着极大的差异,只要它们都实现了Comparable接口,那么在obj.compare(anotherObject)方法中就可以互换使用这些对象。你还可以使用接口开发不同的类之间的共同性。如果把原本要求以一个特定的类为参数的函数改为要求以一个特定的接口为参数函数,那么任何实现了该接口的对象都可以作为参数传递给它。这样一来,彼此不想关的对象也可以被同等对待。
2.1.1 接口之利
既定的一批接口具有自我描述性,并能促进代码的重用。接口可以告诉程序员一个类实现了哪些方法,从而帮助其他使用这个类。如果你熟悉一个特定的接口,那么久已经知道如何使用任何实现了它的类,从而更有可能重用现有的类。
接口还有助于稳定不同的类之间的通信方式。如果事先知道了接口,你就能减少在集成两个对象的过程中出现问题。
测试和调试因此也能变得更轻松。在JavaScript这种弱类型语言中,类型不匹配错误很难追踪。使用接口可以让这种错误的查找变得更容易一点,因为此时如果一个对象不像所要求的类型,或者没有实现必要方法,那么你会得到包含有用信息的明确的错误提示。这样一来,逻辑错误可以被限制在方法自身,而不是在对象的构成之中。接口还能让代码变得更稳固,因为对接口的任何改变在所有实现他的类中都必须体现出来。如果接口添加了一个操作,而某个实现他的类并没有相应地添加这个操作,那么你肯定会立即见到一个错误。
2.1.2 接口之弊
接口并非没有缺点。JavaScript是一种具有极强的表现力的语言,这主要得益于其弱类型的特点。而接口的使用则在一定程度上强化了类型的作用。这降低了语言的灵活性。
JavaScript并没有提供对接口的内置支持,而试图模仿其他语言内置的功能总会有一些风险。JavaScript中没有Interface这个关键词,因此,不管你用什么方法实现接口,他总是与c++和java这些语言中所用的方法大相径庭,这加大了初涉JavaScript时所遇到的困难。
JavaScript中任何实现接口的方法都会对性能造成一些影响,再某种程度上这得归咎于额外的方法调用的开销。我们的实现方法中使用了两个for循环来遍历所需要的每一个接口中的每一个方法。对于大型接口和需要实现许多不同的接口对象,这种检查可能要花点时间,从而对性能造成负面影响。如果你在乎这个问题,那么可以在开发完成之后剔除这种代码,或者将其执行与一个调式标志关联起来,这样在运营环境中他就不会执行。但要注意不要过早进行优化处理。firebug这类性能分析器可以帮助你判断是否真有必要剔除接口的代码。
接口使用中最大的问题在于,无法强迫其他程序员遵守你定义的接口。在其他语言中,接口的概念是内置的,如果某人定义了实现一个接口的类,那么编译器会确保该类的确实现了这个接口。而在JavaScript中则必须用手工的办法保证某个类实现了一个接口。编码规范和辅助类可以提供一些帮助,但无法彻底根除这个问题。如果项目的其他程序员不认真对待接口,那么这些接口的使用是无法得到强制性的保证的。除非项目的所有人都同意使用接口并对其进行检查,否则接口的很多价值都无从体现。
2.2 在JavaScript中模仿接口
下面我们将探讨在JavaScript中模仿接口的三种方法:注释法、属性检查法和鸭式辩型法。没有哪种技术是完美的,但三者结合使用基本上可以令人满意。
2.2.1 用注释描述接口
用注释模仿接口是最简单的方法,但效果确实最差的。这种方法模仿其他面向对象语言中的做法,使用了interface和implements关键字,但把它们放在注释中,以免引起语法错误。下面的示例演示了如何把这些关键字添加到代码中以描述所要求的方法:
1 /* 2 3 interface Composite { 4 function add(child); 5 function remove(child); 6 function getChild(index); 7 } 8 9 interface FormItem { 10 function save(); 11 } 12 13 */ 14 15 var CompositeForm = function(id, method, action) { // implements Composite, FormItem 16 ... 17 }; 18 19 // Implement the Composite interface. 20 CompositeForm.prototype.add = function(child) { 21 ... 22 }; 23 CompositeForm.prototyppe.remove = function(child) { 24 ... 25 }; 26 CompositeForm.prototyppe.getChild = function(index) { 27 ... 28 }; 29 30 // Imlement the FormItem interface. 31 CompositeForm.prototyppe.save = function(child) { 32 ... 33 };
这种模仿并不是很好。他没有为确保CompositeForm真正实现了正确的方法集而进行检查,也不会抛出错误以告知程序员程序中有问题。说到底它主要还是属于程序文档范畴。在这种做法中,对接口约定的遵守完全依靠自觉。
尽管如此,这种方法也有其优点。它易于实现,不需要额外的类或函数。他可以提高代码的可重用性,因为现在那些类实现的接口都有说明,程序员可以把它们与其他实现了同样接口的类互换使用。这种方法并不影响文件尺寸或执行速度,因为它所用的注释可以在对代码进行部署时不费吹灰之力的予以剔除。但是,由于不会提供错误信息,它对测试和调试没有什么帮助。
2.2.2 用属性检查模仿接口
第二种方法要更严谨一点。所有类都明确地声明自己实现了哪些接口,那些想与这些类打交道的对象可以针对这些声明进行检查。那些接口自身仍然只是注释,但现在你可以通过检查一个属性得知某个类自称实现了什么接口:
1 /* 2 3 interface Composite { 4 function add(child) 5 function remove(child) 6 function getChild(child) 7 } 8 9 interface FormItem { 10 function save(); 11 } 12 */ 13 14 var CompositeForm = function(id, method, action) { 15 this.implementsInterface = ['Composite', 'FormItem']; 16 ... 17 }; 18 19 function addForm(formInstance) { 20 if(!implements(formInstance, 'Composite', 'FormItem')) { 21 throw new Error("Object does not implement a required interface."); 22 } 23 ... 24 }; 25 26 // the implements function, which checks to see if an object declares that it 27 // implements the required interfaces. 28 29 function implements(object) { 30 for (var i = 1; i < arguments.length; i++) { // loop through all arguments after the first one. 31 var interfaceName = arguments[i]; 32 var interfaceFound = false; 33 for (var j = 0; j < object.implementsInterfaces.length; j++) { 34 if(object.implementsInterfaces[j] == interfaceName) { 35 interfaceFound = true; 36 break; 37 } 38 } 39 if(!interfaceFound) { 40 return false; // an interface was not found. 41 } 42 } 43 return ture; // all interface were found. 44 }
在这个例子中,CompositeForm宣称自己实现了Composite和FormItem这两个接口,其做法是把这两个接口的名称加入一个名为implementsInterface的数组。类显示声明自己支持什么接口。任何一个要求其参数属于特定类型的函数都可以对这个属性进行检查,并在所需接口未在声明之列时抛出一个错误。
这种方法有几个优点。他对类所实现的接口提供了文档说明。如果需要一个接口不在一个类宣称支持的接口之列,你会看到错误信息。通过利用这些错误,你可以强迫其他程序员声明这些接口。
这种方法的只要缺点在于它并未确保类真正的实现了自称实现的接口。你只知道他是否说自己实现了接口。在创建一个类时声明它实现了一个接口,但后来在实现该接口所规定的方法时却漏掉其中的某一个,这一种错误很常见。此时所有检查都能通过,但那个方法却并不存在,这将在代码中埋下一个隐患。另外,显示声明类所支持的接口也需要一些额外的工作。
2.2.3 用鸭式辨型模仿接口
其实,类是否声明自己支持哪些接口并不重要,只要它具有这些接口中的方法就行。鸭式辨型(这个名称来自James Whitcomb Riley的名言:“像鸭子一样走路并且嘎嘎叫的就是鸭子”)正是基于这样的认识。它把对象实现的方法集作为判断它是不是某个类的实例的唯一标准。这种技术在检查一个类是否实现了某个接口时也可大显身手。这种方法背后的观点很简单:如果对象具有与接口定义的方法同名的所有方法,那么就可以认为它实现了这个接口。你可以用一个辅助函数来确保对象具有所有必需的方法:
1 // Interfaces. 2 3 var Composite = new Interface('Composite', ['add', 'remove', 'getChild']); 4 var FormItem = new Interface('FormItem', ['save']); 5 6 // CompositeForm class 7 8 var CompositeForm = function(id, method, action) { 9 ... 10 }; 11 12 function addForm(formInstance) { 13 ensureImplements(formInstance, Composite, FormItem); 14 // this function will throw an error if a required method is not implemented. 15 ... 16 }
与另外两种方法不同,这种方法并不借助于2注释。其各个方面都是可以强制实施的。ensureImplements函数需要至少两个参数。第一个参数是想要检查的对象。其余参数是据以对那个对象进行检查的接口。该函数检查其第一个参数代表的对象是否实现了那些接口所声明的所有方法。如果发现漏掉了任何一个方法,他都会抛出错误,其中包含了所缺少的那个方法和未被正确实现的接口的名称等有用信息。这种检查可以用在代码中任何需要确保某个对象实现了某个接口的地方。在本例中,addForm函数仅当一个表单对象支持所有必要的方法时才会对其执行添加操作。
尽管鸭式辨型可能是上述三种方法中最有用的一种,但它也有一些缺点。在这种方法中,类并不声明自己实现了哪些接口,这降低了代码的可重用性,并且也缺乏其他两种方法那样的自我描述。他需要使用一个辅助类(Interface)和一个辅助函数(ensureImplements)。而且,他只关心方法的名称,并不检查其参数的名称、数目或类型。
2.3 采用的接口实现方法
综合使用了第一种和第三种方法。我们用注释声明类支持的接口,从而提高代码的可重用性及其文档的完善性。我们还用辅助类Interface及其类方法Interface.ensureImplements来对对象实现的方法进行显示检查。如果对象未能通过检查,这个方法返回一条有用的错误信息。
下面是一个结合使用Interface类与注释的实例:
1 // Interface. 2 3 var Composite = new Interface('composite', ['add', 'remove', 'getChild']); 4 var FormItem = new Interface('FormItem', ['save']); 5 6 //CompositeForm class 7 8 var CompositeForm = function(id, method, action) { 9 // implements Composite, FormItem 10 ... 11 }; 12 13 function addForm(formInstance) { 14 Interface.ensureImplements(formInstance, Composite, FormItem); 15 // this function will throw an error if a required methods is not implemented, 16 // halting execution of the function. 17 // all code beneath this line will be executed only if the checks pass. 18 }
Interface.ensureImplements起着严格把关的作用。如果他发现有问题,就会抛出一个错误。这个错误要么被其他代码捕捉到并得到处理,要么中断程序的执行。无论是哪种情况,程序员都能立即知道代码中存在问题,并且知道其来源位置。
2.4 Interface类
下面是案例中使用的Interface类的定义:
1 // Constructor 2 3 var Interface = function(name, methods) { 4 if(arguments.length != 2) { 5 throw new Error("Interface constructor called with " + arguments.length + "arguments, but expected exactly 2."); 6 } 7 8 this.name = name; 9 this.methods = []; 10 for(var i = 0; len = methods.length; i < len; i++) { 11 if(typeof methods[i] !== 'string') { 12 throw new Error("Interface constructor expects method names to be passed in as a string.") 13 } 14 this.methods.push(methods[i]); 15 } 16 }; 17 18 // Static class method. 19 20 Interface.ensureImplements = function(object) { 21 if(arguments.length < 2) { 22 throw new Error("unction interface.ensureImplements called with " + arguments.length + "arguments, but expected at least 2."); 23 } 24 25 for(var i = 1, len = arguments.length; i < len; i++) { 26 var interface = arguments[i]; 27 if(interface.comstructor !== Interface) { 28 throw new Error("Funtiom Interface.ensureImplements expects arguments two and above to instances of Interface.") 29 } 30 31 for(var j = 0; methodsLen = interface.methods.length; j < methodLen; j++) { 32 var method = interface.methods[j]; 33 if(!object[method] || typeof pbject[method] !== 'function') { 34 throw new Error("Function Interface.ensureImplements: object does not implement the interface.name interface. Method" + method + was not found"); 35 } 36 } 37 } 38 } ;
从中可以看到,该类的所有方法对其参数都有严格的要求,如果参数未能通过检查,将导致错误的抛出。我们特地加入这种检查的目的在于:如果没有错误被抛出,那么你可以肯定接口已经得到正确的声明和实现。
2.4.1 Interface类的使用场景
严格的类型检查并不总是明智的。许多JavaScript程序员根部不用接口或它所提供的那种检查,也照样一干多年。接口在运用设计模式实现复杂系统的时候最能体现其价值。它看似降低了JavaScript的灵活性,而实际,因为使用接口可以降低对象间的耦合程度,所以他提高了代码的灵活性。接口的使用可以使函数变得更灵活,因为你既能响函数传递任何类型的参数,又能保证他只会使用那些具有必要方法的对象。某些场合接口很有用处。
在有许多程序员参与的大型项目中,接口起着至关重要的作用。程序员常常需要使用还未编写出来的api,或者需要提供一些占位代码(sub)以免延误开发进度。接口在这种场合中的重要性表现在很多方面。它们记载着Api,可作为程序员正式交流的工具。在占位代码被替换为最终的api时,你立刻就能知道所需要的方法是否得到了实现。在开发过程中,如果api发生了变化,只要新的api实现了同样的接口,他就能天衣无缝地替换原有的api。
现在项目中用到来自因特网上的、你无法直接控制的代码的情况越来越普遍。部署在外部环境中的程序库以及搜索、电子邮件、地图等服务api都是这类代码的例子。即使他们有着可信的来源,也必须谨慎使用,确保其变化不会在自己的代码中引起问题。一种对应之策是为所依赖的每一个api创建一个Interface对象,然后对接收到的每一个对象都进行检查,以确保其正确实现了那些接口:
1 var DynamicMap = new Interface('DynamicMap', ['centerOnPoint', 'zoom', 'draw']); 2 3 function displayRoute(mapInstance) { 4 Interface.ensureImplements(mapInstace, DynamicMap); 5 mapInstance.centerOnPoint(12, 34); 6 mapInstance.zoom(5); 7 mapInstance.draw(); 8 ... 9 }
在这个示例中,displayRoute函数要传入的参数具有3个特定方法。通过使用一个Interface对象和调用Interface.ensureImplements方法,可以确保这些方法已经得到实现,否则你将见到一个错误。这个错误可以用一个try/catch快捕获,然后可能会被用于发送一条Ajax请求,将外部api引起的问题告知用户。这有助于提高mash-up应用系统的安全性和稳定性。
2.4.2 Interface类的用法
判断在代码中使用接口是否划算是最重要(也是最困难)的一步。对于小型的,不太费事的项目来说,接口的好处也许并不明显,只是徒增其复杂度而已。你需要自我权衡其利弊。如果为在项目中使用接口利大于弊,那么可以参照如下使用说明。
(1)将Interface类纳入HTML文件。http://jsdesignpatterns.com/上下载到Interface.js文件。
(2)遂一检查代码中所有以对象为参数的方法。搞清楚代码的正常运转要求这些对象参数具有哪些方法。
(3)为你需要的每一个不同的方法创建一个Interface对象。
(4)剔除所有针对构造器的显示检查。因为我们使用的是鸭式辨型,所以对象的类型不再重要。
(5)以Interface.ensureImplements取代原来的构造器检查。
这样做有什么好处?答案是代码的耦合程度降低了。因为现在你不再依赖于任何特定的类的实例,二十检查所需要的特性是否都已就绪(不管其具体如何实现)。由此你再对代码进行优化和重构时将拥有更大的自由。
2.4.3 示例:使用Interface类
假设你要创建一个类,它可以将一些自动化测试结果转化为祤在网页上查看的格式。该类的构造器以一个TestResult类的实例为参数。它会应客户的请求对这个TestResult对象所封装的数据进行格式化,然后输出。这个ResultFormatter类最初的实现如下:
1 // ResultFormatter class, before we implement interface checking. 2 3 var ResultFormatter = function(resultsObject) { 4 if(!(resuktFormatter instanceOf TestResult)) { 5 throw new Error("ResultsFormatter: constructor requires an instance of TestResult as an argument."); 6 } 7 this.resultsObject = resultsObject; 8 }; 9 10 ResultFormatter.prototype.renderResults = function() { 11 var dateOfTest = this.resultObject.getDate(); 12 var resultsArray = this.resultsObject.getResults(); 13 14 var resultsContainer = document.createElement('div'); 15 16 var resultsHeader = document.createElement('h3'); 17 resultsHeader.innerHTML = 'Test Result from' + dateOfTest.toUTCString(); 18 resultsContainer.appendChild(resultsHeader); 19 20 var resultsList = document.createElement('ul'); 21 resultsContainer.appendChild(resultsList); 22 23 for(var i = 0; len = resultsArray.length; i < len; i++) { 24 var listItem = document.createElement('li'); 25 listItem.innerHTML = resultsArray[i]; 26 resultsList.appendChild(listItem); 27 } 28 return resultsContainer; 29 };
该类的构造器会对参数进行检查,以确保其的确为TestResult类的实例。如果参数达不到要求,构造器将抛出一个错误。有了这样的保证,在编写renderResults方法时,你就可以认定有getDate和getResults这两个方法可供使用。嗯......当真是这样吗?在构造中只对resultsObject是否为TestResult类的实例进行检查。实际上这并不能保证所需要的方法得到了实现。TestResult类可能会被修改,致使其不再拥有getDate方法。再次情况下,构造器中的检查仍能通过,但renderResult方法却会失灵。
此外,构造函数中的这个检查施加了一些不必要的限制。他不允许使用其他的类的实例做参数,哪怕它们原本可以如愿发挥作用。例如,有一个名为WeatherData的类也拥有getDate和getResults这两个方法。他本来可以被ResultsFormatter类用得好好的,但是那个显示类型检查(它使用的是instanceOf运算符)会阻止使用WeatherData类的任何实例。
问题的解决办法是删除那个使用instanceOf的检查,并用接口代替它。首先,我们需要创建这个接口:
1 // ResultSet Interface. 2 3 var ResultSet = new Interface('ResultSet', ['getDate', 'getResults']);
这行代码创建了一个Interface对象的新实例。第一个参数是接口的名称,第二个参数是一个字符串数组,其中的每个字符串都是一个必需的方法和名称。有了这个接口有之后,就可以用接口检查替代instanceOf检查了:
1 // ResultFormatter class, after adding Interface checking. 2 3 var ResultFormatter = function(resultsObject) { 4 Interface.ensureImplements(resultsObject, ResultSet); 5 this.resultsObject = resultsObject; 6 }; 7 8 ResultFormatter.prototype.renderResults = function() { 9 ... 10 };
renderResults方法保持不变。而构造器则被改为使用ensureImplements方法而不是instanceOf运算符。现在这个构造器可以接受WeatherData或其他任何实现了所需方法的类的实例。我们只修改了几行ResultFormatter类的代码,就让那个检查变得更准确(它要求参数对象实现所有必须的方法),而且也更宽容(它允许使用任何匹配该接口的对象)。
2.5 依赖于接口的设计模式
下面列出的设计模式尤其依赖接口。
□ 工厂模式。 对象工厂所创建的具体对象会因具体情况而异。使用接口可以确保所创建的这些对象可以互相使用。也就是说,对象工厂可以保证其生产出来的对象都实现了必须的方法。
□ 组合模式。 如果不用接口你就不可能使用这个模式。组合模式的中心思想在于可以将对象群体育其组成对象同等对待。这是通过让它们实现同样的接口来做到的。如果不进行某种形式的鸭式辨型或类型检查,组合模式就会失去大部分作用。
□ 装饰者模式。 装饰者通过透明地为另一个对象提供包装而发挥作用。这是通过实现与另外那个对象完全相同的接口而做到的。对于外界而言,一个装饰者和它所包装的对象看不出有什么区别。我们将使用Interface类来确保所创建的装饰者对象实现了必须的方法。
□ 命令模式。 代码中所有的命令对象都要实现同一批方法(他们通常被命名为execute、run或undo)。通过使用接口,你为执行这些命令对象而创建的类可以不必知道这些对象具体是什么,只要知道它们都实现了正确的接口即可。籍此你可以创建出模块化程度很高而耦合成都很低的用户界面和api。
2.7 小结
本章讨论了接口在一些流行的面向对象语言中的使用和实现方法,而且说明了接口概念的各种不同实现方式都有一些共同特性:它们都提供一种规定必须方法的手段;他们都提供一种检查这些方法是否确实得到实现的手段,并且在结果为否定的时候能提供有用的错误信息。在JavaScript中可以结合使用文档手段(注释)、辅助类和鸭式辨型来模仿这些的特性。使用接口的难点在于判断是否有必要使用它。它并不总是不可或缺。灵活性是JavaScript最强大的特色之一。强制进行不必要的严格类型检查会损害这种灵活性。谨慎地使用Interface类有助于创建更健壮的类和更稳定的代码。