接口

接口是面向对象JavaScript程序员的工具箱中最有用的工具之一。问题在于,JavaScript中没有内置的创建或实现接口的方法。它也没有内置的方法可以用于判断一个对象是否实现了与另一个对象相同的一套方法,这使对象很难互换使用。好在JavaScript有着出色的灵活性,因此添加这些特性并非难事。
本章将考察其他面向对象的语言中实现接口的方法,并对它们在这方面最突出的特性进行模仿。

2.1 什么是接口

接口提供了一种用以说明对象应该具有哪些方法的手段。
尽管它可以表明这些方法的语义,但它并不规定这些方法应该如何实现。例如,如果一个接口包含有一个setName的方法,那么你有理由认为这个方法的实现应该具有一个字符串参数,并且会把这个参数赋给一个name变量。

2.1.1 接口之利

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

2.1.1 接口之弊

  • 得益于JavaScript的弱类型的特点,JavaScript是一种具有极强表现力的语言,而接口的使用则在一定程度上强化了类型的作用,这降低了语言的灵活性。
  • JavaScript并没有提供对接口的内置支持,而试图模仿其他语言内置的功能总会有一些风险。JavaScript中没有Interface这个关键词,因此不管用什么方法实现接口,它总是与C++、Java中所使用的方法大相径庭,这加大了初涉JavaScript时所遇到的困难。
  • JavaScript中任何实现接口的方法都会对性能造成一些影响,在某种程度上这得归咎于额外的方法调用的开销。
  • 接口使用中的最大问题在于,无法强迫其他程序员遵守你定义的接口。在其他语言中,接口的概念是内置的,如果某人定义了实现一个接口的类,那么编译器会确保该类的确实现了这个接口。而在Javascript中必须用手工的方法保证某个类实现了一个接口。编码规范和辅助类可以提供一些帮助,但无法彻底根除这个问题。如果项目的其他程序员不认真对待接口,那么这些接口的使用是无法得到强制性保证的。除非项目的所有人都同意使用接口并对其进行检查,否则接口的很多价值都无从体现。

2.2 其他面向对象语言处理接口的方式

Java中的接口

public interface DataOutput {
	void writeBoolean(boolean value) throws IOException;
	void writeByte(int value) throws IOException;
	void writeChar(int value) throws IOException;
	void writeShort(int value) throws IOException;
	void writeInt(int value) throws IOException;
	...
}

它列出了一个类应该实现的一批方法,包括方法的参数和可能会抛出的异常。每一行都像是一个方法声明,只不过是以一个分号而不是一对大括号结尾的。
创建一个实现这个接口的类需要使用关键字implements:

public class DataOutputStream extends FilterOutputStream implements DataOutput {
	public final void writeBoolean (boolean value) throws IOException {
		write (value ? 1 : 0;
	}
	...
}

该类声明并具体实现了接口中列出的每一个方法。漏掉任何一个方法都会导致在编译时显示错误。

PHP中的接口

interface MyInterface {
	public function interfaceMethod($argumentOne, $argumentTwo);
}

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

class BadClass implements MyInterface {
	// No method declarations
}
// BadClass causes this error at run-time:
// Fatal error: Class BadClass contains 1 abstract methods and must therefore
// be declared abstract (MyInterface::interfaceMethod)

C#中的接口

interface MyInterface {
	string interfaceMethod (string argumentOne, string argumentTwo);
}

class MyClass : MyInterface {
	public string interfaceMethod(string argumentOne, string argumentTwo) {
		return argumentOne +argumentTwo;
	}
}

class BadClass : MyInterface {
	// No method declarations.
}

// BadClass causes this error at compile-time:
// BadClass does not implement interface member MyInterface.interfaceMethod()

上述语言使用接口的方式大体相似。

  • 接口结构包含的信息说明了需要实现什么方法以及这些方法应该具有什么参数。
  • 类的定义明确地声明了它们实现了这个接口(通常是使用implements关键字)
  • 一个类可以实现不止一个接口。
  • 如果接口中的某个方法没有被实现,则会产生一个错误。

2.3 在JavaScript中模仿接口

显然我们不能如法炮制,因为JavaScript没有interface和implements关键字,也不在运行时对接口约定是否得到遵守进行检查。但是我们可以通过使用辅助类和显式的进行检查模仿出它们的大部分特性。

JavaScript中模仿接口的三种方法:注释法、属性检查法、鸭式辨型法
没有哪种技术是完美的,但三者结合使用基本上可以令人满意。

2.3.1 用注释描述接口

这是最简单的方法,但效果却是最差的。

/*
interface Composite {
	function add(child);
	function remove(child);
	function getChild(index);
}

interface FormItem {
	function save();
}
*/

var CompositeForm = function (id, method, action) {//implements Composit,FormItem
	...
}

// Implement the Composite interface.

CompositeForm.prototype.add = function (child) {
	...
};
CompositeForm.prototype.remove = function (child) {
	...
};
CompositeForm.prototype.getChild = function (child) {
	...
};

//Implement the FormItem interface.
CompositeForm.prototype.save = function () {
	...
};

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

2.3.2 用属性检查模仿接口

这种方法,接口自身仍然只是注释,但所有类都明确地声明自己实现了哪些接口,那些想与这些类打交道的对象可以针对这些声明进行检查,你可以通过检查一个属性得知某个类自称实现了什么接口。

/*
// 对类所实现的接口提供文档说明
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) {
	// formInstance,要检查的对象
	if(!implements(formInstance, 'Composite', 'FormItem')) {
		throw new Error("Object does not implement a required interface.");
	}
	...
}

// implements方法检查object声明是否实现了要求的接口
function implements(object) {
	for(var i=1;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;
		}
	}
	return true;
}

缺点:你只知道它是否说自己实现了接口,并未确保类真正实现了自称实现的接口。

2.3.3 用鸭式辨型模仿接口

这个名称来自James Whitcomb Riley的名言:“像鸭子一样走路并且嘎嘎叫的就是鸭子”。怎么用这个名言来理解模仿接口呢?
其实,类是否声明自己支持哪些接口并不重要,只要他具有这些接口中的方法就行。而“属性检查”方法是“像鸭子一样走路”,它有没有嘎嘎叫我们不知道,所以不能称为鸭子。
基于这样的认识,把对象实现的方法集作为判断它是不是某个类的实例的唯一标准。

// Interfaces
var Composite = new Interface('Composite', ['add', 'remove', 'getChild']);
var FormItem = new Interface('Composite', ['save']);

// CompositeForm class
var CompositeForm = function(id, method, action) {
	...
};

...

function addForm(formInstance) {
	// formInstance,要检查的对象
	ensureImplements(formInstance,Composite,FormItem);
	// 如果接口所需的方法没有实现的话,这个方法将抛出错误
	...
}

在这种方法中,类并不声明自己实现了哪些接口,这降低了代码的可重用性,并且也缺乏其他两种方法那样的自我描述性。它需要一个辅助类(Interface)和一个辅助函数(ensureImplements)。而且它只关心方法的名称,并不检查其参数的名称、数目或类型。

2.4 本书采用的接口实现方式

综合使用“注释法”和“鸭式辨型法”。用注释声明类支持的接口,从而提高代码的可重用性及其文档的完善性。我们还用辅助类Interface及其类方法Interface.ensureImplements来对对象实现的方法进行显式检查。

// Interfaces
var Composite = new Interface('Composite', ['add', 'remove', 'getChild']);
var FormItem = new Interface('Composite', ['save']);

// CompositeForm class
var CompositeForm = function(id, method, action) {//implements Composite,FormItem
	...
};

...

function addForm(formInstance) {
	// formInstance,要检查的对象
	Interface.ensureImplements(formInstance,Composite,FormItem);
	// 如果接口所需的方法没有实现的话,这个方法将抛出错误
	// 此错误要么被其他代码捕捉,要么中断程序的执行。
	...
}

2.5 Interface类

下面是本书中Interface类的定义:

// Constructor

var Interface = function(name, methods) {
	if(arguments.length != 2) {
		throw new Error("Interface constructor called with " + arguments.length + "arguments, but expected exactly 2.");
	}
	this.name = name;
	this.method = [];
	for(var i = 0, len = methods.length; i < len; i++) {
		if(typeof methods[i] !== 'string') {
			throw new Error("Interface constructor expects 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("Function Interface.ensureImplements called with " 
		+ arguments.length + "arguments, but expected 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, methodsLen = interface.methods.length; j< methodsLen;j++) {
			var method = interface.methods[j];
			if(!object[method] || typeof object[method]!=='function') {
				throw new Error("Function Interface.ensureImplements:" 
				+ "object does not implement the "+ interface.name+ 
				+ " interface.Method "+ method + " was not found.")
			}
		}
	} 
};

该类的所有方法对其参数都有严格的要求,如果参数未能通过检查,将导致错误的抛出。我们特地加入这种检查的目的在于,如果没有错误被抛出,那么你可以肯定接口已经得到了正确的声明和实现。

2.5.1 Interface类的使用场合

接口在运用设计模式实现复杂系统的时候最能体现其价值。它看似降低了JavaScript的灵活性,实际上,因为使用接口可以降低对象间的耦合程度,所以它提高了代码的灵活性。接口的使用可以让函数变得更灵活,因为你既能向函数传递任何类型的参数,又能保证它只会使用那些具有必要方法的对象。
在有许多程序员参与的大型项目中,接口起着至关重要的作用。程序员常常需要使用还未编写出来的API,或者需要提供一些占位代码以免延误开发进度。接口在这种场合中的重要性表现在许多方面。它们记载着API,可作为程序员正式交流的工具。
现在项目中用到来自因特网上的,你无法直接控制的代码的情况越来越普遍。部署在外部环境中的程序库以及搜索、电子邮件、地图等服务的API都是这类代码的例子。即使它们有着可信的来源,也必须谨慎使用,确保其变化不会在自己的代码中引起问题。下面是一种应对之策,为所依赖的每一个API创建一个Interface对象,然后对接收到的每一个对象都进行检查,以确保其正确实现了那些接口:

var DynamicMap = new Interface('DynamicMap', ['centerOnPoint', 'zoom', 'draw']);

function displayRoute(mapInstance){
	Interface.ensureImplements(mapInstance, DynamicMap);
	mapInstance.centerOnPoint(12, 34);
	mapInstance.zoom(5);
	mapInstance.draw();
}

displayRoute函数要求传入的参数具有3个特定方法。通过使用一个Interface对象和调用Interface.ensureImplements方法,可以确保这些方法已经得到了实现,否则你将见到一个错误。

2.5.2 Interface类的用法

判断在代码中使用接口是否划算是最重要的一步,也是最困难的一步。对于小型的、不太费事的项目来说,接口的好处或许并不明显,只是徒增其复杂度而已。你需要自行权衡其利弊。如果认为在项目中使用接口利大于弊,那么可以参照如下使用说明。

  1. 将Interface类纳入HTML文件。
  2. 逐一检查代码中所有以对象为参数的方法。搞清代码的正常运转要求这些对象参数具有哪些方法。
  3. 为你需要的每一个不同的方法集创建一个Interface对象。
  4. 剔除所有针对构造器的显式检查。因为我们使用的是鸭式辨型,所以对象的类型不再重要。
  5. 以Interface.ensureImplements取代原来的构造器检查。

这样做有什么好处?
现在你不再依赖于任何特定的类的实例,而是检查所需的特性是否都已就绪,代码的耦合程度降低了。由此你在对代码进行优化和重构时将拥有更大的自由。

2.5.3 示例:使用Interface类

现在要创建一个类,它可以将一些自动化测试结果转化为适于在网页上查看的格式。该类的构造器以一个TestResult类的实例为参数。它会应客户的请求对这个TestResult对象所封装的数据进行格式化,然后输出。这个ResultFormatter类最初的实现如下:

// ResultFormatter class, before we implement interface checking.
var ResultFormatter = function(resultsObject) {
	if(!(resultsObject instanceOf TestResult)){
		throw new Error("ResultsFormatter: constructor requires an instance "
		+"of TestResult as an 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 = 'Test Results from '+ dateOfTest.toUTCString();
	resultsContainer.appendChild(resultsHeader);

	var resultsList = document.createElement('ul');
	resultsContainer.appendChild(resultsList);

	for(var i=0, len = resultsArray.length;i<len;i++) {
		var listItem = document.createElement('li');
		listItem.innerHTML =  resultsArray[i];
		resultsList.appendChild(listItem);
	}
	return resultsContainer;
}

代码解读:

  • 该类的构造器会对参数resultsObject进行检查,以确保其的确为TestResult类的实例,但实际上并不能保证所需的方法getDate、getResults得到了实现
  • TestResult类可能修改,致其不再拥有getDate方法,在此情况下,构造器中的检查仍能通过,但renderResults方法却会失灵
  • 此外,构造器中的这个检查施加了一些不必要的限制。它不允许使用其他类的实例作为参数,哪怕它们原本可以如愿发挥作用。

如何解决上述问题呢?答案是使用接口

// ResultSet Interface
var ResultSet = new Interface('ResultSet', ['getDate', 'getResults']);

var ResultFormatter = function(resultsObject) {
	// 用接口检查替代instanceOf检查
	Interface.ensureImplements(resultObject, ResultSet);
	this.resultsObject = resultsObject;
};

ResultFormatter.prototype.renderResults = function() {
	...
};

2.6 依赖于接口的设计模式

下面列出的设计模式尤其依赖接口,目前做一个了解即可,后面章节会详细介绍

  • 工厂模式 。对象工厂所创建的具体对象会因具体情况而异。使用接口可以确保所创建的这些对象可以互换使用。也就是说,对象工厂可以保证其生产出来的对象都实现了必须的方法。
  • 组合模式。其中心思想在于可以将对象群体与其组成对象同等对待。这是通过让他们实现同样的接口来做到的。如果不进行某种形式的鸭式辨型或类型检查,组合模式就会失去大部分作用。
  • 装饰者模式。装饰者通过透明地为另一对象提供包装而发挥作用。这是通过实现与另外那个对象完全相同的接口而做到的。对外界而言,一个装饰者和它所包装的对象看不出有什么区别。我们将使用Interface类来确保所创建的装饰者对象实现了必需的方法。
  • 命令模式。代码中所有的命令对象都要实现同一批方法(它们通常被命名为execute、run或undo)。通过使用接口,你为执行这些命令对象而创建的类可以不必知道这些对象具体是什么,只要知道它们都实现了正确的接口即可。藉此你可以创建出模块化程度很高而耦合度程度很低的用户界面和API。

2.7 小结

本章说明了接口概念的各种不同实现方式都有一些共同特性:它们都提供一种规定必需方法的手段、它们都提供一种检查这些方法是否确实得到了实现的手段,并且在结果为否定的时候能提供有用的错误信息。
在JavaScript中可以结合使用注释、辅助类和鸭式辨型来模仿这些特性。
使用接口的难点在于判断是否有必要使用它,它并不总是不可或缺的。
灵活性是JavaScript最强大的特色之一,强制进行不必要的严格类型检查会损害这种灵活性,谨慎使用Interface类有助于创建更健壮的类和更稳定的代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值