面向對象設計模式總結之四常見的設計模式(2)

一. 常見的軟件設計模式

接上一篇,我們說到面向對象設計模式總體來說23種設計模式分為三大類:

創建型模式(5種)

單例模式、建造者模式、工廠方法模式、抽象工廠模式、原型模式。

結構型模式(7種)

適配器模式、裝飾器模式、代理模式、外觀模式、橋接模式、組合模式、享元模式。

行為型模式(11種)

策略模式、模板方法模式、觀察者模式、迭代子模式、責任鏈模式、命令模式、備忘錄模式、狀態模式、訪問者模式、中介者模式、解釋器模式。

二. 適配器模式

1. 概念

將一個類的接口轉換成客戶希望的另外一個接口,從而使原本因接口不匹配而無法在一起工作的兩個類能夠在一起工作。
例子:筆記本電腦電源一般用的都是5V電壓,但是我們的家用電是220V,我們要讓筆記本充上電,最好的辦法應該是通過一個工具把220V的電壓轉換成5V,這個工具就是適配器。


2. 適配器模式涉及3個角色

  • 目標 Target(目標抽象類):目標抽象類定義客戶所需接口,可以是一個抽象類或接口,也可以是具體類,相當于插座。
  • 適配器 Adapter(適配器類):適配器可以調用另一個接口,作為一個轉換器,對Adaptee和Target進行適配,適配器類是適配器模式的核心,在對象適配器中,它通過繼承Target并關聯一個Adaptee對象使二者產生聯系,相當于插頭轉換器。
  • 源 Adaptee(適配者類):適配者即被適配的角色,它定義了一個已經存在的接口,這個接口需要適配,適配者類一般是一個具體類,包含了客戶希望使用的業務方法,在某些情況下可能沒有適配者類的源代碼,相當于插頭。

3. 適配器模式分類

適配器模式主要分為三類:類的適配器模式、對象的適配器模式、接口的適配器模式。

4. 類適配器模式

一般手機充電器輸出的直流電壓為5V,我們把交流電220V稱為源,希望得到的直流電5V稱為目標,而充電器即為適配器。

//源,交流電 已存在的、具有特殊功能、但不符合我們既有的標準接口的類
public class AC {
    public int outputAC(){
        return 220;
    }
}
//目標接口,或稱為標準接口,直流電
public interface IDC {
    int outputDC();
}
//適配器
public class ClsAdapter extends AC implements IDC{

    @Override
    public int outputDC() {
        return outputAC()/44;  //直流電為交流電的電壓值除以44
    }

    public static void main(String[] args) {
        ClsAdapter adapter = new ClsAdapter();
        System.out.println("交流電電壓:" + adapter.outputAC());
        System.out.println("直流電電壓:" + adapter.outputDC());
    }
}

/** 
輸出結果為:
交流電電壓:220
直流電電壓:5
*/

可以看到,類適配器是通過繼承源類,實現目標接口的方式實現適配的。但是,由于Java單繼承的機制,這就要求目標必須是接口,有一定的局限性。

5. 對象適配器模式

對象適配器,不是繼承源類,而是依據關聯關系,持有源類的對象,這也隱藏了源類的方法。在這里,適配器和源類的關系不是繼承關系,而是組合關系。

public class ObjAdapter implements IDC {
    //持有源類的對象
    private AC ac;

    public ObjAdapter(AC ac){
        this.ac = ac;
    }

    public int outputAC(){
        return ac.outputAC();
    }

    @Override
    public int outputDC() {
        return ac.outputAC()/44;
    }

    public static void main(String[] args) {
        ObjAdapter adapter = new ObjAdapter(new AC());
        System.out.println("交流電電壓:" + adapter.outputAC());
        System.out.println("直流電電壓:" + adapter.outputDC());
    }
}
//輸出結果同上

6.適配器模式優點

  • 客戶端通過適配器可以透明的調用目標接口。
  • 復用了現存的類,程序員不需要修改原有代碼而重用現有的適配者類。
  • 將目標類和適配類解耦,解決了目標類和適配類接口不一致的問題。

7.適配器模式缺點

  • 對類適配器來說,更換適配器的過程比較復雜。

需要注意的是,在設計階段,不要想著用適配器模式,這根本就是用于修改不合理的設置才產生的.并且過多地使用適配器,會讓系統非常零亂,不易整體進行把握。比如,明明看到調用的是 A 接口,其實內部被適配成了 B 接口的實現,一個系統如果太多出現這種情況,無異于一場災難。因此如果不是很有必要,可以不使用適配器,而是直接對系統進行重構。

三. 裝飾器模式

1. 概念

裝飾器模式( Decorator Pattern ) ,也稱為包裝模式( Wrapper Pattern )。裝飾器模式是一種結構型的設計模式。在不改變原有對象的基礎之上,將功能附加到對象上,擴展原有對象的功能。
使用該模式的目的是為了較為靈活的對類進行擴展,而且不影響原來類的結構。

2. 在裝飾器模式中的角色有:

抽象構件(Component)角色:給出一個抽象接口,已規范準備接收附加責任的對象。
具體構件(ConcreteComponent)角色:定義一個將要接收附加責任的類
裝飾(Decorator)角色:持有一個構件(Component)對象的實例,并定義一個與抽象構件接口一致的接口。
具體裝飾(ConcreteDecorator)角色:負責給構件對象“貼上”附加的責任。

3. 代碼實現

  1. 以繪制形狀為例,平時繪制的形狀五花八門,可能有圓形、矩形、三角形...等等,我們抽取出一個抽象的形狀接口:

    /**
     * 形狀接口
     */
    public interface Shape {
        //定義公用的形狀繪制方法
        void draw();
    }
    
  2. 接下來,假如我們要繪制一個圓形,會實現Shape接口,定義具體繪制圓形的方法:

         /**
         * 圓形類
         */
        public class Cicle implements Shape {
            @Override
            public void draw() {
                System.out.println("繪制一個圓形");
            }
        }
    
  3. 如果某一天,有一個特殊的需求,需要繪制一個紅色的圓形,但又不允許更改Circle類,如果是按繼承寫法會是如下:

       /**
        * 紅色的圓形
        */
       public class RedCircle extends Circle{
           @Override
           public void onDraw() {
               super.onDraw();
               System.out.println("給圓形填充上紅色");
           }
       }
    
  4. 效果是實現了,等到某一天,又來了個需求,要繪制一個帶邊框的紅色圓,且依然不能修改原來的類,那就繼續繼承RedCircle實現就行了...如此反復循環,繼承的具體形狀類會越來越多,且可能會嵌套了很多層,不利于代碼的維護。
    將以上改為裝飾器模式來實現的話,可以先定義一個抽象裝飾者來裝飾Shape:

    /**
     * 抽象裝飾者類,對Shape接口進行包裝 
     */
    public  class ShapeDecorator implements Shape {
        protected Shape shape;
    
        public ShapeDecorator(Shape shape) {
            this.shape = shape;
        }
    
        @Override
        public void draw() {
            shape.draw();
        }
    }
    
  5. 假如是實現一個帶邊框的紅色圓形.使用裝飾器來實現如下:

    /**
     * 創建一個繼承了抽象裝飾器類實體裝飾器類
     *
     * 具體的裝飾器類,實現具體要向被裝飾對象添加的功能。用來裝飾具體的組件對象或者另外一個具體的裝飾器對象
     *
     */
    public class RedOutlineCircleDecorator extends ShapeDecorator {
        public RedOutlineCircleDecorator(Shape shape) {
            //調用父類的構造方法,將參數傳給父類
            super(shape);
        }
    
        @Override
        public void draw() {
            //這里可以選擇性的調用父類的方法,如果不調用則相當于完全改寫了方法,
            super.draw();
            System.out.println("給圓形填充上紅色");
            System.out.println("給圓形畫上邊框");
        }
    }
    
    
  6. 第五步測試

    public static void main(String agrs[]){
       Shape circle = new Circle();
       //繪制一個帶邊框的紅色圓形
       RedOutlineCircleDecorator redOutlineCircleDecorator = new RedOutlineCircleDecorator(circle);
       redOutlineCircleDecorator.onDraw();
    }
    

優點:

  • 相比與靜態的繼承,裝飾器模式正如它定義的,那樣可以動態的給一個對象添加額外的職責, 顯得更加靈活。靜態繼承的情況下,如果要添加其他的功能就需要添加新的子類實現功能,然后相互之間繼承,以達到一個組合的功能,對于每一個要添加的功能都要,新建類,顯得特別麻煩,也使得系統越來越復雜,而對于裝飾器來說,為一個特定的Component提供多種不同的Decorator,對于一些要達成的功能,相互組合就可以達成目的。
  • 裝飾類和被裝飾類可以獨立發展,而不會相互耦合。
  • 裝飾模式是繼承關系的一個替代方案。我們看裝飾類Decorator,不管裝飾多少層,返回的對象還是Component,實現的還是is-a的關系。

缺點:
對于裝飾模式記住一點就足夠了:多層的裝飾是比較復雜的。為什么會復雜呢?你想想看,就像剝洋蔥一樣,你剝到了最后才發現是最里層的裝飾出現了問題,想象一下工作量吧,因此,盡量減少裝飾類的數量,以便降低系統的復雜度。

裝飾模式和前面的代理模式有點類似,容易把裝飾模式看成代理模式。裝飾模式是繼承的一種替代方案,主要為所裝飾的對象增強功能,動態的增加方法。而代理模式主要是為了控制對原有對象的訪問權限,不對原有對象進行功能增強。

四. 代理模式

1. 概念

代理(Proxy)是一種設計模式,提供了對目標對象另外的訪問方式;即通過代理對象訪問目標對象。這樣做的好處是:可以在目標對象實現的基礎上,增強額外的功能操作,即擴展目標對象的功能。這里使用到編程中的一個思想:不要隨意去修改別人已經寫好的代碼或者方法,如果需改修改,可以通過代理的方式來擴展該方法。

  • 中介隔離作用:在某些情況下,一個客戶類不想或者不能直接引用一個委托對象,而代理對象可以在客戶類和委托對象之間起到中介的作用(代理類和委托類實現相同的接口)。
  • 開放封閉原則:可以通過給代理類增加額外的功能來擴展委托類的功能,這樣只需要修改代理類而不需要再修改委托類,符合開閉原則。代理類本身并不真正實現服務,而是同過調用委托類的相關方法,來提供特定的服務。使用代理模式,可以在調用委托類業務功能的前后加入一些公共的服務(例如鑒權、計時、緩存、日志、事務處理等),甚至修改委托類的業務功能。

2. 代理模式主要角色

  • Subject(抽象主題角色):定義代理類和真實主題的公共對外方法,也是代理類代理真實主題的方法。比如:廣告、出售等。
  • RealSubject(真實主題角色):真正實現業務邏輯的類。比如實現了廣告、出售等方法的廠家(Vendor)。
  • Proxy(代理主題角色):用來代理和封裝真實主題。比如,同樣實現了廣告、出售等方法的超時(Shop)。

3. 代理模式的分類

代理可以分為靜態代理和動態代理。

  • 靜態代理是由程序員編寫代理類的源碼,再編譯代理類。所謂靜態也就是在程序運行前就已經存在代理類的字節碼文件,代理類和委托類的關系在運行前就已確定。
  • 動態代理是代理類的源碼是在程序運行期間由編譯器動態的生成(如JVM根據反射等機制生成代理類)。代理類和委托類的關系在程序運行時確定。

4.靜態代理

  1. 創建服務類接口
        // 定義接口
        public interface ShoesStore {
            void sell();
        }
    
  2. 實現服務接口
        // 真正的鞋子類
        public class NikeShoesStore implements ShoesStore {
            @Override
            public void sell() {
                System.out.println("賣出了一雙Nike鞋子");
            }
        }
    
  3. 創建代理類
        //微商 代理
        public class MicroSell implements ShoesStore {
            NikeShoesStore nikeShoesStore;
        
            public MicroSell(NikeShoesStore nikeShoesStore) {
                this.nikeShoesStore = nikeShoesStore;
            }
        
            @Override
            public void sell() {
                beforeSell();
                nikeShoesStore.sell();
                afterSell();
            }
        
            public void beforeSell() {
                System.out.println("買之前宣傳:我們的鞋子質量杠杠的,快來買一雙吧。");
            }
        
            public void afterSell() {
                System.out.println("買之后推銷:歡迎下次光臨");
            }
        }
    
  4. 編寫測試類
        public static void main(String[] args) {
            NikeShoesStore nikeShoes = new NikeShoesStore();
            MicroSell microSell = new MicroSell(nikeShoes);
            microSell.sell();
        }
    

優點:
可以做到在符合開閉原則的情況下對目標對象進行功能擴展。

缺點:
我們得為每一個服務都得創建代理類,工作量太大,不易管理。同時接口一旦發生改變,代理類也得相應修改。

4.JDK動態代理

在JDK動態代理中我們不再需要再手動的創建代理類,我們只需要編寫一個動態處理器就可以了。真正的代理對象由JDK再運行時為我們動態的來創建。

  1. 改造靜態代理的代碼為:
        public class DynamicMicroSell implements InvocationHandler {
            Object shoes;
        
            public DynamicMicroSell(Object shoes) {
                this.shoes = shoes;
            }
        
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                beforeSell();
                method.invoke(shoes);
                afterSell();
                return null;
            }
        
            public void beforeSell() {
                System.out.println("買之前宣傳:我們的鞋子質量杠杠的,快來買一雙吧。");
            }
        
            public void afterSell() {
                System.out.println("買之后推銷:歡迎下次光臨");
            }
        }
    
  2. 測試類:
        public static void main(String[] args) {
            NikeShoesStore realShoes = new NikeShoesStore();
            DynamicMicroSell dynamicMicroSell = new DynamicMicroSell(realShoes);
            ShoesStore nikeProxy = (ShoesStore) Proxy.newProxyInstance(NikeShoesStore.class.getClassLoader(), NikeShoesStore.class.getInterfaces(), dynamicMicroSell);
            nikeProxy.sell();
        }
    

可以看到并沒有像靜態代理那樣重新實現一個代理類,而是實現了 InvocationHandler 接口的invoke方法實現的代理。通過Proxy.newProxyInstance()創建了一個代理類來執行sell方法。
此時如果想要拓展一個阿迪鞋子的接口,很簡單:

  1. 新建一個AdidasShoes還是實現ShoesStore接口
        public class AdidasShoes implements ShoesStore {
            @Override
            public void sell() {
                System.out.println("賣出去一雙阿迪~");
            }
        }
    
  2. 測試類:
        public static void main(String[] args) {
            AdidasShoes adidasShoes = new AdidasShoes();
            DynamicMicroSell dynamicMicroSell1 = new DynamicMicroSell(adidasShoes);
            Shoes shoes = (Shoes) Proxy.newProxyInstance(AdidasShoes.class.getClassLoader(), AdidasShoes.class.getInterfaces(), dynamicMicroSell1);
            shoes.sell();
        }
    

缺點:
雖然相對于靜態代理,動態代理大大減少了我們的開發任務,同時減少了對業務接口的依賴,降低了耦合度。但是還是有一點點小小的遺憾之處,那就是它始終無法擺脫僅支持interface代理,因為它的設計注定了這個遺憾。

3.CGLIB代理

CGLIB代理的作用

JDK實現動態代理需要實現類通過接口定義業務方法,對于沒有接口的類,如何實現動態代理呢,這就需要CGLib了。
Cglib(Code Generation Library)是個功能強大、高性能、開源的代碼生成包,它可以為沒有實現接口的類提供代理。運行時在內存中動態生成一個子類對象從而實現對目標對象功能的擴展。
CGLib采用了非常底層的字節碼技術,其原理是通過字節碼技術為一個類創建子類,并在子類中采用方法攔截的技術攔截所有父類方法的調用,順勢織入橫切邏輯。
但因為采用的是繼承,所以不能對final修飾的類進行代理。JDK動態代理與CGLib動態代理均是實現Spring AOP的基礎。
Cglib動態代理的特點如下:

  • 需要引入Cglib的jar文件。
  • CGLIB是一個強大的高性能的代碼生成包,它可以在運行期擴展Java類與實現Java接口。
    它廣泛的被許多AOP的框架使用,例如Spring AOP和dynaop,為他們提供方法的interception(攔截)。
  • CGLIB包的底層是通過使用一個小而快的字節碼處理框架ASM,來轉換字節碼并生成新的類。
    不鼓勵直接使用ASM,因為它需要你對JVM內部結構包括class文件的格式和指令集都很熟悉。
  • 委托類不能為final,否則報錯java.lang.IllegalArgumentException: Cannot subclass final class xxx。
  • 不會攔截委托類中無法重載的final/static方法,而是跳過此類方法只代理其他方法。
cglib與JDK動態代理的區別
  • 使用動態代理的對象必須實現一個或多個接口
  • 使用cglib代理的對象則無需實現接口,達到代理類無侵入。

4. 動態代理在AOP中的應用

AOP(Aspect Orient Programming),我們一般稱為面向方面(切面)編程,作為面向對象的一種補充,用于處理系統中分布于各個模塊的橫切關注點,比如事務管理、日志、緩存等等。
AOP可以兩種動態代理來實現攔截切入功能:jdk動態代理和cglib動態代理。兩種方法同時存在,各有優劣。 jdk動態代理是由java內部的反射機制來實現的,cglib動態代理底層則是借助asm來實現的。
總的來說,反射機制在生成類的過程中比較高效,執行時候通過反射調用委托類接口方法比較慢;而asm在生成類之后的相關代理類執行過程中比較高效(可以通過將asm生成的類進行緩存,這樣解決asm生成類過程低效問題)。
還有一點必須注意:jdk動態代理的應用前提,必須是委托類基于統一的接口。如果沒有上述前提,jdk動態代理不能應用。 由此可以看出,jdk動態代理有一定的局限性,cglib這種第三方類庫實現的動態代理應用更加廣泛,且在效率上更有優勢。

AOP實現主要分為 靜態代理 和 動態代理 。

  • 靜態代理 主要是 AspectJ
  • 動態代理 主要是 Spring AOP

五. 觀察者模式

1. 概念

觀察者模式(Observer),又叫發布-訂閱模式(Publish/Subscribe),定義對象間一種一對多的依賴關系,使得每當一個對象改變狀態,則所有依賴于它的對象都會得到通知并自動更新。
被觀察者和觀察者一般是一對多的關系,一個被觀察者對應多個觀察者,當一個被觀察者的狀態發生改變時,被觀察者通知觀察者, 然后可以在觀察者內部 進行業務邏輯的處理。

2. 觀察者模式的主要角色

Subject(抽象的被觀察者)
是一個接口(實際上也有人使用抽象類),主要包含了3個方法:

  1. addObserver方法可以添加觀察者對象,可以理解為觀察者把自己注冊到了被觀察者這里,只有注冊了的觀察者,才能接到被觀察者的通知。
  2. deleteObserver方法是將觀察者移除,被移除的觀察者自然就不能再接到通知了。
  3. notifyObserves方法可以把通知發送給所有的已注冊的觀察者,至于觀察者們后續做什么事情,被觀察者是完全不關心的。

Observer(抽象的觀察者)
也是一個接口,必須實現的只有一個通知方法,被觀察者通過調用這個方法,讓觀察者了解到事件的發生。

ConcreteSubject(被觀察者的具體實現)
實現抽象被觀察者接口,是被觀察者的具體業務邏輯,另外還需要一個存儲觀察者的集合。當主題的內部狀態改變時,向所有集合中的觀察者發出通知。

ConcreteObserver(觀察者的具體實現)
實現抽象觀察者接口,處理不同具體觀察者的不同業務邏輯。

3. 代碼實現:

  1. 抽象的被觀察者:
        /**
         * 被觀察者觀察者抽象接口(發布者、被觀察者)
         */
        public interface Observerable {
            /**
             * 注冊觀察者
             */
            void addObserver(Observer observer);
        
            /**
             * 移除觀察者
             */
            void deleObserver(Observer observer);
        
            /**
             * 通知觀察者
             */
            void notifyObserver();
        }
    
  2. 抽象的觀察者:
        /**
         * 觀察者抽象接口
         */
        public interface Observer {
            /**
             * 接收變動通知
             */
            void upData(String message);
        }
        
    
  3. 被觀察者的具體實現:
        public class NewServer implements Observerable {
            private List<Observer> mObservers = new ArrayList<>();
            private String news;
        
            @Override
            public void addObserver(Observer observer) {
                mObservers.add(observer);
            }
        
            @Override
            public void deleObserver(Observer observer) {
                mObservers.remove(observer);
            }
        
            @Override
            public void notifyObserver() {
                for (Observer mObserver : mObservers) {
                    mObserver.upData(news);
                }
            }
        
            public void setNews(String news) {
                this.news = news;
                notifyObserver();
            }
        }
    
  4. 觀察者的具體實現:
        public class User implements Observer {
            private String name;
        
            public User(String name) {
                this.name = name;
            }
        
            @Override
            public void upData(String news) {
                System.out.println("我是觀察者" + name + "我收到的消息是:" + news);
            }
        }
    
  5. 測試類:
        public static void main(String[] args) {
            NewServer wechatServer = new NewServer();
            User user = new User("張三");
            User user1 = new User("李四");
            User user2 = new User("王五");
            wechatServer.addObserver(user);
            wechatServer.addObserver(user1);
            wechatServer.addObserver(user2);
            wechatServer.setNews("國乒勇奪冠軍!");
            wechatServer.deleObserver(user1);
            wechatServer.setNews("國足慘遭淘汰!");
        }
    

結果:


4. 優點

  • 觀察者和被觀察者之間建立一個抽象的耦合,降低了目標與觀察者之間的耦合關系。
  • 觀察者模式支持廣播通訊的觸發機制。

5. 缺點

  • 如果一個被觀察者對象有很多的直接和間接的觀察者的話,將所有的觀察者都通知到會花費很多時間。
  • 如果觀察者和觀察目標間有循環依賴,可能導致系統崩潰。
  • 沒有相應的機制讓觀察者知道所觀察的目標對象是怎么發生變化的。

6. 使用場景

  • 關聯行為場景。
  • 事件多級觸發場景。
  • 跨系統的消息變換場景,如消息隊列的處理機制。

參考資料:
Android開發之設計模式-適配器模式
Android 適配器模式(ListView與Adapter)
android設計模式二十三式(七)——裝飾器模式(Decorator)
談談 23 種設計模式在 Android 項目中的應用

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容