ホームページ > 記事 > ウェブフロントエンド > JavaScript のインターフェース コードの詳細な説明に関する簡単な説明
インターフェイスは、オブジェクト指向 JavaScript プログラマ のツールボックスの中で最も便利なツールの 1 つです。 デザインパターンで提案されている再利用可能なオブジェクト指向設計の原則の一つが「実装プログラミングではなくインターフェースのためのプログラミング」であり、いわゆるインターフェース指向プログラミングであることがわかります。しかし、問題は、JavaScript の世界には、インターフェイスを作成または実装するための組み込みメソッドがなく、オブジェクトが別のオブジェクトと同じように実装されているかどうかを判断できる一連のメソッドがないため、使用が困難であることです。幸いなことに、JavaScript には優れた柔軟性があるため、従来のオブジェクト指向インターフェイスを簡単にシミュレートし、これらの機能を追加できます。インターフェイスは、オブジェクトが持つべきメソッドを記述する手段を提供しますが、これらのメソッドの意味を示すことはできますが、特定の実装は含まれません。このツールを使用すると、オブジェクトが提供するプロパティによってオブジェクトをグループ化できます。たとえば、A と B およびインターフェイス I の場合、A オブジェクトと B オブジェクトが大きく異なっていても、両方とも I インターフェイスを実装している限り、A と B は A.I(B) メソッドで同じ意味で使用できます。 B.I(A)など。インターフェイスを使用して、異なるクラス間の共通性を高めることもできます。もともとパラメータとして特定のクラスを必要とする関数が、パラメータとして特定のインターフェイスを必要とする関数に変更された場合、そのインターフェイスを実装するすべてのオブジェクトをパラメータとして渡すことができるため、関連しないすべてのオブジェクトがそのインターフェイスに渡されます。相互にオブジェクトをパラメータとして渡すこともできます。
確立されたインターフェースは自己記述的であり、どのメソッドを実装する必要があるかを外部クラスに伝えるための一種の情報を提供できます。また、異なるクラス間の通信方法が安定し、2 つのオブジェクトを継承する過程で発生する問題が軽減されます。これは、JavaScript のような弱い型指定の言語では、インターフェイスの使用時に型の不一致を追跡するのが難しく、問題が発生した場合に明確なエラー メッセージが表示されます。もちろん、インターフェイスに欠点がないわけではありません。インターフェイスを多用すると、弱い型付け言語としての柔軟性がある程度低下します。一方、JavaScript にはインターフェイスのサポートが組み込まれておらず、従来のオブジェクトをシミュレートするだけです。これにより、本質的に柔軟性のある JavaScript の制御がより困難になります。さらに、インターフェイスをどのような方法で実装しても、追加のメソッド呼び出しのオーバーヘッドが部分的に原因となり、パフォーマンスに影響します。インターフェイスを使用する場合の最大の問題は、他の厳密に型指定された言語とは異なり、JavaScript がインターフェイスの規約に準拠していない場合、そのインターフェイスの柔軟性により上記の問題を効果的に回避できることです。エラーを引き起こさずに破損する可能性が非常に高く、制御できません。
オブジェクト指向言語では、インターフェイスの使用方法は一般的に似ています。インターフェイスに含まれる情報には、クラスが実装する必要があるメソッドと、これらのメソッドのシグネチャが記述されています。クラス定義では、これらのインターフェイスを実装することを明示的に記述する必要があります。そうしないと、クラスはコンパイルされません。明らかに、JavaScript では同じことを行うことはできません。インターフェイスや実装キーワードがなく、インターフェイスがコントラクトに従っているかどうか実行時にチェックされないためです。しかし、補助メソッドと明示的なチェックを通じて、その機能のほとんどを模倣することができます。
JavaScript でインターフェースを模倣するには、コメント、属性チェック、ダックタイプの引数を使用するという 3 つの主な方法があります。上記の 3 つの方法を効果的に組み合わせることで、インターフェースのような効果が得られます。
アノテーションは、インターフェースに関連するキーワード (インターフェース、実装など) をアノテーション内の JavaScript コードと一緒に配置してインターフェースをシミュレートする比較的直感的な方法です。これは最も単純な方法ですが、最悪の効果があります。コードは次のとおりです:
//以注释的形式模仿描述接口 /* interface Composite{ function add(child); function remove(child); function getName(index); } interface FormItem{ function save(); } */ //以注释的形式模仿使用接口关键字 var CompositeForm =function(id , method,action) { //implements Composite , FormItem // do something } //模拟实现具体的接口方法 此处实现Composite接口 CompositeForm.prototype.Add=function(){ // do something } CompositeForm.prototype.remove=function(){ // do something } CompositeForm.prototype.getName=function(){ // do something } //模拟实现具体的接口方法 此处实现FormItem接口 Composite.prototype.save=function(){ // do something }
この種の模倣は文書仕様の範囲内に留まるため、開発者がこの合意を完全に遵守するかどうかはまだ検討の余地があるため、この方法は実際にはあまり良くありません。開発者の意識について。さらに、このメソッドでは、関数が合意した「インターフェイス」を実際に実装しているかどうかはチェックされません。それにもかかわらず、このアプローチには追加のクラスや関数を必要とせずに実装が容易であるという利点もあり、クラスによって実装されるインターフェイスには注釈が付けられるため、コードの再利用性が向上します。コメント化されたコードはデプロイメント中に簡単に削除できるため、このアプローチはファイルが占めるスペースや実行速度には影響しません。ただし、エラー メッセージは表示されないため、テストやデバッグにはあまり役に立ちません。次のメソッドは、インターフェイスが実装されているかどうかを確認します。コードは次のとおりです。
//以注释的形式模仿使用接口关键字 var CompositeForm =function(id , method,action) { //implements Composite , FormItem // do something this.implementsinterfaces=['Composite','FormItem']; //显式地把接口放在implementsinterfaces中 } //检查接口是否实现 function implements(Object){ for(var i=0 ;i< arguments.length;i++){ var interfaceName=arguments[i]; var interfaceFound=false; for(var j=0;j<Object.implementsinterfaces.length;j++){ if(Object.implementsinterfaces[j]==interfaceName){ interfaceFound=true; break; } } if(!interfaceFound){ return false; }else{ return true; } } } function AddForm(formInstance){ if(!implements(formInstance,'Composite','FormItem')){ throw new Error('Object does not implements required interface!'); } }
上述代码是在方式一的基础上进行完善,在这个例子中,CompositeForm宣称自己实现了Composite和FormItem这两个接口,其做法是把这两个接口的名称加入一个implementsinterfaces的数组。显式地声明自己支持什么接口。任何一个要求其参数属性为特定类型的函数都可以对这个属性进行检查,并在所需要的接口未在声明之中时抛出错误。这种方式相对于上一种方式,多了一个强制性的类型检查。但是这种方法的缺点在于它并未保证类真正地实现了自称实现的接口,只是知道它声明自己实现了这些接口。其实类是否声明自己支持哪些接口并不重要,只要它具有这些接口中的方法就行。鸭式辩型(像鸭子一样走路并且嘎嘎叫的就是鸭子)正是基于这样的认识,它把对象实现的方法集作为判断它是不是某个类的实例的唯一标准。这种技术在检查一个类是否实现了某个接口时也可以大显身手。这种方法的背后观点很简单:如果对象具有与接口定义的方法同名的所有方法,那么就可以认为它实现了这个接口。可以使用一个辅助函数来确保对象具有所有必需的方法,代码如下:
//interface var Composite =new Interface('Composite',['add','remove','getName']); var FormItem=new Interface('FormItem',['save']); //class var Composite=function(id,method,action){ } //Common Method function AddForm(formInstance){ ensureImplements(formInstance,Composite,FormItem); //如果该函数没有实现指定的接口,这个函数将会报错 }
与另外两种方式不同,这种方式无需注释,其余的各个方面都是可以强制实施的。EnsureImplements函数需要至少两个参数。第一个参数是想要检查的对象,其余的参数是被检查对象的接口。该函数检查器第一个参数代表的对象是否实现了那些接口所声明的方法,如果漏掉了任何一个,就会抛错,其中会包含被遗漏的方法的有效信息。这种方式不具备自我描述性,需要一个辅助类和辅助函数来帮助实现接口检查,而且它只关心方法名称,并不检查参数的名称、数目或类型。
在下面的代码中,对Interface类的所有方法的参数都进行了严格的控制,如果参数没有验证通过,那么就会抛出异常。加入这种检查的目的就是,如果在执行过程中没有抛出异常,那么就可以肯定接口得到了正确的声明和实现。
var Interface = function(name ,methods){ if(arguments.length!=2){ throw new Error('2 arguments required!'); } this.name=name; this.methods=[]; for(var i=0;len=methods.length;i<len;i++){ if(typeof(methods[i]!=='String')){ throw new Error('method name must be String!'); } this.methods.push(methods[i]); } } Interface.ensureImplements=function(object){ if(arguments.length<2){ throw new Error('2 arguments required at least!'); } for(var i=0;len=arguments.length;i<len;i++){ var interface=arguments[i]; if(interface.constructor!==Interface){ throw new Error('instance must be Interface!'); } for(var j=0;methodLength=interface.methods.length;j<methodLength;j++){ var method=interface.methods[j]; if(!object[method]||typeof(object[method])=='function')){ throw new Error('object does not implements method!'); } } } }
其实多数情况下,接口并不是经常被使用的,严格的类型检查并不总是明智的。但是在设计复杂的系统的时候,接口的作用就体现出来了,这看似降低了灵活性,却同时也降低了耦合性,提高了代码的重用性。这在大型系统中是比较有优势的。在下面的例子中,声明了一个displayRoute方法,要求其参数具有三个特定的方法,通过Interface对象和ensureImplements方法来保证这三个方法的实现,否则将会抛出错误。
//声明一个接口,描述该接口包含的方法 var DynamicMap=new Interface{'DynamicMap',['centerOnPoint','zoom','draw']}; //声明一个displayRoute方法 function displayRoute(mapInstance){ //检验该方法的map //检验该方法的mapInsstance是否实现了DynamicMap接口,如果未实现则会抛出 Interface.ensureImplements(mapInstance,DynamicMap); //如果实现了则正常执行 mapInstance.centerOnPoint(12,22); mapInstance.zoom(5); mapInstance.draw(); }
下面的例子会将一些数据以网页的形式展现出来,这个类的构造器以一个TestResult的实例作为参数。该类会对TestResult对象所包含的数据进行格式化(Format)后输出,代码如下:
var ResultFormatter=function(resultObject){ //对resultObject进行检查,保证是TestResult的实例 if(!(resultObject instanceof TestResult)){ throw new Error('arguments error!'); } this.resultObject=resultObject; } ResultFormatter.prototype.renderResult=function(){ var dateOfTest=this.resultObject.getData(); var resultArray=this.resultObject.getResults(); var resultContainer=document.createElement('p'); var resultHeader=document.createElement('h3'); resultHeader.innerHTML='Test Result from '+dateOfTest.toUTCString(); resultContainer.appendChild(resultHeader); var resultList=document.createElement('ul'); resultContainer.appendChild(resultList); for(var i=0;len=resultArray.length;i<len;i++){ var listItem=document.createElement('li'); listItem.innerHTML=resultArray[i]; resultList.appendChild('listItem'); } return resultContainer; }
该类的构造器会对参数进行检查,以确保其的确为TestResult的类的实例。如果参数达不到要求,构造器将会抛出一个错误。有了这样的保证,在编写renderResult方法的时候,就可以认定有getData和getResult两个方法。但是,构造函数中,只对参数的类型进行了检查,实际上这并不能保证所需要的方法都得到了实现。TestResult类会被修改,致使其失去这两个方法,但是构造器中的检查依旧会通过,只是renderResult方法不再有效。
此外,构造器中的这个检查施加了一些不必要的限制。它不允许使用其他的类的实例作为参数,否则会直接抛错,但是问题来了,如果有另一个类也包含并实现了getData和getResult方法,它本来可以被ResultFormatter使用,却因为这个限制而无用武之地。
解决问题的办法就是删除构造器中的校验,并使用接口代替。我们采用这个方案对代码进行优化:
//接口的声明 var resultSet =new Interface('ResultSet',['getData','getResult']); //修改后的方案 var ResultFormatter =function(resultObject){ Interface.ensureImplements(resultObject,resultSet); this.resultObject=resultObject; }
上述代码中,renderResult方法保持不变,而构造器却采用的ensureImplements方法,而不是typeof运算符。现在的这个构造器可以接受任何符合接口的类的实例了。
f35d6e602fd7d0f0edfa6f7d103c1b57工厂模式:对象工厂所创建的具体对象会因具体情况而不同。使用接口可以确保所创建的这些对象可以互换使用,也就是说对象工厂可以保证其生产出来的对象都实现了必需的方法;
2cc198a1d5eb0d3eb508d858c9f5cbdb组合模式:如果不使用接口就不可能使用这个模式,其中心思想是可以将对象群体与其组成对象同等对待。这是通过接口来做到的。如果不进行鸭式辩型或类型检查,那么组合模式就会失去大部分意义;
5bdf4c78156c7953567bb5a0aef2fc53デコレータ パターン: デコレータは、別のオブジェクトのラッパーを透過的に提供することによって機能します。これは、他のオブジェクトのインターフェイスとまったく同じインターフェイスを実装することによって実現されます。外部から見ると、デコレータとそれがラップするオブジェクトの間に違いはありません。したがって、インターフェイスを使用して、作成されたデコレータが必要なメソッドを実装していることを確認します
<4> コマンド モード: コード内では、すべてのコマンド オブジェクトが実装されます。インターフェイスを使用することにより、これらのコマンド オブジェクトを実行せずに作成されたクラスは、それらが正しく実装されていることがわかっている限り、これらのオブジェクトが何であるかを知る必要がありません。それがインターフェースです。これにより、高度なモジュール性と結合度の低い API を作成できます。
上記は JavaScript のインターフェイス コードの簡単な説明です。その他の関連コンテンツについては、PHP 中国語 Web サイト (www.php.cn) に注目してください。