JavaScript设计模式之接口

本文摘自《JavaScript设计模式》第二章 接口。


什么是接口
接口是提供了一种用以说明一个对象应该具有哪些方法的手段。尽管它可以表明这些方法的语义,但它并不规定这些方法应该如何实现。例如,如果一个接口包含有一个名为setName的方法,那么你有理由认为这个方法的实现应该具有一个字符串参数,并且会把这个参数赋给一个name变量。有了这个工具,你就能按对象提供的特性对它们进行分组。例如,即使一批对象彼此存在着极大的差异,只要它们都实现了Comparable接口,那么在object.compare(anotherObject)方法中就可以互换使用这些对象。你还可以使用接口开发不同的类之间的共同性。如果把原本要求以一个特定的类为参数的函数改为要求以一个特定的接口为参数的函数,那么任何实现了该接口的对象都可以作为参数传递给。这样一来,彼此不相关的对象也可以被同等对待。


接口之利
在面向对象的JavaScript中,接口有些什么作用呢?既定的一批接口具有自我描述性,并能促进代码的重用。接口可以告诉程序员一个类实现了哪些方法,从而帮助其使用这个类。如果你熟悉一个特定的接口,那么就已经知道如何使用任何实现了它的类,从而更有可能重用现有的类。接口还有助于稳定不同类之前的通信方式。如果事先知道了接口,你就能减少在集成两个对象的过程中出现的问题。借助它,你可以事先就说明你希望一个类具有哪些特性和操作。一个程序可以针对所需要的类定义一个接口,并把它转交给另一个程序。第二个程序员可以随心所欲地编写自己的代码,只要他定义的类实现了那个接口就行。这在大型项目中尤其有用。在大型项目中尤其有用。
测试和调式因此也能变得更轻松。在javascript这种弱类型语言中,类型不匹配错误很难跟踪。使用接口可以让这种错误的查找变午更容易一点,因为此时如果一个对象不像所要求的类型,或者没有实现必要的方法,那么你会得到包含有用信息的明确的错误提示。这样一来,逻辑错误可以被限制在方法自身,而不是在对象构成之中。接口还能让代码变得更稳固,因为对接口的任何改变在所有实现它的类都必须体现出来。如果接口添加了一个操作,而某个实现它的类并没有相应的添加这个操作,那么你肯定会立即见到一个错误。


接口之弊
接口并没非没有缺点。javascript是一种具有极强表现图片的语言,这主要得益于其弱类型的特点。而接口的使用则一定程序上强化了类型的作用。这降低了语言的灵活性。
javascript并没有提供对接口的内置支持,而试图模仿其它语言内置的功能总会有一些风险。javascript没有interface这个关键词,因此不管你用什么方法实现接口,它总是与C++和Java这些语言的方法大相径庭,这加大了初涉javascript时所遇的困难。
javascript中任何实现接口的方法都会性能造成一些影响,在某种程序上这得归咎于额外的方法调用的开销。我们的实现方法中使用了两个for循环来遍历所需要的每个接口中的每个方法。对于大型接口和需要实现许多不同接口的对象,这种检查可能要花点时间,从而对性能造成负面影响。在乎这个问题,那么 可以开必完成之后剔除这种代码,或者将其执行与一个调试标志关联起来,这样在运营环境中它就会执行。但要注意不要过早进行优化处理。firebug这类性能分析器可以帮助你判断是否真有必要剔除接口代码。
js中接口使用的最大问题在于,无法强迫其他程序员遵守你定义的接口。在其它语言中,接口的概念是内置的,如果某人定义了实现一个接口的类,那么编译器会确保该类的确实现了这个接口。而在javascript中则必须用手工的办法保证某个类实现了一个接口。编码规范和辅助类可以提供一些帮助,但无法彻底根除这个问题。如果项目的其他程序员不认真对待接口,那么这些接口的使用是无法得到强制性保证的。除非项目的所有人都同意使用接口并对其进行检查,否则接口的很多价值都无从体现。

其他面向对象语言处理接口的方式
这里先概览一下三种广泛使用的面向对象语言处理接口的方式。你会发现它们的办法大体相似
java:java.io包中的接口
public interface DataOutput{
void writeBoolean(boolean value) throwsIOException;
void writeByte(int value) throwsIOException;
void writeChar(int value) throws IOException;
…..
}

它列出一个类应该实现的一批方法,包括方法的参数和可能会抛出的异常。每一行都像是一个方法声明,只不过是以一个分号而不是一对大括号结尾。
实现该接口的类
public class DataOutputStream extends FilterOutputStream implemets DataOutput{
public final void writeBoolean(boolean value) throwsIOException{
write(value?1:0);
}
….
}
这个类声明并具体实现了接口中列出的每一个方法。漏掉任何一个方法都会导致在编译时显示错误。


PHP
interface MyInterface{
public functioninterfaceMethod($argumentOne, $argumentTow);
}

class MyClass implements MyInterface{
public function interfaceMethod($argumentOne,$argumentTow){
return $argumentOne*$argumentTwo;
}
}

class BadClass implements MyInterface{
//no method declarations.
}
运行进会报错..
C#跟java类似


在javascript中模仿接口
javascript中模仿接口的三种方法:注释法、属性检查法和鸭式辨型法。没有哪种技术是完美的,但三者结合使用基本上可以令人满意。
用注释描述接口
用注释模仿接口是最简单的方法,但效果却是最差的。这种方法模仿其他页面对象语言中的做法,使用了interface和implements关键字,但把它们放在注释中,以免引起语法错误。如下:
/*
interface Composite{
function add(child);
function remove(child);
function getChild(index);
}

interface FormItem{
function save();
}
*/
var CompositeForm= function(id,method,action){
//implements Composite,FormItem
}
CompositeForm.prototype.add=function(child){

}
CompositeForm.prototype.remove=function(child){

}
CompositeForm.prototype.getChild=function(indx){

}

//Implement theFormItem interface.
CompositeForm.prototype.save=function(){

}

这种模仿并不是很好。它没有为确保CompositeForm真正实现了正确的方法集而进行检查,也不会抛出错误以告知程序员程序中的问题。说到底它主要还是属于程序文档范畴。在这种做法中,对接口约定的遵守完全依靠自觉。
尽管如此,这种方法也有其优点。它易于实现,不需要额外的类或函数。它可以提高代码的可重用性,因为现丰那些类实现的的接口都有说明,程序员可以把它们与其他实现了同样接口的类互换使用。这种方法并不影响文件尺寸或执行速度,因为它所用的注释可以在对代码进行部署时,不费吹灰之图片地予以剔除。但是,由于不会提供错误消息,它对测试和调式没有什么帮助。


用属性检查模仿接口
这种方法更严谨一点。所有类都明确地声明自己实现了哪些接口,那些想与这些类打交道的对象可能针对这些声明进行检查。那些接口自身仍然只是注释,但现在你可以通过检查一个属性得知某个类自称实现了什么接口
/*
interface Composite{
function add(child);
function remove(child);
function getChild(index);
}

interface FormItem{
function save();
}
*/
var CompositeForm= function(id,method,action){
this.implementsInterfaces = [‘Composite’,’FormItem’];

}

..
function addForm(formInstance){
if(!implements(formInstance,’Composite’,’FormItem’))
throw new Error(“Object does notimplement a required interface.”)
}

//下面的implements方法,是用来检查声明对象是否实现了需要的接口
function implements(object){
for(var i=1;i<argements.length;i++){
var interfaceName = arguments[i];
var interfaceFound =false;
for(varj=0;j<object.implementsInterfaces.length;j++){
if(object.implementsInterfaces[j]==interfaceName){
interfaceFound=true;
break;
}
}
if(!interfaceFound)return false;
}
return true; //all interfaces were found.
}

这个例子中,CompositeForm宣称自己实现了Composite和FormItem接口,其做法是把这两个接口名称加入一个名为implementsInterfaces的数组。类显式声明自己支持什么接口。任何一个要求基于参数属于特定类型的函数都可以对这个属性进行检查,并在所需接口未在声明之列时抛出一个错误。
这种方法有几个优点。它对类所实现的接口提供了文档说明。如果需要的接口不在一个类宣称支持的接口之列,你会看到错误消息。通过利用这些错误,你可以强迫其他程序员声明这些接口。
这种方法的主要缺点在于它并未确保类真正实现了自称实现的接口。你只知道它是否说自己实现了接口。在创建一个类时声明它实现了一个接口,但后来在实现该接口所规定的方法时却漏掉其中的某一个,这种错误很常见。此时所有检查都能通过,但那个方法却不存在,这将在代码中埋下一个隐患。另外显式声明类所支持的接口也需要一些额外的工作。


用鸭式辨型模仿接口
其实,类是否声明自己支持哪些接口并不重要,只要它具有这些接口中的方法就行。鸭式辨型(这个名称来自James Whitomb Riley的名言:“像鸭子一样走路并且嘎嘎叫的就是鸭子”)正是基于这样的认识。它把对象实现的方法集作作为判断它是不是某个类的实例的唯一标准。这种技术在检查一个类是否实现了某个接口时也可大显向身手。这种方法背后的观点很简单:如果对象具有与接口定义的方法同名的所有方法,那么就可以认为它实现了这个接口。你可以用一个辅助函数来确保对象具有所有必需的方法: //Interface.
var Composite =new Interface(“Composite”,[“add”,”remove”,”getChild”]);
var FormItem = new Interface(“FormItem”,[‘save’]);
//CompositeFormclass
var CompositeForm= function(id,method,action){
….
}
function addForm(formInstance){
ensureImplements(formInstance,Composite,FormItem);
//This function will throw an error if a required method is notimplemented.
}
与另外两种方法不同,这种方法并不借助注释。其各个方面都是可以强制实施的。ensureImplements函数需要至少两个参数。第一个参数是想要检查的对象。其余参数是据以对那个对象进行检查的接口。该函数检查其第一个参数代表的对象是否实现了那些接口所声明的所有方法。如果发现漏掉了任何一个方法,它就会抛出错误,其中包含了所缺少的那个方法和未被正确实现的接口的名称等有用信息。这种检查可以用在代码中任何需要确保某个对象实现了某个接口的地方。在本例中,addForm函数仅当一个表单对象支持所有必要的方法时才会对其执行添加操作。
尽管鸭式辨型可能是上述三种方法中最有用的一种,但它也有一些缺点。这种方法中,类并不声明自己实现了哪些接口,这降低了代码的可重用性,并且也缺乏其他两种方法那样的自我描述性。它需要使用一个辅助类Interface和一个辅助函数ensureImplements。而且,它只关心方法的名称,并不检查其参数的名称、数目或类型。



第一种和第三种结合
我们用注释声明类支持的接口,从而提高代码的可重用性及其文档的完善性。我们还用辅助类Interface及类方法Interface.ensureImplements来对对象实现的方法进行显示检查。如果对象未能通过检查,这个访求将返回一条有用的错误消息。

Interface类
var Interface =function(name,methods){
if(arguments.length!=2){
throw newError(“Interface constructor called width”+arguments.legnth+”arguments,butexpected exactly 2. ”);
}
this.name = name;
this.methods = [];
for(var i=1,len=methods.length;i<len;i++){
if(typeof methods[i] !==’string’){
throw new Error(“interface costructorexpects method names to be passed in as a string”);
}
this.methods.push(methods[i]);
}
}

//Static class method
Interface.ensureImplements =function(object){
if(arguments.length<2){
throw new Error("FunctionInterface.ensureImplements called with”+arguments.length+”arguments, but expeted at least 2.");
}
for(var i=1,len=arguments.length;i<len;i++){
var interface = arguments[i];
if(interface.constructor!==Interface){
throw new Error("Function Interface.ensureImplements expects arguments two and above to be instances of Interface.")
}
for(var j= 0,methodLen =interface.methods.length;j<methodLen;j++){
var method = interface.methods[j];
if(!object[method] || typeof object[method]!=='function'){
throw newError("Function Interface.ensureImplements:object does not implement the"+interface.name+"interface. Method"+method+" was not found.")
}
}
}
}
从中可以看到,该类所有方法对其参数都有严格的要求,如果参数未能通过检查,将导致错误的抛出。我们特地加入这种检查的目的在于:如果没有错误抛出,那么你可以肯定接口已经得到了正确的声明和实现。



Interface类的使用场合
严格的类型检查并不总是明智的。许多js程序员根本不用接口或它所提供的那种检查,也照样一干多年。接口在运用设计模式实现复杂系统的时候最能体现其价值。它看似降低javascript的灵活性,而实际上,因为使用接口可以降低对象间的耦合程度,所以它提高了代码的灵活性。接口可以让函数变得更灵活,因为你既能向函数传递任何类型的参数,又能保证它只会使用那些具有必要方法的对象。
Interface类的用法
判断代码中使用接口是否划算是最重要的一步。对于小型的、不太费事的项目来说,接口的好处也许并不明显,只是徒增其复杂度而已。你需要自行权衡其利弊。如果认为在项目中使用接口利大于弊,那么可以参照如下使用说明:
1、 将Interface类纳入HTML文件。
2、 逐一检查代码中所有以对象为参数的方法。搞清代码正常运转要求的这些对象参数具有哪些方法
3、 为你需要的每一个不同的方法集创建一个Interface对象。
4、 剔除所有针对构造器显式检查。因为我们使用是鸭式辨型,所以对象的类型不再重要。
5、 以Interface.ensureImplements取代原来的构造器检查。
示例
假设你要创建一个类,它可以将一些自动化测试结果转化为适于在网页上查看的格式。该类的构造器以一个TestResult类的实例为参数。它会应客户的请求对这个TestResult对象所封装的数据进行格式化,然后输出。
原始定义:
var ResultFormatter =function(resultsObject){
if(!(resultsObject instanceof TestResult)){
throw newError("ResultsFormatter:constructor requires an instance of TestResult asan argument.")
}
this.resultsObject = resultsObject;
}
ResultFormatter.prototype.renderResults =function(){
var dateOfTest = this.resultsObject.getDate();
var resultsArray =this.resultsObject.getResults();
var resultsContainer =document.createElement('div');
var resultsHeader =document.createElement("h3");
resultsHeader.innerHTML = "TestResults from "+dateOfTest.toUTCString();
resultsContainer.appendChild(resultsHeader);
var resultList =document.createElement("ul");
resultsContainer.appendChild(resultList);
for(var i=0,len=resultsArray.length;i<len;i++){
var listItem=document.createElement('li');
listItem.innerHTML =resultsArray[i];
resultList.appendChild(listItem);
}
return resultsContainer;
}

该类的构造器会对参数进行检查,以确保其的确为TestResult类的实例。如果参数达不到要示,构造器将抛出一个错误。有了这样的保证,在编写renderResults方法时,你就可以认定有getDate和getResults这两个方法可供使用。实际上这并不能保证所需要的方法得到了实现。TestResult类可能会被修改,致使其不再拥有getDate()方法。在此情况下,构造器中的检查仍能通过,但renderResults方法却会失灵。
此外,构造器的这个检查施加了一些不必要的限制。它不允许使用其他类的实例作为参数,哪怕它们原本可以如愿发挥作用。例如,有一个名为WeatherData在也拥有getDate和getResults这两个方法。它本来可以被ResultFormatter类用得好好的。但是那个显式类型检查会阻止使用WeatherData类的任何实例。
问题解决办法是删除那个使用instanceOf的检查,并用接口代替它。首先,我们需要创建这个接口:
//ResultSetInterface.
var ResultSet =new Interface(“ResultSet”,[‘getDate’,’getResults’]);
上面的这行代码创建了一个Interface对象的新实例。第一个参数是接口的名称,第二个参数是一个字符串数组,其中的每个字符串都是一个必需的方法名称。有了这个接口之后,就可以用接口检查替代instanceOf检查了
var ResultFormatter = function(resultsObject){
Interface.ensureImplements(resultsObject,ResultSet);
this.resultsObject = resultsObject;
}
ResultFormatter.prototype.renderResults= function(){

}
renderResults方法保持不变。而构造器则被改为使用ensureImplements方法而不是instanceof运算符。现在构造器可以接受WeatherData或其他任何实现所需要方法的类的实例。我们只修改了几行ResultFormatter类代码,就让那个检查变得更准确,而且更宽容。


依赖于接口的设计模式
l 工厂模式
l 组合模式
l 装饰模式
l 命令模式

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值