(八)適配器模式詳解

(八)適配器模式詳解

                  作者:zuoxiaolong8810(左瀟龍),轉載請註明出處,特別說明:本博文來自博主原博客,爲保證新博客中博文的完整性,特複製到此留存,如需轉載請註明新博客地址即可。

                  各位好,我們本次接着討論第八個設計模式,適配器模式。

                  適配器模式從實現方式上分爲兩種,類適配器和對象適配器,這兩種的區別在於實現方式上的不同,一種採用繼承,一種採用組合的方式。

                  另外從使用目的上來說,也可以分爲兩種,特殊適配器和缺省適配器,這兩種的區別在於使用目的上的不同,一種爲了複用原有的代碼並適配當前的接口,一種爲了提供缺省的實現,避免子類需要實現不該實現的方法。

                  首先應該明白一點,適配器模式是補救措施,所以在系統設計過程中請忘掉這個設計模式,這個模式只是在你無可奈何時的補救方式。

                  那麼我們什麼時候使用這個模式呢?場景通常情況下是,系統中有一套完整的類結構,而我們需要利用其中某一個類的功能(通俗點說可以說是方法),但是我們的客戶端只認識另外一個和這個類結構不相關的接口,這時候就是適配器模式發揮的時候了,我們可以將這個現有的類與我們的目標接口進行適配,最終獲得一個符合需要的接口並且包含待複用的類的功能的類。

                  接下來我們舉一個例子,比如我們在觀察者一章中就提到一個問題,就是說觀察者模式的一個缺點,即如果一個現有的類沒有實現Observer接口,那麼我們就無法將這個類作爲觀察者加入到被觀察者的觀察者列表中了,這實在太遺憾了。

                  在這個問題中,我們需要得到一個Observer接口的類,但是又想用原有的類的功能,但是我們又改不了這個原來的類的代碼,或者原來的類有一個完整的類體系,我們不希望破壞它,那麼適配器模式就是你的不二之選了。

                  我們舉個具體的例子,比如我們希望將HashMap這個類加到觀察者列表裏,在被觀察者產生變化時,假設我們要清空整個MAP。但是現在加不進去啊,爲什麼呢?

                  因爲Observable的觀察者列表只認識Observer這個接口,它不認識HashMap,怎麼辦呢?

                  這種情況下,我們就可以使用類適配器的方式將我們的HashMap做點手腳,剛纔已經說了,類適配器採用繼承的方式,那麼我們寫出如下適配器。

複製代碼
public class HashMapObserverAdapter<K, V> extends HashMap<K, V> implements Observer{

    public void update(Observable o, Object arg) {
        //被觀察者變化時,清空Map
        super.clear();
    }

}
複製代碼

                 即我們繼承我們希望複用其功能的類,並且實現我們想適配的接口,在這裏就是Observer,那麼就會產生一個適配器,這個適配器具有原有類(即HashMap)的功能,又具有觀察者接口,所以這個適配器現在可以加入到觀察者列表了。

                 看,類適配器很簡單吧?那麼下面我們來看看對象適配器,剛纔說了對象適配器是採用組合的方式實現。

                 爲什麼要採用組合呢?上面的方式不是很好嗎?

                 究其根本,是因爲JAVA單繼承的原因,一個JAVA類只能有一個父類,所以當我們要適配的對象是兩個類的時候,你怎麼辦呢?你難道要將兩個類全部寫到extends後面嗎,如果你這麼做了,那麼編譯器會表示它的不滿的。

                 我們還是拿觀察者模式那一章的例子來說(觀察者模式比較慘,老要適配器模式擦屁股),比如我們現在有一個寫好的類,假設就是個實體類吧。如下。

複製代碼
public class User extends BaseEntity{
    private Integer id;
    private String name;
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
複製代碼

                 看到了吧,我們的實體類大部分都是繼承自BaseEntity的,那現在你怎麼辦吧,你要想具有被觀察者的功能還要繼承Observable類,你說你怎麼繼承吧。

                 你是不是想說,那我的User不繼承BaseEntity不就完事了,我把BaseEntity裏面的東西全部挪動到User類,或者我不繼承Observable了,把Observable裏面的東西全部挪到User類裏面。

                 這並不是不行,但是這是個很大的隱患,比如我們項目到時候要針對BaseEntity的子類進行掃描,用來做一些事情,這時候如果User沒繼承BaseEntity,那麼你就會遺漏掉這個類,這會破壞你的繼承體系,付出太大了。

                 相反,如果你不繼承Observable,那麼你的User類看起來會非常雜亂,而且假設我現在不僅User類可以被觀察了,我的Person類,Employee都能被觀察了,你難道要把Observable的代碼COPY三次到這三個類裏面嗎?

                不要忘了剛纔說的,適配器模式就是爲了幫助我們複用代碼的,這裏使用適配器模式就可以幫我們複用Observable的代碼或者說功能。

                基於上面LZ的討論,我們做出如下適配器,這裏採用的對象適配器。

複製代碼
//我們繼承User,組合Observable.
public class ObservableUser extends User{
    
    private Observable observable = new Observable();

    public synchronized void addObserver(Observer o) {
        observable.addObserver(o);
    }

    public synchronized void deleteObserver(Observer o) {
        observable.deleteObserver(o);
    }

    public void notifyObservers() {
        observable.notifyObservers();
    }

    public void notifyObservers(Object arg) {
        observable.notifyObservers(arg);
    }

    public synchronized void deleteObservers() {
        observable.deleteObservers();
    }

    protected synchronized void setChanged() {
        observable.setChanged();
    }

    protected synchronized void clearChanged() {
        observable.clearChanged();
    }

    public synchronized boolean hasChanged() {
        return observable.hasChanged();
    }

    public synchronized int countObservers() {
        return observable.countObservers();
    }
    
    
}
複製代碼

              我們繼承User,而不是繼承Observable,這個原因剛纔已經說過了,我們不能破壞項目中的繼承體系,所以現在可觀察的User(ObservableUser)依然處於我們實體的繼承體系中,另外如果想讓ObservableUser具有User的屬性,則需要將User的屬性改爲protected。

              這下好了,我們有了可觀察的User了。不過LZ早就說過,設計模式要活用,這裏明顯不是最好的解決方案。因爲我們要是還有Person,Employee類都要具有可觀察的功能的話,那其實也相當慘,因爲下面那些Observable的方法我們還要再複製一遍。

              提示到這裏,不知各位想到更好的解決方案了嗎?尤其是新手可以好好思考下。

              LZ這裏給出最終相對來說比較好的解決方案,那就是我們定義如下可觀察的基類。

複製代碼
//我們擴展BaseEntity,適配出來一個可觀察的實體基類
public class BaseObservableEntity extends BaseEntity{

    private Observable observable = new Observable();

    public synchronized void addObserver(Observer o) {
        observable.addObserver(o);
    }

    public synchronized void deleteObserver(Observer o) {
        observable.deleteObserver(o);
    }

    public void notifyObservers() {
        observable.notifyObservers();
    }

    public void notifyObservers(Object arg) {
        observable.notifyObservers(arg);
    }

    public synchronized void deleteObservers() {
        observable.deleteObservers();
    }

    protected synchronized void setChanged() {
        observable.setChanged();
    }

    protected synchronized void clearChanged() {
        observable.clearChanged();
    }

    public synchronized boolean hasChanged() {
        return observable.hasChanged();
    }

    public synchronized int countObservers() {
        return observable.countObservers();
    }
    
}
複製代碼

              這下好了,現在我們的User,Person,Employee要是想具有可被觀察的功能,那就改去繼承我們適配好的BaseObservableEntity就好了,而且由於BaseObservableEntity繼承了BaseEntity,所以他們三個依然處於我們實體的繼承體系中,而且由於我們的BaseObservableEntity是新增的擴展基類,所以不會對原來的繼承體系造成破壞。

              適配器模式的用法還是比較清晰的,我們以上兩種方式都是爲了複用現有的代碼而採用的適配器模式,LZ剛纔說了,根據目的的不同,適配器模式也可以分爲兩種,那麼上述便是第一種,可稱爲定製適配器,還有另外一種稱爲缺省適配器

              首先我們得先說下缺省適配器爲什麼要出現,因爲適配器模式大部分情況下是爲了補救,所以既然補救,那麼肯定是歷史原因造成的我們需要使用這個模式。

              我們來看看缺省適配器的歷史來由,不知各位還是否記得在第一章總綱中,LZ曾經提到過一個原則,最小接口原則。

              這個原則所表達的思想是說接口的行爲應該儘量的少,那麼還記得LZ當時說如果你沒做到的話會產生什麼情況嗎?

              結果就是實現這個接口的子類,很可能出現很多方法是空着的情況,因爲你的接口設計的過大,導致接口中原本不該出現的方法出現了,結果現在子類根本用不上這個方法,但由於JAVA語言規則的原因,實現一個接口必須實現它的全部方法,所以我們的子類不得不被迫寫一堆空方法在那,只爲了編譯通過。

              所以爲了解決這一問題,缺省適配器就出現了。比如我們有如下接口。

複製代碼
public interface Person {
    
    void speak();
    
    void listen();
    
    void work();
    
}
複製代碼

                 這是一個人的接口,這個接口表示了人可以說話,聽和工作,假設是兩年前的LZ,還在家待業呢,LZ沒工作啊,但是LZ也是個人啊,所以LZ要實現這個接口,所以LZ只能把work方法抄下來空着放在那了,假設LZ是個聾啞人,好吧,三個方法都要空着了,但是LZ表示,LZ是人,LZ一定要實現Person接口。

                 當然,上述只是舉個例子,但是真實項目當中也會出現類似的情況,那麼怎麼辦呢?

                 這下來了,我們的缺省適配器來了,如下。

複製代碼
public class DefaultPerson implements Person{

    public void speak() {
    }

    public void listen() {
    }

    public void work() {
    }

}
複製代碼

                 我們創造一個Person接口的默認實現,它裏面都是一些默認的方法,當然這裏因爲沒什麼可寫的就空着了,實際當中可能會加入一些默認情況下的操作,比如如果方法返回結果整數,那麼我們在缺省適配器中可以默認返回個0。

                 這下好了,LZ只要繼承這個默認的適配器(DefaultPerson),然後覆蓋掉LZ感興趣的方法就行了,比如speak和listen,至於work,由於適配器幫我們提供了默認的實現,所以就不需要再寫了。

                 這種情況其實蠻多的,因爲接口設計的最小化只是理想狀態,難免會有一些實現類,對其中某些方法不感興趣,這時候,如果方法過多,子類也很多,並且子類的大部分方法都是空着的,那麼就可以採取這種方式了。

                 當然,這樣做違背了里氏替換原則,但是上面的做法原本就違背了接口的最小化原則,所以我們在真正使用時要權衡二者的利弊,到底我們需要的是什麼。所以從此也可以看出來,原則只是指導,並不一定也不可能全部滿足,所以我們一定要學會取捨。

                 總結下兩種實現方式的適配器所使用的場景,兩者都是爲了將已有類的代碼複用並且適配到客戶端需要的接口上去。

                 1,第一種類適配器,一般是針對適配目標是接口的情況下使用。

                 2,第二種對象適配器,一般是針對適配目標是類或者是需要複用的對象多於一個的時候使用,這裏再專門提示一下,對象適配器有時候是爲了將多個類一起適配,所以纔不得不使用組合的方式,而且我們採用對象適配器的時候,繼承也不是必須的,而是根據實際的類之間的關係來進行處理,上述例子當中一定要直接或間接的繼承自BaseEntity是爲了不破壞我們原來的繼承體系,但有些情況下這並不是必須的。

                 對於第三個缺省適配器,一般是爲了彌補接口過大所犯下的過錯,但是也請注意衡量利弊,權衡好以後再考慮是否要使用缺省適配器。

                 好了,本次適配器模式的分享就到此結束了,希望各位可以從中得到點收穫。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章