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
後,呼叫 hasNext
、next
來進行迭代,從展開後的程式碼(也就是客戶端)來看,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 那夥人命名時的冷知識就行了!