接口是面向对象JavaScript程序员的工具箱中最有用的工具之一。在设计模式中提出的可重用的面向对象设计的原则之一就是“针对接口编程而不是实现编程”,即咱们所说的面向接口编程,这个概念的重要性可见一斑。但问题在于,在JavaScript的世界中,没有内置的建立或实现接口的方法,也没有能够判断一个对象是否实现了与另外一个对象相同的一套方法,这使得对象之间很难互换使用,好在JavaScript拥有出色的灵活性,这使得模拟传统面向对象的接口,添加这些特性并不是难事。接口提供了一种用以说明一个对象应该具备哪些方法的手段,尽管它能够代表这些方法的含义,可是却不包含具体实现。有了这个工具,就能按对象提供的特性对它们进行分组。例如,假如A和B以及接口I,即使A对象和B对象有极大的差别,只要他们都实现了I接口,那么在A.I(B)
方法中就能够互换使用A和B
,如B.I(A)
。还可使用接口开发不一样的类的共同性。若是把本来要求以一个特定的类为参数的函数改成要求以一个特定的接口为参数的函数,那么全部实现了该接口的对象均可以做为参数传递给它,这样一来,彼此不相关的对象也能够被相同地对待。html
既定的接口具备自我描述性,并可以促进代码的重用性,接口能够提供一种信息,告诉外部一个类须要实现哪些方法。还有助于稳定不一样类之间的通讯方式,减小了继承两个对象的过程当中出现的问题。这对于调试也是有帮助的,在JavaScript这种弱类型语言中,类型不匹配很难追踪,使用接口时,若是出现了问题,会有更明确的错误提示信息。固然接口并不是彻底没有缺点,若是大量使用接口会必定程度上弱化其做为弱类型语言的灵活性,另外一方面,JavaScript并无对接口的内置的支持,只是对传统的面向对象的接口进行模拟,这会使自己较为灵活的JavaScript变得更加难以驾驭。此外,任何实现接口的方式都会对性能形成影响,某种程度上归咎于额外的方法调用开销。接口使用的最大的问题在于,JavaScript不像是其余的强类型语言,若是不遵照接口的约定,就会编译失败,其灵活性能够有效地避开上述问题,若是是在协同开发的环境下,其接口颇有可能被破坏而不会产生任何错误,也就是不可控性。程序员
在面向对象的语言中,使用接口的方式大致类似。接口中包含的信息说明了类须要实现的方法以及这些方法的签名。类的定义必须明确地声明它们实现了这些接口,不然是不会编译经过的。显然在JavaScript中咱们不能如法炮制,由于不存在interface和implement关键字,也不会在运行时对接口是否遵循约定进行检查,可是咱们能够经过辅助方法和显式地检查模仿出其大部分特性。编程
在JavaScript中模仿接口主要有三种方式:经过注释、属性检查和鸭式辩型法,以上三种方式有效结合,就会产生相似接口的效果。
注释是一种比较直观地把与接口相关的关键字(如interface
、implement
等)与JavaScript代码一同放在注释中来模拟接口,这是最简单的方法,可是效果最差。代码以下:设计模式
1 //以注释的形式模仿描述接口 2 /* 3 interface Composite{ 4 function add(child); 5 function remove(child); 6 function getName(index); 7 } 8 9 interface FormItem{ 10 function save(); 11 } 12 */ 13 14 15 //以注释的形式模仿使用接口关键字 16 var CompositeForm =function(id , method,action) { //implements Composite , FormItem 17 // do something 18 } 19 //模拟实现具体的接口方法 此处实现Composite接口 20 CompositeForm.prototype.Add=function(){ 21 // do something 22 } 23 24 CompositeForm.prototype.remove=function(){ 25 // do something 26 } 27 28 CompositeForm.prototype.getName=function(){ 29 // do something 30 } 31 32 //模拟实现具体的接口方法 此处实现FormItem接口 33 Composite.prototype.save=function(){ 34 // do something 35 }
这种方式其实并非很好,由于这种模仿还只停留在文档规范的范畴,开发人员是否会严格遵照该约定有待考量,对接口的遵照彻底依靠开发人员的自觉性。另外,这种方式并不会去检查某个函数是否真正地实现了咱们约定的“接口”。尽管如此,这种方式也有优势,它易于实现而不须要额外的类或者函数,能够提升代码的可重用性,由于类实现的接口都有注释说明。这种方式不会影响到文件占用的空间或执行速度,由于注释的代码能够在部署的时候轻松剔除。可是因为不会提供错误消息,它对测试和调试没什么帮助。下面的一种方式会对是否实现接口进行检查,代码以下:数组
1 //以注释的形式模仿使用接口关键字 2 var CompositeForm =function(id , method,action) { //implements Composite , FormItem 3 // do something 4 this.implementsinterfaces=['Composite','FormItem']; //显式地把接口放在implementsinterfaces中 5 } 6 7 8 //检查接口是否实现 9 function implements(Object){ 10 for(var i=0 ;i< arguments.length;i++){ 11 var interfaceName=arguments[i]; 12 var interfaceFound=false; 13 for(var j=0;j<Object.implementsinterfaces.length;j++){ 14 if(Object.implementsinterfaces[j]==interfaceName){ 15 interfaceFound=true; 16 break; 17 } 18 } 19 if(!interfaceFound){ 20 return false; 21 }else{ 22 return true; 23 } 24 } 25 } 26 27 28 function AddForm(formInstance){ 29 if(!implements(formInstance,'Composite','FormItem')){ 30 throw new Error('Object does not implements required interface!'); 31 } 32 }
上述代码是在方式一的基础上进行完善,在这个例子中,CompositeForm
宣称本身实现了Composite
和FormItem
这两个接口,其作法是把这两个接口的名称加入一个implementsinterfaces
的数组。显式地声明本身支持什么接口。任何一个要求其参数属性为特定类型的函数均可以对这个属性进行检查,并在所须要的接口未在声明之中时抛出错误。这种方式相对于上一种方式,多了一个强制性的类型检查。可是这种方法的缺点在于它并未保证类真正地实现了自称实现的接口,只是知道它声明本身实现了这些接口。其实类是否声明本身支持哪些接口并不重要,只要它具备这些接口中的方法就行。鸭式辩型(像鸭子同样走路而且嘎嘎叫的就是鸭子)正是基于这样的认识,它把对象实现的方法集做为判断它是否是某个类的实例的惟一标准。这种技术在检查一个类是否实现了某个接口时也能够大显身手。这种方法的背后观点很简单:若是对象具备与接口定义的方法同名的全部方法,那么就能够认为它实现了这个接口。可使用一个辅助函数来确保对象具备全部必需的方法,代码以下:app
1 //interface 2 var Composite =new Interface('Composite',['add','remove','getName']); 3 var FormItem=new Interface('FormItem',['save']); 4 5 //class 6 var Composite=function(id,method,action){ 7 8 } 9 10 //Common Method 11 function AddForm(formInstance){ 12 ensureImplements(formInstance,Composite,FormItem); 13 //若是该函数没有实现指定的接口,这个函数将会报错 14 }
与另外两种方式不一样,这种方式无需注释,其他的各个方面都是能够强制实施的。EnsureImplements
函数须要至少两个参数。第一个参数是想要检查的对象,其他的参数是被检查对象的接口。该函数检查器第一个参数表明的对象是否实现了那些接口所声明的方法,若是漏掉了任何一个,就会抛错,其中会包含被遗漏的方法的有效信息。这种方式不具有自我描述性,须要一个辅助类和辅助函数来帮助实现接口检查,并且它只关心方法名称,并不检查参数的名称、数目或类型。模块化
在下面的代码中,对Interface
类的全部方法的参数都进行了严格的控制,若是参数没有验证经过,那么就会抛出异常。加入这种检查的目的就是,若是在执行过程当中没有抛出异常,那么就能够确定接口获得了正确的声明和实现。函数
1 var Interface = function(name ,methods){ 2 if(arguments.length!=2){ 3 throw new Error('2 arguments required!'); 4 } 5 this.name=name; 6 this.methods=[]; 7 for(var i=0;len=methods.length;i<len;i++){ 8 if(typeof(methods[i]!=='String')){ 9 throw new Error('method name must be String!'); 10 } 11 this.methods.push(methods[i]); 12 } 13 } 14 15 16 Interface.ensureImplements=function(object){ 17 if(arguments.length<2){ 18 throw new Error('2 arguments required at least!'); 19 } 20 for(var i=0;len=arguments.length;i<len;i++){ 21 var interface=arguments[i]; 22 if(interface.constructor!==Interface){ 23 throw new Error('instance must be Interface!'); 24 } 25 for(var j=0;methodLength=interface.methods.length;j<methodLength;j++){ 26 var method=interface.methods[j]; 27 if(!object[method]||typeof(object[method])=='function')){ 28 throw new Error('object does not implements method!'); 29 } 30 } 31 } 32 }
其实多数状况下,接口并非常常被使用的,严格的类型检查并不老是明智的。可是在设计复杂的系统的时候,接口的做用就体现出来了,这看似下降了灵活性,却同时也下降了耦合性,提升了代码的重用性。这在大型系统中是比较有优点的。在下面的例子中,声明了一个displayRoute
方法,要求其参数具备三个特定的方法,经过Interface
对象和ensureImplements
方法来保证这三个方法的实现,不然将会抛出错误。工具
1 //声明一个接口,描述该接口包含的方法 2 var DynamicMap=new Interface{'DynamicMap',['centerOnPoint','zoom','draw']}; 3 4 //声明一个displayRoute方法 5 function displayRoute(mapInstance){ 6 //检验该方法的map 7 //检验该方法的mapInsstance是否实现了DynamicMap接口,若是未实现则会抛出 8 Interface.ensureImplements(mapInstance,DynamicMap); 9 //若是实现了则正常执行 10 mapInstance.centerOnPoint(12,22); 11 mapInstance.zoom(5); 12 mapInstance.draw(); 13 }
下面的例子会将一些数据以网页的形式展示出来,这个类的构造器以一个TestResult的实例做为参数。该类会对TestResult
对象所包含的数据进行格式化(Format)后输出,代码以下:性能
1 var ResultFormatter=function(resultObject){ 2 //对resultObject进行检查,保证是TestResult的实例 3 if(!(resultObject instanceof TestResult)){ 4 throw new Error('arguments error!'); 5 } 6 this.resultObject=resultObject; 7 } 8 9 ResultFormatter.prototype.renderResult=function(){ 10 var dateOfTest=this.resultObject.getData(); 11 var resultArray=this.resultObject.getResults(); 12 var resultContainer=document.createElement('div'); 13 var resultHeader=document.createElement('h3'); 14 resultHeader.innerHTML='Test Result from '+dateOfTest.toUTCString(); 15 resultContainer.appendChild(resultHeader); 16 17 var resultList=document.createElement('ul'); 18 resultContainer.appendChild(resultList); 19 20 for(var i=0;len=resultArray.length;i<len;i++){ 21 var listItem=document.createElement('li'); 22 listItem.innerHTML=resultArray[i]; 23 resultList.appendChild('listItem'); 24 } 25 return resultContainer; 26 } 27 28
该类的构造器会对参数进行检查,以确保其的确为TestResult
的类的实例。若是参数达不到要求,构造器将会抛出一个错误。有了这样的保证,在编写renderResult
方法的时候,就能够认定有getData
和getResult
两个方法。可是,构造函数中,只对参数的类型进行了检查,实际上这并不能保证所须要的方法都获得了实现。TestResult
类会被修改,导致其失去这两个方法,可是构造器中的检查依旧会经过,只是renderResult
方法再也不有效。
此外,构造器中的这个检查施加了一些没必要要的限制。它不容许使用其余的类的实例做为参数,不然会直接抛错,可是问题来了,若是有另外一个类也包含并实现了getData
和getResult
方法,它原本能够被ResultFormatter
使用,却由于这个限制而无用武之地。
解决问题的办法就是删除构造器中的校验,并使用接口代替。咱们采用这个方案对代码进行优化:
1 //接口的声明 2 var resultSet =new Interface('ResultSet',['getData','getResult']); 3 4 //修改后的方案 5 var ResultFormatter =function(resultObject){ 6 Interface.ensureImplements(resultObject,resultSet); 7 this.resultObject=resultObject; 8 }
上述代码中,renderResult
方法保持不变,而构造器却采用的ensureImplements
方法,而不是typeof
运算符。如今的这个构造器能够接受任何符合接口的类的实例了。
<1>工厂模式:对象工厂所建立的具体对象会因具体状况而不一样。使用接口能够确保所建立的这些对象能够互换使用,也就是说对象工厂能够保证其生产出来的对象都实现了必需的方法;
<2>组合模式:若是不使用接口就不可能使用这个模式,其中心思想是能够将对象群体与其组成对象同等对待。这是经过接口来作到的。若是不进行鸭式辩型或类型检查,那么组合模式就会失去大部分意义;
<3>装饰者模式:装饰者经过透明地为另外一个对象提供包装而发挥做用。这是经过实现与另外那个对象彻底一致的接口实现的。对于外界而言,一个装饰者和它所包装的对象看不出有什么区别,因此使用Interface来确保所建立的装饰者实现了必需的方法;
<4>命令模式:代码中全部的命令对象都有实现同一批方法(如run、ecxute、do等)经过使用接口,未执行这些命令对象而建立的类能够没必要知道这些对象具体是什么,只要知道他们都正确地实现了接口便可。借此能够建立出模块化程度很高的、耦合度很低的API。
做者:悠扬的牧笛
博客地址:http://www.cnblogs.com/xhb-bky-blog/p/5887242.html
声明:本博客原创文字只表明本人工做中在某一时间内总结的观点或结论,与本人所在单位没有直接利益关系。非商业,未受权贴子请以现状保留,转载时必须保留此段声明,且在文章页面明显位置给出原文链接。