結構型模式 - 介面卡
技術標籤:設計模式
將一個類的介面轉換成客戶希望的另外一個介面,使得原本由於介面不相容而不能一起工作的那些類可以一起工作。
介面卡模式是Adapter,也稱Wrapper,是指如果一個介面需要B介面,但是待傳入的物件卻是A介面,怎麼辦?
我們舉個例子。如果去美國,我們隨身帶的電器是無法直接使用的,因為美國的插座標準和中國不同,所以,我們需要一個介面卡:
在程式設計中,介面卡也是類似的。我們已經有一個Task
類,實現了Callable
介面:
public class Task implements Callable<Long> { private long num; public Task(long num) { this.num = num; } public Long call() throws Exception { long r = 0; for (long n = 1; n <= this.num; n++) { r = r + n; } System.out.println("Result: " + r); return r; } }
現在,我們想通過一個執行緒去執行它:
Callable<Long> callable = new Task(123450000L);
Thread thread = new Thread(callable); // compile error!
thread.start();
發現編譯不過!因為Thread
接收Runnable
介面,但不接收Callable
介面,腫麼辦?
一個辦法是改寫Task
類,把實現的Callable
改為Runnable
,但這樣做不好,因為Task
很可能在其他地方作為Callable
被引用,改寫Task
的介面,會導致其他正常工作的程式碼無法編譯。
另一個辦法不用改寫Task
Callable
介面“變成”Runnable
介面,這樣,就可以正常編譯:
Callable<Long> callable = new Task(123450000L);
Thread thread = new Thread(new RunnableAdapter(callable));
thread.start();
這個RunnableAdapter
類就是Adapter,它接收一個Callable
,輸出一個Runnable
。怎麼實現這個RunnableAdapter
呢?我們先看完整的程式碼:
public class RunnableAdapter implements Runnable { // 引用待轉換介面: private Callable<?> callable; public RunnableAdapter(Callable<?> callable) { this.callable = callable; } // 實現指定介面: public void run() { // 將指定介面呼叫委託給轉換介面呼叫: try { callable.call(); } catch (Exception e) { throw new RuntimeException(e); } } }
編寫一個Adapter的步驟如下:
- 實現目標介面,這裡是
Runnable
; - 內部持有一個待轉換介面的引用,這裡是通過欄位持有
Callable
介面; - 在目標介面的實現方法內部,呼叫待轉換介面的方法。
這樣一來,Thread就可以接收這個RunnableAdapter
,因為它實現了Runnable
介面。Thread
作為呼叫方,它會呼叫RunnableAdapter
的run()
方法,在這個run()
方法內部,又呼叫了Callable
的call()
方法,相當於Thread
通過一層轉換,間接呼叫了Callable
的call()
方法。
介面卡模式在Java標準庫中有廣泛應用。比如我們持有資料型別是String[]
,但是需要List
介面時,可以用一個Adapter:
String[] exist = new String[] {"Good", "morning", "Bob", "and", "Alice"};
Set<String> set = new HashSet<>(Arrays.asList(exist));
注意到List<T> Arrays.asList(T[])
就相當於一個轉換器,它可以把陣列轉換為List
。
我們再看一個例子:假設我們持有一個InputStream
,希望呼叫readText(Reader)
方法,但它的引數型別是Reader
而不是InputStream
,怎麼辦?
當然是使用介面卡,把InputStream
“變成”Reader
:
InputStream input = Files.newInputStream(Paths.get("/path/to/file"));
Reader reader = new InputStreamReader(input, "UTF-8");
readText(reader);
InputStreamReader
就是Java標準庫提供的Adapter
,它負責把一個InputStream
適配為Reader
。類似的還有OutputStreamWriter
。
如果我們把readText(Reader)
方法引數從Reader
改為FileReader
,會有什麼問題?這個時候,因為我們需要一個FileReader
型別,就必須把InputStream
適配為FileReader
:
FileReader reader = new InputStreamReader(input, "UTF-8"); // compile error!
直接使用InputStreamReader
這個Adapter是不行的,因為它只能轉換出Reader
介面。事實上,要把InputStream
轉換為FileReader
也不是不可能,但需要花費十倍以上的功夫。這時,面向抽象程式設計這一原則就體現出了威力:持有高層介面不但程式碼更靈活,而且把各種介面組合起來也更容易。一旦持有某個具體的子類型別,要想做一些改動就非常困難。