Adapter

December 27, 2021

在〈Default Adapter〉談過,具有 adapter 字眼的模式,代表著會有一方制定了規範,而另一方必須滿足規範,要滿足規範的那方就是 adapter。

有時作為 adapter 的角色,在實作上會用來對應某物件的操作,這麼一來,原本不符合規範的該物件,就能與要求規範的一方合作了。

Object Adapter

例如,對於一組有序資料,要從頭迭代至尾,Java 可以透過 for 語法。例如 List

var names = List.of("Justin", "Monica", "Irene");
for(var name : names) {
    System.out.println(name);
}

那麼字串呢?字串不也是一組有序資料?能不能逐一迭代出其中的字元呢?沒辦法直接套用!不過,若你知道實作 Iterable 的物件,就可以使用 for 來從頭至尾迭代,試著如下定義 IterableString

import java.util.Iterator;

class IterableString implements Iterable<Character> {
    private String str;
    IterableString(String str) {
        this.str = str;
    }
    public Iterator<Character> iterator() {
        return new Iterator<>() {
                   private int index;
                   public boolean hasNext() {
                       return index < str.length();
                   }
                   public Character next() {
                       return str.charAt(index++);
                   }
                   public void remove() {
                       throw new RuntimeException("Not supported");
                   }
               };
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
         for(char c : new IterableString("I like foreach!")) {
             System.out.println(c);
         }
    }
}

Iterable 物件必須能傳回 Iterator,如果你認識 Java 夠多,應該會知道以上程式會被編譯器展開,底層會是透過 iterator 取得 Iterator 後,呼叫 hasNextnext 來進行迭代,從展開後的程式碼(也就是客戶端)來看,Iterator 實例就是 adapter 的角色,將基於索引的字串操作,對應至客戶端要求的介面。

在 Gof 中是將這種概念,稱為 Object Adapter,其實也沒什麼,就是滿足客戶端要求的實作,只不過從物件間彼此組合時的結構上來看,就像是將字串透過配接器,插到 for 使用而已。

Gof 在談模式時,分為創建、結構與行為三個大類,這只是試著從不同角度來談設計,同一個設計,從結構來看類似 A 模式,從行為上來看是 B 模式,其實是很正常的事。

因此,被放在結構分類的模式,不是說它必然就是該分類,而是說你可以從結構的角度來思考,也就是從組裝的角度來思考;上面的例子也可以從行為的角度來思考,如果是這樣的話,你覺得會像是什麼模式呢?這也沒有標準答案,要看你是從哪個角色來思考,從 Iterator?從 Iterable?還是從 for 呢?

Class Adapter

在 Gof 中,還談到了 Class Adapter,基本上適用於有多重繼承概念的場合,你可能會想要將多個來源的行為,以繼承的方式配接到客戶端。

來看個 Python 的概念實現:

class Adaptee1:
    def doAction1(self):
        print('action 1')

class Adaptee2:
    def doAction2(self):
        print('action 2')

class Adapter(Adaptee1, Adaptee2):
    def doRequest(self): # doRequest 是期望之介面
        self.doAction1()
        self.doAction2()
        print('request')

adapter = Adapter()
adapter.doRequest()

並不是說 Java 就沒有機會從這方面來思考,方才談到,可以用在具有多重繼承概念的場合,在 Java 中實作介面,其實就是一種廣義的多重繼承,有時候你會需要繼承某類別的功能,來實現某個介面要求的行為:

public class Adapter extends Adaptee implements Target {
    public void request() {
        specificRequest();
    }
}

那麼從結構的角度來看,你就是將 Adaptee,組裝至要求 Target 行為的客戶端了;另一方面,由於 Java SE 8 以後,介面可以有預設實作方法,這也給了共用相同實作的方便性。

例如,可以如下定義自己的 Comparable 介面:

public interface Comparable<T> {
    int compareTo(T that);
 
    default boolean lessThan(T that) {
        return compareTo(that) < 0;
    }
    default boolean lessOrEquals(T that) {
        return compareTo(that) <= 0;
    }
    default boolean greaterThan(T that) {
        return compareTo(that) > 0;
    }
    ...
}

若有個 Ball 類別想實作這個 Comparable 介面,只需要實作 compareTo 方法:

public class Ball implements Comparable<Ball> {
    private int radius;
    ...
    public int compareTo(Ball that) {
        return this.radius - that.radius;
    }
}

這麼一來,每個 Ball 實例就會擁有 Comparable 定義的預設方法。因為類別可以實作多個介面,運用預設方法,就可以在某介面定義可共用的操作,若有個類別需要某些可共用操作,只需要實作相關介面,就可以混入(Mixin)這些共用的操作了。

從這點來看,如果你需要繼承某類別,並混入一組具有預設方法的介面,也是在實現 Class Adapter 的概念了:

public class Adapter extends Adaptee implements Target, IAdaptee1, Adaptee2 {
public void request() {
        specificRequest();
    }
}

不過,什麼 Object Adapter、Class Adapter 的,在寫程式時其實也不會去特別想到這些名詞,我相信你可能早就做過類似思考與實現,但是沒特別用過這些名詞,這完全不打緊,就當多認識 Gof 那夥人命名時的冷知識就行了!