談談Java常用類庫中的設計模式 – Part Ⅱ

概述

本系列上一篇:建造者、工廠方法、享元、橋接

本文介紹的設計模式(建議按順序閱讀):

適配器
模板方法
裝飾器

相關縮寫:EJ – Effective Java

Here We Go

適配器 (Adapter)

定義:將一個類的介面轉換成客戶希望的另外一個介面。適配器模式使得原本由於介面不兼容而不能一起工作的那些類可以一起工作。

場景:想使用現有的類,但此類的介面不符合已有系統的需要,同時雙方不太容易修改;通過介面轉換,將一個類插入到另一個類系中。

類型:結構型

適配器聽起來像是一種亡羊補牢,彷彿使用了它就代表你承認了系統設計糟糕、不易擴展,所以才需要在兩個類系之間增加中間者實現兼容。
但適配器真正的靈魂所在,是為一個事物提供多種 視角(perspective)

雖然 HashMap 快被講爛了,但並不妨礙我們以 Design Pattern 的角度來欣賞 HashMapMap::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 SetMap等介面。

摘取一段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)