概述
本系列上一篇:建造者、工廠方法、享元、橋接
本文介紹的設計模式(建議按順序閱讀):
配接器
模板方法
裝飾器
相關縮寫:EJ - Effective Java
Here We Go
配接器 (Adapter)
定義:將一個類的介面轉換成客戶希望的另外一個介面,配接器模式使得原本由于介面不兼容而不能一起作業的那些類可以一起作業,
場景:想使用現有的類,但此類的介面不符合已有系統的需要,同時雙方不太容易修改;通過介面轉換,將一個類插入到另一個類系中,
型別:結構型
配接器聽起來像是一種亡羊補牢,仿佛使用了它就代表你承認了系統設計糟糕、不易擴展,所以才需要在兩個類系之間增加中間者實作兼容,
但配接器真正的靈魂所在,是為一個事物提供多種 視角(perspective) ,
雖然 HashMap 快被講爛了,但并不妨礙我們以 Design Pattern 的角度來欣賞 HashMap 中 Map::keySet 的實作細節,
/**
* Returns a {@link Set} view of the keys contained in this map.
* The set is backed by the map, so changes to the map are
* reflected in the set, and vice-versa. If the map is modified
* while an iteration over the set is in progress (except through
* the iterator's own <tt>remove</tt> operation), the results of
* the iteration are undefined. The set supports element removal,
* which removes the corresponding mapping from the map, via the
* <tt>Iterator.remove</tt>, <tt>Set.remove</tt>,
* <tt>removeAll</tt>, <tt>retainAll</tt>, and <tt>clear</tt>
* operations. It does not support the <tt>add</tt> or <tt>addAll</tt>
* operations.
*
* @return a set view of the keys contained in this map
*/
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
final class KeySet extends AbstractSet<K> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<K> iterator() { return new KeyIterator(); }
public final boolean contains(Object o) { return containsKey(o); }
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
}
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
}
Map::keySet 是配接器模式的典型適用場景: HashMap 實作了 Map 介面,其與標準集合介面 Set 在繼承層次(Map與Set)和資料結構(異構容器與同構容器)上大相徑庭,它們代表著兩個類系,
ketSet() 的職責是將鍵值對中的鍵抽出,組成一個 Set實體,
構造一個HashSet?回圈add?
我們來看看 HashMap 是如何實作這一需求的:
觀察代碼可以發現,keySet() 本身邏輯十分簡單,創建一個內部類 KeySet 的實體,并對其進行實體控制,
再來看看 KeySet 類的邏輯:一個繼承自 AbstractSet的內部類, AbstractSet 是實作了 Set 標準的骨架實作類 ,繼承它之后 KeySet 類只需實作剩下的基本型別介面就可稱自己是一個 Set 了,那么這些介面是如何實作的呢?
size()-> 回傳外部類的size欄位,即鍵值對個數,
clear()-> 呼叫外部類clear()方法,即清空鍵值對陣列,
iterator()-> 回傳內部類KeyIterator實體,此類繼承自通用迭代器HashIterator,重寫next()回傳下一元素的key欄位,
contains()-> 呼叫外部類containsKey()方法,
remove()-> 呼叫外部類輔助方法removeNode(),即洗掉鍵值對,
針對 Set 所要求的介面能力, HashMap 最大限度地復用已有邏輯,在保持資料正確的前提下,將兩個介面的職責建立映射,
再來看看上述代碼片段中 keySet() 的JavaDoc注釋,
Returns a {@link Set} view of the keys contained in this map.
The set is backed by the map, so changes to the map are
reflected in the set, and vice-versa.回傳此map中包含的鍵的set視圖,這個set是由map支撐的,所以對map的修改都會反映到set上,反之亦然,
不僅是 keySet(), values() 、 entrySet() 都使用了相同的配接器模式,這些配接器方法避免了為適應新標準而重新生成資料結構造成的浪費,
配接器的思路,就是對同一個物件建立多個視角,每一種視角下其特征、行為都不同,從而以更多維度來服務系統,
正所謂——
橫看成嶺側成峰,遠近高低各不同,
不識廬山真面目,只緣身在此山中,
模板方法 (Template Method)
定義:定義一個操作中的演算法的骨架,而將一些步驟延遲到子類中,模板方法使得子類可以不改變一個演算法的結構即可重定義該演算法的某些特定步驟,
場景:多個子類共有邏輯相同的方法;重要的、復雜的方法
型別:行為型
在上文配接器的介紹中參考了 HashMap的實作,其中介紹 KeySet 時提及到了 骨架實作類
的概念,而骨架實作類恰恰是模板模式的一種實踐,本節以此為例,
首先復習一下Java的集合框架
藍色部分是我們熟知的各種集合實作,它們都繼承自亮綠色部分、以Abstract開頭命名的抽象類,這些類便稱為骨架實作類,它們直接實作了 List 、 Set 、 Map等介面,
摘取一段EJ中關于骨架實作類的描述,
通過對介面提供一個抽象的骨架實作(skeletal implementation)類,可以把介面和抽象類的優點結合起來,介面負責定義型別,或許還提供一些預設方法,而骨架實作類則負責實作除基本型別介面方法之外,剩下的非基本型別介面方法,擴展骨架實作占了實作介面之外的大部分作業,
什么是基本型別介面方法呢?對于這個冗長的命名,我理解就好比Java中萬物皆物件,但所有物件最終的狀態都要由基本型別來表示,組合物件也可看作是被封裝好的基本型別之間進行組合,
換句話說,非基本型別介面方法可以憑借基本型別介面方法推匯出自身的邏輯,這一點和介面的預設方法十分相似,
比如這是List中排序介面的預設方法,
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
這樣,List的實作類只要保證toArray()、listIterator()這些基本型別介面方法行為正確,排序方法就隱式地被實作了,
再摘取AbstractList中的片段,
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
abstract public E get(int index);
public List<E> subList(int fromIndex, int toIndex) {
return (this instanceof RandomAccess ?
new RandomAccessSubList<>(this, fromIndex, toIndex) :
new SubList<>(this, fromIndex, toIndex));
}
class SubList<E> extends AbstractList<E> {...}
class RandomAccessSubList<E> extends SubList<E> implements RandomAccess {...}
}
AbstractList中的實作更趨于完整,已經通過撰寫輔助內部類將迭代器、子串列等功能進行了實作,
介面-預設方法 與 骨架實作類-非基本型別介面方法 都是可根據其他方法推演自身邏輯的方法,那它們之間的區別在哪呢?不如把骨架實作類中的代碼搬到介面當中!然而這樣是不妥的,它們之間還是有區別的,
骨架實作類為抽象類提供了實作上的幫助,但又不強加“抽象類被用作型別定義時”所特有的嚴格限制, 如果預置的類無法擴展骨架實作類,這個類始終都可以手工實作這個介面,同時仍然受益于介面的預設方法,
介面定義了整個類系的型別,預設方法是針對這一批型別的通解;而骨架實作類是介面的某一種實作方案,它趨于完整,方便最終實作類的撰寫,但不一定是最佳方案,所以不能系結到整個類系之上,
無論是預設方法,還是骨架實作類,都是模板方法的實踐,通過定義模板、繼承模板,可以讓開發者專注于關鍵邏輯,同時也能隨意覆寫模板,讓子類實作高效又靈活,
裝飾器 (Decorator)
定義:動態地將一個物件添加一些額外的職責,就添加功能來說,裝飾模式比生成子類更為靈活,
場景:在不想增加很多子類的情況下擴展類;動態增加功能,動態撤銷,
型別:結構型
復合優先于繼承,這是EJ中提到裝飾器時的Tip標題,它很好的表達了裝飾器出現的原因,
繼承是實作代碼重用的強大工具,但并非總是最佳工具,其中一個原因是:繼承破壞了封裝性,
換句話說,子類依賴于其超類中特定功能的實作細節,超類的實作有可能會隨著發行版本的不同而有所變化,如果真的發生了變化,子類可能會遭到破壞,即使它的代碼完全沒有改變,
這里所講的變化可以是以下任意一種:
- 父類的方法在類內互相呼叫,這種 自用性 (self-use) 是實作細節,開發者可能會認為方法之間是獨立的,如果覆寫某個被依賴的方法,依賴方也會受影響,并且在未來的發行版本中這種依賴關系是變化的、不穩定的,
- 子類對所有方法加入了一種先決條件,例如驗參,父類如果在后續的發行版本添加新的方法,就會成為“漏網之魚”,造成安全問題,
- 在新的發行版本中父類撰寫了一個新方法,恰好與某個子類的新增方法簽名沖突,造成編譯失敗,
- ···
總而言之,倘若不是專門為了繼承而設計并且具有很好的檔案說明的類,在多人協作,特別是跨越包邊界時(泛指不再對子類撰寫、迭代的規范有強約束力)使用繼承非常危險,會讓系統變得更加脆弱,
所幸有一種方法可以避免繼承的種種問題,即 復合-轉發,
不擴展現有的類,而是在新的類中增加私有域,參考現有類的一個實體,這種設計被稱為 “復合”(composition) ;
新類中每個實體方法都可以呼叫被包含的現有類實體中對應的方法,并回傳他的結果,這被稱為 “轉發”(forwarding) ,
我們還是來看集合框架中的一個典型例子(今天跟集合框架杠上了...
public class Collections {
public static <K, V> Map<K, V> checkedMap(Map<K, V> m,
Class<K> keyType,
Class<V> valueType) {
return new CheckedMap<>(m, keyType, valueType);
}
private static class CheckedMap<K,V> implements Map<K,V>, Serializable {
private final Map<K, V> m;
final Class<K> keyType;
final Class<V> valueType;
CheckedMap(Map<K, V> m, Class<K> keyType, Class<V> valueType) {
this.m = Objects.requireNonNull(m);
this.keyType = Objects.requireNonNull(keyType);
this.valueType = Objects.requireNonNull(valueType);
}
public int size() { return m.size(); }
public boolean isEmpty() { return m.isEmpty(); }
public boolean containsKey(Object key) { return m.containsKey(key); }
public boolean containsValue(Object v) { return m.containsValue(v); }
public V get(Object key) { return m.get(key); }
public V remove(Object key) { return m.remove(key); }
public void clear() { m.clear(); }
public Set<K> keySet() { return m.keySet(); }
public Collection<V> values() { return m.values(); }
public boolean equals(Object o) { return o == this || m.equals(o); }
public int hashCode() { return m.hashCode(); }
public String toString() { return m.toString(); }
public V put(K key, V value) {
typeCheck(key, value);
return m.put(key, value);
}
private void typeCheck(Object key, Object value) {
if (key != null && !keyType.isInstance(key))
throw new ClassCastException(badKeyMsg(key));
if (value != null && !valueType.isInstance(value))
throw new ClassCastException(badValueMsg(value));
}
//省略剩余方法
}
}
這回介紹的是Collections::checkedMap,這個靜態工廠使用不多,其作用是為map實體提供鍵值對型別檢查,現如今Map介面已是一個泛型,但在JAVA SE5之前撰寫的各類map實作,是沒有型別檢查的能力的,當我們不能去改造老的類別庫時,只需一句簡單的呼叫:
Map<Integer, String> typeSafeMap = Collections.checkedMap(new OldMap(), Integer.class, String.class);
即可為老類別庫賦予和泛型一樣的型別檢查能力,我們來細品代碼,
靜態工廠中回傳了 CheckedMap 的新實體, CheckedMap 是實作了 Map 介面的內部類,定義了私有變數 Map 用于接收map實體;兩個 Class 欄位,分別保存鍵值的型別,對于大多數方法, CheckedMap 直接將呼叫 轉發 至原map上,但在 put 這樣的插入操作中,在轉發前呼叫了私有的 typeCheck 方法,執行型別檢查,
避開繼承,使用裝飾器,我們同樣能為現有類追加新的功能,同時裝飾器本身還可以再次被裝飾,這使得裝飾器是動態的、可拆卸的,
例如對現有類同時賦予 型別安全、執行緒安全 的特性,
Map<Integer, String> safeMap = Collections.synchronizedMap(
Collections.checkedMap(new OldMap(), Integer.class, String.class)
);
裝飾器本身也有撰寫成本,因為需要將所有方法進行轉發,但往往需要裝飾的方法較少, Guava 做了這方面考慮,在collect包下為所有集合介面撰寫了轉發類,類名格式:ForwardingXXX,開發者只需繼承這些轉發類,重寫需要裝飾的方法即可,
參考:
[1] Effective Java - 機械工業出版社 - Joshua Bloch (2017/11)
[2] 《大話設計模式》 - 清華大學出版社 - 陳杰 (2007/12)
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/163612.html
標籤:Java
上一篇:學習筆記之Stream流
