1. 程式人生 > 其它 >結構型模式 - 介面卡

結構型模式 - 介面卡

技術標籤:設計模式

將一個類的介面轉換成客戶希望的另外一個介面,使得原本由於介面不相容而不能一起工作的那些類可以一起工作。

介面卡模式是Adapter,也稱Wrapper,是指如果一個介面需要B介面,但是待傳入的物件卻是A介面,怎麼辦?

我們舉個例子。如果去美國,我們隨身帶的電器是無法直接使用的,因為美國的插座標準和中國不同,所以,我們需要一個介面卡:

adapter

在程式設計中,介面卡也是類似的。我們已經有一個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

類,而是用一個Adapter,把這個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的步驟如下:

  1. 實現目標介面,這裡是Runnable
  2. 內部持有一個待轉換介面的引用,這裡是通過欄位持有Callable介面;
  3. 在目標介面的實現方法內部,呼叫待轉換介面的方法。

這樣一來,Thread就可以接收這個RunnableAdapter,因為它實現了Runnable介面。Thread作為呼叫方,它會呼叫RunnableAdapterrun()方法,在這個run()方法內部,又呼叫了Callablecall()方法,相當於Thread通過一層轉換,間接呼叫了Callablecall()方法。

介面卡模式在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也不是不可能,但需要花費十倍以上的功夫。這時,面向抽象程式設計這一原則就體現出了威力:持有高層介面不但程式碼更靈活,而且把各種介面組合起來也更容易。一旦持有某個具體的子類型別,要想做一些改動就非常困難。