談談Java經常使用類庫中的設計模式 - Part Ⅱ

概述

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

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

適配器
模板方法
裝飾器設計模式

相關縮寫:EJ - Effective Java數組

Here We Go

適配器 (Adapter)

定義:將一個類的接口轉換成客戶但願的另一個接口。適配器模式使得本來因爲接口不兼容而不能一塊兒工做的那些類能夠一塊兒工做。安全

場景:想使用現有的類,但此類的接口不符合已有系統的須要,同時雙方不太容易修改;經過接口轉換,將一個類插入到另外一個類系中。數據結構

類型:結構型app

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

雖然 HashMap 快被講爛了,但並不妨礙咱們以 Design Pattern 的角度來欣賞 HashMapMap::keySet 的實現細節。dom

/**
     * 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)

相關文章
相關標籤/搜索