就足以說明介面在物件導向的領域中有多重要。但JS卻不像其他物件導向的進階語言(C#,Java,C++等)擁有內建的介面機制,以確定一組對象和另一組對象包含相似的的特性。所幸的是JS擁有強大的靈活性(我在上文已談過),這使得模仿介面特性又變得非常簡單。那麼到底是介面呢?
介面,為一些具有相似行為的類之間(可能為同一種類型,也可能為不同類型)提供統一的方法定義,使這些類之間能夠很好的實現通訊。
那使用介面到底有哪些好處呢?簡單地說,可提高系統相似模組的重用性,使得不同類的通訊更加穩固。一旦實現介面,則必須實現介面中所有的方法。對於大型的Web項目來說,使得多個複雜模組的相似功能模組,只需要提供介面便可以提供一個實現,而彼此之間不受到影響。但我們必須明確,介面也不是萬能的,由於JS是弱類型語言,你並不能強制其他的團隊成員嚴格遵循你所提供的介面,不過你可以使用代碼規範和輔助類來緩解這個問題。另外,對系統效能也會造成一定的影響,應根據你的系統需求複雜度而定。由於沒有內建的interface和implements關鍵字,下面我們來看看JS是如何模仿實現介面的。
1. 最簡單也是效果最差實現介面的方式是使用注釋。即在注釋中使用interface來說明介面的意圖。 複製代碼 代碼如下:/*
interface Composite {
function add(child);
function remove(child);
function getChild(index);
}
interface FormItem {
funtion save();
}
*/
var CompositeForm = function(id, name, action) {
// implements Composite, FormItem
}
CompositeForm.prototype = {
// implements Composite interface
add: function(child) {
//...
},
remove: function(child) {
//...
},
getChild: function(index) {
//...
}
// implements FormItem interface
save: function() {
//...
}
}
這並沒有很好的類比介面的功能和確保Composite類確實實現了方法的集合,也沒有拋出錯誤通知程式員問題所在,除了說明以外不起任何作用,所有的一致性都需要程式員自覺完成。但它容易實現,不需要額外的類或函數,不影響文檔的大小和執行速度,注釋也能很輕易的剝離,在一定程度上提高了重用性,因為提供了類的說明可以跟其他實現相同介面的類進行通訊。
2. 用屬性檢查類比介面。類顯示聲明了要實現的介面,通過屬性檢查是否實現了相應的介面。 複製代碼 代碼如下:/*
interface Composite {
function add(child);
function remove(child);
function getChild(index);
}
interface FormItem {
funtion save();
}
*/
var CompositeForm = function(id, name, action) {
this.implementsInterfaces = ["Composite", "FormItem"];
//...
}
function checkInterfaces(formInstance) {
if(!implements(formInstance, "Composite", "FormItem")) {
throw new Error("Object doesn't implement required interface.");
}
//...
}
//check to see if an instance object declares that it implements the required interface
function implements(instance) {
for(var i = 1; i < arguments.length; i++) {
var interfaceName = arguments[i];
var isFound = false;
for(var j = 0; j < instance.implementsInterfaces.length; j++) {
if(interfaceName == instance.implementsInterfaces[j]) {
isFound = true;
break;
}
}
if(!isFound) return false;// An interface was not found.
}
return true;// All interfaces were found.
}
在這裡發現,仍然添加了注釋來說明介面。不過在Composite類中添加了一個屬性implementsInterfaces,說明該類必須實現那些介面。通過檢查該屬性來判斷是否實現相應的介面,如果未實現就會拋出錯誤。但缺點在於你仍無法判斷是否真正實現了對應的介面方法,僅僅只是"自稱"實現了介面,同時也增加了相應的工作量。
3. 用"鴨式辨型"來實現介面。從屬性檢查實現中發現一個類是否支援所實現的介面無關緊要,只要介面中所有的方法出現在類中相應的地方,那足以說明已經實現介面了。就像"如果走路像鴨子,像鴨子嘎嘎的叫,不管它貼不貼標籤說自己是鴨子,那我們認為它就是鴨子"。利用輔助類來判斷一個類是否存在(實現)了相應介面中所有的方法,如果不存在則代表沒有實現。 複製代碼 代碼如下:// Interfaces
var Composite = new Interface("Composite", ["add", "remove", "getChild"]);
var FormItem = new Interface("FormItem", ["save"]);
var CompositeForm = function(id, name, action) {
// implements Composite, FormItem interfaces
}
function checkInterfaces(formInstance) {
Interface.ensureImplements(formInstance, "Composite", "FormItem");
//...
}
介面類 複製代碼 代碼如下:// Interface class is for checking if an instance object implements all methods of required interface
var Interface = function(name, methods) {
if(arguments.length != 2) {
throw new Error("Interface constructor expects 2 arguments, but exactly provided for " + arguments.length + " arguments.");
}
this.name = name;
this.methods = [];
for(var i = 0;i < methods.length; i++) {
if(typeof methods[i] != "string") {
throw new Error("Interface constructor expects to pass a string method name.");
}
this.methods.push(methods[i]);
}
}
//static class method
Interface.ensureImplements = function(instance) {
if(arguments.length < 2) {
throw new Error("Function Interface.ensureImplements expects at least 2 arguments, but exactly passed for " + arguments.length + " arguments.");
}
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 at least 2 arguments to be instances of Interface.");
}
for(var j = 0, mLen = interface.methods.length; j < mLen; j++) {
var method = interface.methods[j];
if(!instance[method] || typeof instance[method] != "function") {
throw new Error("Function Interface.ensureImplements: object doesn't implements " + interface.name + ". Method " + method + " wasn't found.");
}
}
}
}
嚴格的類型檢查並非總是必需的,在平時的Web前端開發中很少用到以上的介面機制。但當你面對一個複雜特別是擁有很多相似模組的系統時,面向介面編程將變得非常重要。看似降低了JS的靈活性,實質上卻提高了類的靈活性,降低了類之間的耦合度,因為當你傳入任何一個實現了相同介面的對象都能被正確的解析。那什麼時候使用介面比較合適呢?對於一個大型項目來說,肯定有許多團隊成員,並且項目會被拆分為更細粒度的功能模組,為了保證進度需提前利用"佔位程式"(介面)來說明模組的功能或與已開發完成的模組之間通訊時,提供一個統一的介面(API)顯得相當必要。隨著項目的不斷推進,可能需求會不斷的發生變動,各模組功能也會發生相應的變動,但彼此之間通訊以及提供給上層模組的API始終保持不變,確保整個架構的穩定性和持久性。下面我們通過一個具體的樣本來說明介面的實際應用。假設設計一個類自動檢測結果對象(TestResult類)並格式化輸出一個網頁視圖,沒有使用介面的實現方式: 複製代碼 代碼如下:var ResultFormatter = function(resultObject) {
if(!(resultObject instanceof TestResult)) {
throw new Error("ResultFormatter constructor expects a instance of TestResult.");
}
this.resultObject = resultObject;
}
ResultFormatter.prototype.render = function() {
var date = this.resultObject.getDate();
var items = this.resultObject.getResults();
var container = document.createElement("div");
var header = document.createElement("h3");
header.innerHTML = "Test Result from " + date.toUTCString();
container.appendChild(header);
var list = document.createElement("ul");
container.appendChild(list);
for(var i = 0, len = items.length; i++) {
var item = document.createElement("li");
item.innerHTML = items[i];
list.appendChild(item);
}
return container;
}
首先ResultFormatter類的建構函式僅僅是檢查了是否為TestResult執行個體,卻無法保證一定實現了render中的方法getDate()和getResults()。另外,隨著需求的不斷變動,現在有一個Weather類,包含了getDate()和getResults()方法,卻因為只能檢查是否為TestResult的執行個體而無法運行render方法,豈不是很無語呢?解決辦法是移除instanceof檢查並以介面代替。 複製代碼 代碼如下://create the ResultSet interface
var ResultSet = new Interface("ResultSet", ["getDate", "getResults"]);
var ResultFormatter = function(resultObject) {
// using Interface.ensureImplements to check the resultObject
Interface.ensureImplements(resultObject, ResultSet);
this.resultObject = resultObject;
}
ResultFormatter.prototype.render = function() {
// keep the same as former
var date = this.resultObject.getDate();
var items = this.resultObject.getResults();
var container = document.createElement("div");
var header = document.createElement("h3");
header.innerHTML = "Test Result from " + date.toUTCString();
container.appendChild(header);
var list = document.createElement("ul");
container.appendChild(list);
for(var i = 0, len = items.length; i++) {
var item = document.createElement("li");
item.innerHTML = items[i];
list.appendChild(item);
}
return container;
}
可以看出render方法沒有發生任何改變。改變的僅僅是添加一個介面和使用介面來進行類型檢查。同時現在能夠傳遞Weather類的執行個體來進行調用,當然也能傳遞實現了ResultSet介面的任何類的執行個體,使檢查更加精確和寬容。隨著後續對JS設計模式的推出,介面會在原廠模式、組合模式、裝飾模式和命令模式中得到廣泛的應用。希望大家可以細細品味介面給我們的JS模組化設計帶來的益處。