no_mkd
在正式開始之前,讓我們先思考幾個問題:
- 如果現有的新專案可以利用舊專案裡大量的遺留程式碼,你打算從頭開始完成新專案還是去了解舊專案的模組功能以及介面?
- 如果你瞭解過遺留程式碼之後,發現有幾個重要的模組介面不同(因為它們可能來自多個舊專案),無法直接複用,你打算放棄使用遺留程式碼嗎?
- 如果你不打算放棄(這樣做應該是對的,畢竟遺留程式碼的正確性是經過實踐檢驗的),那麼是不是只能去改寫剩餘的 n - 1 個介面,甚至改寫所有的 n 個介面?
- 如果不這樣做,還有什麼簡單的方法嗎?
一.什麼是配接器模式?
首先,我們需要知道配接器是什麼東西,嗯,筆記型電腦的電源配接器聽說過吧?它能夠把 220V 的交流電轉換為筆記型電腦需要的 15V 直流電。太神奇了,一個小小的電源配接器解決了家庭用電與筆記型電腦需要的電類型不匹配的問題。發現什麼了嗎?沒錯,我們既沒有改變家庭用電(把它變成 15V 直流電),也沒有改變筆記型電腦(把它變成 220V 交流電),但我們確實解決了這個問題
配接器模式——用來實現不同介面轉換的設計模式
二.舉個例子
假設我們有兩個封裝好的功能模組,但它們需要的參數不同(雖然參數的實質是同一種物件)
比如,我們的 A 模組(文字檢查模組)是這樣的:
package AdapterPattern;/**
-
@author ayqy
-
文字檢查模組(類似與 MSOffice Word 中的「拼寫和語法檢查」) */ public class TextCheckModule { FormatText text;
public TextCheckModule(FormatText text){ this.text = text; }
/*
- 省略很多具體 Check 操作。。 */ }
A 模組入口需要一個 FormatText 類型的參數,它的定義如下:
package AdapterPattern;/**
-
@author ayqy
-
定義格式化文字 */ public interface FormatText { String text = null;
/**
- @return 文字邏輯行數 */ public abstract int getLineNumber();
/**
- @param index 行號
- @return 第 index 行的內容 */ public abstract String getLine(int index);
/*
- 省略其它有用的方法 */ }
還有 B 模組(文字顯示模組),它是這樣的:
package AdapterPattern;/**
-
@author ayqy
-
文字顯示模組 */ public class TextPrintModule { DefaultText text;
public TextPrintModule(DefaultText text){ this.text = text; }
/*
- 省略很多顯示相關操作
- */ }
B 模組入口所需的 DefaultText:
package AdapterPattern;/**
-
@author ayqy
-
定義預設文字 */ public interface DefaultText { String text = null;
/**
- @return 文字邏輯行數 */ public abstract int getLineCount();
/**
- @param index 行號
- @return 第 index 行的內容 */ public abstract String getLineContent(int index);
/*
- 省略其它有用的方法 */ }
我們的新專案要求實現一個文字處理程式(像 MSOffice Word 那樣的),我們需要呼叫 A 模組來實現文字檢查功能,還需要呼叫 B 模組來實現文字顯示功能。但問題是,兩個模組的介面不匹配,導致我們無法直接複用現成的 A 和 B。。
這時我們似乎只有有兩個選擇:
- 修改 FormatText(或者 DefaultText),以滿足 DefaultText(或者 FormatText),還需要修改 A(或者 B)的內部實現
- 定義第三種介面 MyText,修改 A 和 B,讓它們把 MyText 作為參數,以求介面的統一
當然,我們可能更傾向與第一種,畢竟所需的修改相對較少,不過即使這樣,工作量仍然很大,我們需要打開 A 的封裝,理解其內部實現,並修改方法呼叫細節。其實我們還有更好的選擇——定義一個 Adapter,負責 FormatText 到 DefaultText 的轉換(或者與此相反):
package AdapterPattern;/**
-
@author ayqy
-
定義預設文字配接器 */ public class DefaultTextAdapter implements DefaultText{ FormatText formatText = null;//源物件
/**
- @param text 需要轉換的源文字物件 */ public DefaultTextAdapter(FormatText formatText){ this.formatText = formatText; }
@Override public int getLineCount() { int lineNumber;
lineNumber = formatText.getLineNumber(); /* * 在此添加額外的轉換處理 * */ return lineNumber;}
@Override public String getLineContent(int index) { String line;
line = formatText.getLine(index); /* * 在此添加額外的轉換處理 * */ return line;} }
我們的做法其實相當簡單:
- 定義 Adapter 實現目標介面
- 獲取並保留源介面物件
- 實現目標介面中的各個方法(在方法體中呼叫源介面物件的方法並添加額外的處理以實現轉換)
配接器做好了,要怎麼用呢?不妨實現一個 Test 類來測試一下:
package AdapterPattern;/**
-
@author ayqy
-
測試介面配接器 */ public class Test implements FormatText{
public static void main(String[] args) { //建立源介面物件 FormatText text = new Test(); //建立文字檢查模組物件 TextCheckModule tcm = new TextCheckModule(text); /呼叫 tcm 實現文字檢查/
//建立配接器物件,進行源介面物件到目標介面物件的轉換 DefaultTextAdapter textAdapter = new DefaultTextAdapter(text); //用 Adapter 建立文字顯示模組物件 TextPrintModule tpm = new TextPrintModule(textAdapter); /*呼叫 tcm 實現文字顯示*/}
/請忽略下面偷懶的部分。。/ @Override public int getLineNumber() { // TODO Auto-generated method stub return 0; }
@Override public String getLine(int index) { // TODO Auto-generated method stub return null; }
}
(P.S. ��諒我的偷懶行為,誰讓 FormatText 偏偏是個介面呢。。)
當然,Test 是不會有運行結果的,但能通過編譯就足夠說明我們的轉換沒有問題。。
其實我們忽略了一個很重要的問題,例子中源介面與目標介面的方法都是對應的,換句話說就是:源介面中定義的方法在目標介面中都有類似的方法與之對應。當然,這樣的情況是極少的,通常都存在方法不對應的問題(源介面中存在目標介面未定義的方法,或者相反的情況)
這時我們有 2 個選擇:
- 擲回異常,但應該在註釋或者文檔作出詳細說明,就像這樣:
throw new UnsupportedOperationException();//源介面不支援此操作
- 完成一個空的實現,比如,return false,0,null 等等
具體選擇哪一種,取決於具體情境,各有各的好處,不能一概而論
三.另一種配接器實現方式
例子中我們採用了「持有源介面物件,實現目標介面」的方式來實現配接器,其實還存在另一種方式——多繼承(或者實現多個介面)
如果一個 Adapter 類既實現了 A 介面又實現了 B 介面,那麼,毫無疑問,Adapter 物件既屬於 A 類型又屬於 B 類型(多繼承的原理類似。。)
雖然 Java 不支援多繼承,但在支援多繼承的語言環境下我們應當想到這樣的實現方式,再視具體情況決定是否採用多繼承來實現 Adapter
四.總結
當我們手裡同時握著一個兩孔插頭和一個三孔插口時,總是習慣把插頭芯擰成八字形的。為什麼不去買一個配接器呢?
- 既不需要破壞插頭,也不需要破壞插口(有時候程式碼修改確實是破壞性的,我們避免了修改也就避免了破壞)
- 更關鍵的是:我們可以把買來的配接器借給朋友用(可複用)
暫無評論,快來發表你的看法吧