Javascript基礎知識篇(2): 面向對象之接口實現


接口是面向對象Javascript工具箱中最有用的特性之一。我們都知道GOF在設計模式中說到:面向接口編程,而非面向實現編程。就足以說明接口在面向對象的領域中有多重要。但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 < len; 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模塊化設計帶來的益處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM