一篇文章帶你瞭解設計模式——結構型模式

秋落雨微涼發表於2023-02-02

一篇文章帶你瞭解設計模式——結構型模式

在之前的文章中我們已經介紹了設計模式中的建立者模式

下面我們來學習第二種型別的設計模式,結構型模式描述如何將類或物件按某種佈局組成更大的結構

它分為類結構型模式和物件結構型模式,前者採用繼承機制來組織介面和類,後者釆用組合或聚合來組合物件。

由於組合關係或聚合關係比繼承關係耦合度低,滿足“合成複用原則”,所以物件結構型模式比類結構型模式具有更大的靈活性。

下面我們會介紹到七種結構型模式:

  • 代理模式
  • 介面卡模式
  • 裝飾者模式
  • 橋接模式
  • 外觀模式
  • 組合模式
  • 享元模式

代理模式

首先我們先來介紹第一種結構型模式代理模式

代理模式簡述

代理模式概念:

  • 由於某些原因需要給某物件提供一個代理以控制對該物件的訪問。
  • 這時,訪問物件不適合或者不能直接引用目標物件,代理物件作為訪問物件和目標物件之間的中介。

代理模式分類:

  • Java中的代理按照代理類生成時機不同又分為靜態代理和動態代理。
  • 靜態代理代理類在編譯期就生成,而動態代理代理類則是在Java執行時動態生成;動態代理又有JDK代理和CGLib代理兩種。

代理模式結構

代理(Proxy)模式分為三種角色:

  • 抽象主題(Subject)類: 透過介面或抽象類宣告真實主題和代理物件實現的業務方法。
  • 真實主題(Real Subject)類: 實現了抽象主題中的具體業務,是代理物件所代表的真實物件,是最終要引用的物件。
  • 代理(Proxy)類 : 提供了與真實主題相同的介面,其內部含有對真實主題的引用,它可以訪問、控制或擴充套件真實主題的功能。

我們對上述角色做一個簡單舉例:

  • 抽象主題類:一個抽象類,具有賣票sell抽象方法
  • 真實主題類:火車售票站,負責售賣火車票,具有賣票sell方法
  • 代理類:地方代售處,負責代售火車票,具有sell方法,但sell方法中呼叫了真實主題類(火車售票站)的sell方法

靜態代理介紹

我們透過一個簡單的例子來介紹動態代理:

具體分析:

/*

【例】火車站賣票

如果要買火車票的話,需要去火車站買票,坐車到火車站,排隊等一系列的操作,顯然比較麻煩。
而火車站在多個地方都有代售點,我們去代售點買票就方便很多了。這個例子其實就是典型的代理模式,火車站是目標物件,代售點是代理物件。

*/

/* 程式碼展示 */

// 抽象主題類: 賣票介面
public interface SellTickets {
    void sell();
}

// 真實主題類: 火車站  
// 火車站具有賣票功能,所以需要實現SellTickets介面
public class TrainStation implements SellTickets {

    public void sell() {
        System.out.println("火車站賣票");
    }
}

// 代理類: 代售點
// 代售點具有賣票功能,所以需要實現SellTickets介面
public class ProxyPoint implements SellTickets {

    // 代售點最終還是使用的真實主題類,所以需要建立一個真實主題類,並呼叫其方法
    private TrainStation station = new TrainStation();

    // 在代理類中可以做一些功能增強操作
    public void sell() {
        System.out.println("代理點收取一些服務費用");
        station.sell();
    }
}

//測試類
public class Client {
    public static void main(String[] args) {
        ProxyPoint pp = new ProxyPoint();
        pp.sell();
    }
}

動態代理JDK

我們同樣採用上面的案例來進行JDK動態代理說明:

具體分析:

/*

JDK動態代理說明:
	Java中提供了一個動態代理類Proxy,Proxy並不是我們上述所說的代理物件的類
	而是提供了一個建立代理物件的靜態方法(newProxyInstance方法)來獲取代理物件。

*/

/* 程式碼展示 */

//賣票介面
public interface SellTickets {
    void sell();
}

//火車站  火車站具有賣票功能,所以需要實現SellTickets介面
public class TrainStation implements SellTickets {

    public void sell() {
        System.out.println("火車站賣票");
    }
}

//代理工廠,用來建立代理物件
public class ProxyFactory {

    // 我們需要一個真實主題類來獲得相關資訊
    private TrainStation station = new TrainStation();

    // 建立一個方法,用於生成代理物件
    public SellTickets getProxyObject() {
        
        //使用Proxy獲取代理物件
        
        /*
            newProxyInstance()方法引數說明:
                ClassLoader loader : 類載入器,用於載入代理類,使用真實物件的類載入器即可
                Class<?>[] interfaces : 真實物件所實現的介面,代理模式真實物件和代理物件實現相同的介面
                InvocationHandler h : 代理物件的呼叫處理程式
         */
        
        SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance(station.getClass().getClassLoader(),
                station.getClass().getInterfaces(),
                new InvocationHandler() {
                    /*
                    	InvocationHandler就是呼叫由factory工廠獲得的代理物件呼叫方法後所執行的內容
                    	採用匿名內部類的形式重寫invoke方法,下面書寫基本為固定形式
                    	
                        InvocationHandler中invoke方法引數說明:
                            proxy : 代理物件
                            method : 對應於在代理物件上呼叫的介面方法的 Method 例項
                            args : 代理物件呼叫介面方法時傳遞的實際引數
                     */
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                        // 增強操作
                        System.out.println("代理點收取一些服務費用(JDK動態代理方式)");
                        
                        //執行真實物件(代理物件呼叫什麼方法就會對應呼叫station的方法,並傳入args引數)
                        Object result = method.invoke(station, args);
                        return result;
                    }
                });
        return sellTickets;
    }
}

//測試類
public class Client {
    public static void main(String[] args) {
        //獲取代理物件
        ProxyFactory factory = new ProxyFactory();
        
        SellTickets proxyObject = factory.getProxyObject();
        proxyObject.sell();
    }
}

那麼我們給出一個問題:

  • ProxyFactory是代理類嗎?
  • ProxyFactory不是代理模式中所說的代理類,而代理類是程式在執行過程中動態的在記憶體中生成的類。

所以我們需要檢視真正的代理類的內部結構:

//程式執行過程中動態生成的代理類
public final class $Proxy0 extends Proxy implements SellTickets {
    private static Method m3;

    public $Proxy0(InvocationHandler invocationHandler) {
        super(invocationHandler);
    }

    // 這裡採用反射獲得方法
    static {
        m3 = Class.forName("com.itheima.proxy.dynamic.jdk.SellTickets").getMethod("sell", new Class[0]);
    }

    // 這裡對不同的方法呼叫不同的真實主題類的方法
    public final void sell() {
        this.h.invoke(this, m3, null);
    }
}

//Java提供的動態代理相關類
public class Proxy implements java.io.Serializable {
	protected InvocationHandler h;
	 
	protected Proxy(InvocationHandler h) {
        this.h = h;
    }
}

//代理工廠類
public class ProxyFactory {

    private TrainStation station = new TrainStation();

    public SellTickets getProxyObject() {
        SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance(station.getClass().getClassLoader(),
                station.getClass().getInterfaces(),
                new InvocationHandler() {
                    
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                        System.out.println("代理點收取一些服務費用(JDK動態代理方式)");
                        Object result = method.invoke(station, args);
                        return result;
                    }
                });
        return sellTickets;
    }
}


//測試訪問類
public class Client {
    public static void main(String[] args) {
        //獲取代理物件
        ProxyFactory factory = new ProxyFactory();
        SellTickets proxyObject = factory.getProxyObject();
        proxyObject.sell();
    }
}

最後我們給出JDK動態代理的邏輯順序:

1. 在測試類中透過代理物件呼叫sell()方法
2. 根據多型的特性,執行的是代理類($Proxy0)中的sell()方法
3. 代理類($Proxy0)中的sell()方法中又呼叫了InvocationHandler介面的子實現類物件的invoke方法
4. invoke方法透過反射執行了真實物件所屬類(TrainStation)中的sell()方法

動態代理CGLIB

我們首先簡單介紹一下CgLIB動態代理:

  • CGLIB是一個功能強大,高效能的程式碼生成包。
  • 它為沒有實現介面的類提供代理,為JDK的動態代理提供了很好的補充。

我們同樣採用之前的案例,但這次我們不需要賣票介面:

/* jar包展示*/

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>2.2.2</version>
</dependency>
    
/* 程式碼展示 */
    
// 火車站
public class TrainStation {

    public void sell() {
        System.out.println("火車站賣票");
    }
}

// 代理工廠
// 這裡需要繼承MethodInterceptor介面,因為我們下面的setCallback方法需要一個MethodInterceptor
// 我們直接在本類中重寫MethodInterceptor的intercept方法並傳入this本身即可
public class ProxyFactory implements MethodInterceptor {

    private TrainStation target = new TrainStation();

    public TrainStation getProxyObject() {
        //建立Enhancer物件,類似於JDK動態代理的Proxy類,下一步就是設定幾個引數
        Enhancer enhancer =new Enhancer();
        //設定父類的位元組碼物件
        enhancer.setSuperclass(target.getClass());
        //設定回撥函式(執行函式)
        enhancer.setCallback(this);
        //建立代理物件
        TrainStation obj = (TrainStation) enhancer.create();
        return obj;
    }

    /*
        intercept方法引數說明:
            o : 代理物件
            method : 真實物件中的方法的Method例項
            args : 實際引數
            methodProxy :代理物件中的方法的method例項
     */
    public TrainStation intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        // 增強操作
        System.out.println("代理點收取一些服務費用(CGLIB動態代理方式)");
        // 呼叫目標物件的方法,等價於target.sell方法;
        Object result = method.invoke(o,args);
        return result;
    }
}

// 測試類
public class Client {
    public static void main(String[] args) {
        //建立代理工廠物件
        ProxyFactory factory = new ProxyFactory();
        //獲取代理物件
        TrainStation proxyObject = factory.getProxyObject();

        proxyObject.sell();
    }
}

代理模式比較

首先我們給出靜態代理和動態代理的區別:

  • 動態代理最大的好處是介面中宣告的所有方法都被轉移到呼叫處理器一個集中的方法中處理InvocationHandler.invoke;這樣,在介面方法數量比較多的時候,我們可以進行靈活處理,而不需要像靜態代理那樣每一個方法進行中轉。
  • 如果介面增加一個方法,靜態代理模式除了所有實現類需要實現這個方法外,所有代理類也需要實現此方法。增加了程式碼維護的複雜度。而動態代理不會出現該問題。

然後我們給出JDK動態代理和CGLIB動態代理的區別:

  • 使用CGLib實現動態代理,CGLib底層採用ASM位元組碼生成框架,使用位元組碼技術生成代理類,在JDK1.6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能對宣告為final的類或者方法進行代理,因為CGLib原理是動態生成被代理類的子類。

  • 如果有介面使用JDK動態代理,如果沒有介面使用CGLIB代理。

代理模式分析

我們首先給出代理模式的適用場景:

  • 遠端(Remote)代理

    本地服務透過網路請求遠端服務。為了實現本地到遠端的通訊,我們需要實現網路通訊,處理其中可能的異常。為良好的程式碼設計和可維護性,我們將網路通訊部分隱藏起來,只暴露給本地服務一個介面,透過該介面即可訪問遠端服務提供的功能,而不必過多關心通訊部分的細節。

  • 防火牆(Firewall)代理

    當你將瀏覽器配置成使用代理功能時,防火牆就將你的瀏覽器的請求轉給網際網路;當網際網路返回響應時,代理伺服器再把它轉給你的瀏覽器。

  • 保護(Protect or Access)代理

    控制對一個物件的訪問,如果需要,可以給不同的使用者提供不同級別的使用許可權。

同時我們給出代理模式的優點:

  • 代理模式在客戶端與目標物件之間起到一箇中介作用和保護目標物件的作用;
  • 代理物件可以擴充套件目標物件的功能;
  • 代理模式能將客戶端與目標物件分離,在一定程度上降低了系統的耦合度;

最後我們給出代理模式的缺點:

  • 增加了系統的複雜度;

介面卡模式

接下來我們來介紹介面卡模式

介面卡模式簡述

首先我們給出介面卡模式的概述:

  • 將一個類的介面轉換成客戶希望的另外一個介面,使得原本由於介面不相容而不能一起工作的那些類能一起工作。
  • 例如我們的插座是三角插座,但是我們的插頭是二頭插頭,這時我們就需要一個三角轉兩頭的介面卡來幫助我們插上插頭

介面卡模式大致分為兩種:

  • 介面卡模式分為類介面卡模式和物件介面卡模式
  • 前者類之間的耦合度比後者高,且要求程式設計師瞭解現有元件庫中的相關元件的內部結構,所以應用相對較少些。

介面卡模式結構

介面卡模式(Adapter)包含以下主要角色:

  • 目標(Target)介面:當前系統業務所期待的介面,它可以是抽象類或介面。
  • 適配者(Adaptee)類:它是被訪問和適配的現存元件庫中的元件介面。
  • 介面卡(Adapter)類:它是一個轉換器,透過繼承或引用適配者的物件,把適配者介面轉換成目標介面,讓客戶按目標介面的格式訪問適配者。

類介面卡案例

我們透過一個案例來講述類介面卡:

具體分析:

/*

【例】讀卡器

現有一臺電腦只能讀取SD卡,而要讀取TF卡中的內容的話就需要使用到介面卡模式。建立一個讀卡器,將TF卡中的內容讀取出來。

分析:

1. Computer只能接收SDCard型別的類,並呼叫其readSD方法

2. SDCard是一個介面,SDCardImpl是其實現類

3. TFCard是一個介面,TFCardImpl是其實現類

4. SDAdapter是介面卡,我們如果希望發採用Computer去直接使用TFCard,那麼我們就需要一箇中介,但是Computer需要使用SDCard作為引數,所以我們的SDAdapter需要繼承SDCard介面或者類;又由於我們需要去使用TFCard,所以我們需要繼承TFCard來直接使用其方法

*/

/* 程式碼展示 */

//電腦類
public class Computer {

    public String readSD(SDCard sdCard) {
        if(sdCard == null) {
            throw new NullPointerException("sd card null");
        }
        return sdCard.readSD();
    }
}

//SD卡的介面
public interface SDCard {
    //讀取SD卡方法
    String readSD();
    //寫入SD卡功能
    void writeSD(String msg);
}

//SD卡實現類
public class SDCardImpl implements SDCard {
    public String readSD() {
        String msg = "sd card read a msg :hello word SD";
        return msg;
    }

    public void writeSD(String msg) {
        System.out.println("sd card write msg : " + msg);
    }
}

//TF卡介面
public interface TFCard {
    //讀取TF卡方法
    String readTF();
    //寫入TF卡功能
    void writeTF(String msg);
}

//TF卡實現類
public class TFCardImpl implements TFCard {

    public String readTF() {
        String msg ="tf card read msg : hello word tf card";
        return msg;
    }

    public void writeTF(String msg) {
        System.out.println("tf card write a msg : " + msg);
    }
}

//定義介面卡類(SD相容TF)
public class SDAdapterTF extends TFCardImpl implements SDCard {

    // 我們的介面卡繼承SDCard介面,實現其readSD方法,使其呼叫TFCard的readTF方法
    public String readSD() {
        System.out.println("adapter read tf card ");
        return readTF();
    }

    public void writeSD(String msg) {
        System.out.println("adapter write tf card");
        writeTF(msg);
    }
}

//測試類
public class Client {
    public static void main(String[] args) {
        Computer computer = new Computer();
        SDCard sdCard = new SDCardImpl();
        System.out.println(computer.readSD(sdCard));

        System.out.println("------------");

        SDAdapterTF adapter = new SDAdapterTF();
        System.out.println(computer.readSD(adapter));
    }
}

但是類介面卡模式違背了合成複用原則。

類介面卡是客戶類有一個介面規範的情況下可用,反之不可用。

物件介面卡案例

我們同樣採用之前的案例來講解物件介面卡:

具體分析:

/*

【例】讀卡器

我們使用物件介面卡模式將讀卡器的案例進行改寫。

分析:

1. 我們需要注意TFCard和SDAdapterTF的關係由繼承關係變為了聚合關係

2. 我們即使沒有SDCard介面,我們也可以直接繼承SDCardImpl物件,因為我們不需要顧及TFCard了

*/

/* 程式碼展示 */

//建立介面卡物件(SD相容TF)
public class SDAdapterTF  implements SDCard {

    // 我們直接將TFcard作為引數
    private TFCard tfCard;

    // 我們需要有參構造,為了保證存在TFCard,使其能夠呼叫readTF方法
    public SDAdapterTF(TFCard tfCard) {
        this.tfCard = tfCard;
    }

    public String readSD() {
        System.out.println("adapter read tf card ");
        return tfCard.readTF();
    }

    public void writeSD(String msg) {
        System.out.println("adapter write tf card");
        tfCard.writeTF(msg);
    }
}

//測試類
public class Client {
    public static void main(String[] args) {
        Computer computer = new Computer();
        SDCard sdCard = new SDCardImpl();
        System.out.println(computer.readSD(sdCard));

        System.out.println("------------");

        TFCard tfCard = new TFCardImpl();
        SDAdapterTF adapter = new SDAdapterTF(tfCard);
        System.out.println(computer.readSD(adapter));
    }
}

介面卡適用場景

最後我們給出介面卡的適用場景:

  • 以前開發的系統存在滿足新系統功能需求的類,但其介面同新系統的介面不一致。
  • 使用第三方提供的元件,但元件介面定義和自己要求的介面定義不同。

裝飾者模式

下面我們來介紹裝飾者模式

裝飾者模式簡述

我們直接給出裝飾者模式的概念:

  • 指在不改變現有物件結構的情況下,動態地給該物件增加一些職責(即增加其額外功能)的模式。

我們給出一個簡單的例子:

  • 我們在點餐時可以對餐品進行小料新增的操作,例如在炒米中新增火腿腸新增培根等操作

裝飾者模式結構

裝飾(Decorator)模式中的角色:

  • 抽象構件(Component)角色 :定義一個抽象介面以規範準備接收附加責任的物件。
  • 具體構件(Concrete Component)角色 :實現抽象構件,透過裝飾角色為其新增一些職責。
  • 抽象裝飾(Decorator)角色 : 繼承或實現抽象構件,幷包含具體構件的例項,可以透過其子類擴充套件具體構件的功能。
  • 具體裝飾(ConcreteDecorator)角色 :實現抽象裝飾的相關方法,並給具體構件物件新增附加的責任。

裝飾者模式案例

我們透過一個簡單的案例來介紹裝飾者模式:

具體分析:

/*

我們使用裝飾者模式對快餐店案例進行改進,體會裝飾者模式的精髓。

我們首先介紹上述角色:

1. FastFood:快餐,抽象構件據角色

2. FriedRice,FriedNoodles:炒米炒麵,具體構件角色

3. Garnish:小料,抽象裝飾角色

4. Egg,Bacon:雞蛋培根,具體裝飾角色

我們可以注意到Garnish不僅繼承了FastFood還聚合了FastFood,它所聚合的FastFood作為一個原型,自身FastFood作為一個裝飾來完成增強操作

*/

/* 程式碼展示 */

//快餐介面
public abstract class FastFood {
    
    // 價格+描述
    private float price;
    private String desc;

    // 無參構造
    public FastFood() {
    }

    // 有參構造
    public FastFood(float price, String desc) {
        this.price = price;
        this.desc = desc;
    }

    public void setPrice(float price) {
        this.price = price;
    }

    public float getPrice() {
        return price;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }
    
    //獲取價格
    public abstract float cost();
}

//炒飯
public class FriedRice extends FastFood {

    // 有參構造(賦值)
    public FriedRice() {
        super(10, "炒飯");
    }

    public float cost() {
        return getPrice();
    }
}

//炒麵
public class FriedNoodles extends FastFood {

    // 有參構造(賦值)
    public FriedNoodles() {
        super(12, "炒麵");
    }

    public float cost() {
        return getPrice();
    }
}

//配料類(採用抽象類,繼承FastFood使其具有FastFood的屬性方法,建立獨特的構造方法,使其在FastFood的基礎上做增強操作)
public abstract class Garnish extends FastFood {

    // 內建fastFood,用於儲存構件
    private FastFood fastFood;

    // 獲得構件
    public FastFood getFastFood() {
        return fastFood;
    }

    // 設定原構件
    public void setFastFood(FastFood fastFood) {
        this.fastFood = fastFood;
    }

    // 一個疊加方法,第一個引數是構件,後面的內容是裝飾者
    public Garnish(FastFood fastFood, float price, String desc) {
        super(price,desc);
        this.fastFood = fastFood;
    }
}

//雞蛋配料
public class Egg extends Garnish {

    // 新增方法,fastFood是構件,然後後面是裝飾者
    public Egg(FastFood fastFood) {
        super(fastFood,1,"雞蛋");
    }

    // 計算開銷,計算構件開銷+自身裝飾者開銷
    public float cost() {
        return getPrice() + getFastFood().getPrice();
    }

    // 給出具體描述,構件描述+裝飾者描述
    @Override
    public String getDesc() {
        return super.getDesc() + getFastFood().getDesc();
    }
}

//培根配料
public class Bacon extends Garnish {

    public Bacon(FastFood fastFood) {

        super(fastFood,2,"培根");
    }

    @Override
    public float cost() {
        return getPrice() + getFastFood().getPrice();
    }

    @Override
    public String getDesc() {
        return super.getDesc() + getFastFood().getDesc();
    }
}

//測試類
public class Client {
    public static void main(String[] args) {
        //點一份炒飯
        FastFood food = new FriedRice();
        //花費的價格
        System.out.println(food.getDesc() + " " + food.cost() + "元");

        System.out.println("========");
        //點一份加雞蛋的炒飯
        FastFood food1 = new FriedRice();

        food1 = new Egg(food1);
        //花費的價格
        System.out.println(food1.getDesc() + " " + food1.cost() + "元");

        System.out.println("========");
        //點一份加培根的炒麵
        FastFood food2 = new FriedNoodles();
        food2 = new Bacon(food2);
        //花費的價格
        System.out.println(food2.getDesc() + " " + food2.cost() + "元");
    }
}

裝飾者模式分析

首先我們給出裝飾者模式的適用場景:

  • 在不影響其他物件的情況下,以動態、透明的方式給單個物件新增職責。

  • 當物件的功能要求可以動態地新增,也可以再動態地撤銷時。

  • 當不能採用繼承的方式對系統進行擴充或者採用繼承不利於系統擴充套件和維護時。

    不能採用繼承的情況主要有兩類:

    • 第一類是系統中存在大量獨立的擴充套件,為支援每一種組合將產生大量的子類,使得子類數目呈爆炸性增長;
    • 第二類是因為類定義不能繼承(如final類)

然後我們給出裝飾者模式的優點:

  • 裝飾者模式可以帶來比繼承更加靈活性的擴充套件功能,使用更加方便,可以透過組合不同的裝飾者物件來獲取具有不同行為狀態的多樣化的結果。裝飾者模式比繼承更具良好的擴充套件性,完美的遵循開閉原則,繼承是靜態的附加責任,裝飾者則是動態的附加責任。
  • 裝飾類和被裝飾類可以獨立發展,不會相互耦合,裝飾模式是繼承的一個替代模式,裝飾模式可以動態擴充套件一個實現類的功能。

裝飾者模式區別

我們將裝飾者模式和代理模式做一個簡單的區分:

  • 相同點:
    • 都要實現與目標類相同的業務介面
    • 在兩個類中都要宣告目標物件
    • 都可以在不修改目標類的前提下增強目標方法
  • 不同點:
    • 目的不同
      裝飾者是為了增強目標物件
      靜態代理是為了保護和隱藏目標物件
    • 獲取目標物件構建的地方不同
      裝飾者是由外界傳遞進來,可以透過構造方法傳遞
      靜態代理是在代理類內部建立,以此來隱藏目標物件

橋接模式

下面我們來介紹橋接模式

橋接模式簡述

首先我們給出橋接模式的定義:

  • 將抽象與實現分離,使它們可以獨立變化。
  • 它是用組合關係代替繼承關係來實現,從而降低了抽象和實現這兩個可變維度的耦合度。

橋接模式結構

橋接(Bridge)模式包含以下主要角色:

  • 抽象化(Abstraction)角色 :定義抽象類,幷包含一個對實現化物件的引用。
  • 擴充套件抽象化(Refined Abstraction)角色 :是抽象化角色的子類,實現父類中的業務方法,並透過組合關係呼叫實現化角色中的業務方法。
  • 實現化(Implementor)角色 :定義實現化角色的介面,供擴充套件抽象化角色呼叫。
  • 具體實現化(Concrete Implementor)角色 :給出實現化角色介面的具體實現。

橋接模式案例

我們同樣透過一個簡單的案例來解釋橋接模式:

具體分析:

/*

【例】影片播放器

需要開發一個跨平臺影片播放器,可以在不同作業系統平臺(如Windows、Mac、Linux等)上播放多種格式的影片檔案,常見的影片格式包括RMVB、AVI、WMV等。該播放器包含了兩個維度,適合使用橋接模式。

*/

/* 程式碼展示 */

//影片檔案 (實現化)
public interface VideoFile {
    void decode(String fileName);
}

//avi檔案 (具體實現化)
public class AVIFile implements VideoFile {
    public void decode(String fileName) {
        System.out.println("avi影片檔案:"+ fileName);
    }
}
 
//rmvb檔案 (具體實現化)
public class REVBBFile implements VideoFile {

    public void decode(String fileName) {
        System.out.println("rmvb檔案:" + fileName);
    }
}

//作業系統版本 (抽象化角色)
public abstract class OperatingSystemVersion {

    // 內建一個實現化角色,後續子類可以呼叫該實現化角色的方法
    protected VideoFile videoFile;

    public OperatingSystemVersion(VideoFile videoFile) {
        this.videoFile = videoFile;
    }

    public abstract void play(String fileName);
}

//Windows版本 (擴充套件抽象化)
public class Windows extends OperatingSystem {

    public Windows(VideoFile videoFile) {
        super(videoFile);
    }

    public void play(String fileName) {
        videoFile.decode(fileName);
    }
}

//mac版本 (擴充套件抽象化)
public class Mac extends OperatingSystemVersion {

    public Mac(VideoFile videoFile) {
        super(videoFile);
    }

    public void play(String fileName) {
		videoFile.decode(fileName);
    }
}

//測試類
public class Client {
    public static void main(String[] args) {
        OperatingSystem os = new Windows(new AVIFile());
        os.play("戰狼3");
    }
}

橋接模式分析

我們首先給出橋接模式的適用場景:

  • 當一個類存在兩個獨立變化的維度,且這兩個維度都需要進行擴充套件時。
  • 當一個系統不希望使用繼承或因為多層次繼承導致系統類的個數急劇增加時。
  • 當一個系統需要在構件的抽象化角色和具體化角色之間增加更多的靈活性時。避免在兩個層次之間建立靜態的繼承聯絡,透過橋接模式可以使它們在抽象層建立一個關聯關係。

我們再給出橋接模式的好處:

  • 橋接模式提高了系統的可擴充性,在兩個變化維度中任意擴充套件一個維度,都不需要修改原有系統。

    如:如果現在還有一種影片檔案型別wmv,我們只需要再定義一個類實現VideoFile介面即可,其他類不需要發生變化。

  • 實現細節對客戶透明

外觀模式

下面我們來介紹外觀模式

外觀模式簡述

首先我們來簡單介紹一下外觀模式:

  • 門面模式,是一種透過為多個複雜的子系統提供一個一致的介面,而使這些子系統更加容易被訪問的模式。
  • 模式對外有一個統一介面,外部應用程式不用關心內部子系統的具體的細節,大大降低應用程式的複雜度,提高了程式的可維護性。
  • 外觀(Facade)模式是“迪米特法則”的典型應用

我們舉一個簡單的例子:

  • 當我們炒股時,我們需要去了解股票,股票有很多種,我們只有瞭解了才能去使用
  • 但是在股票之外有基金,我們如果想要炒股,可以直接投入基金中,讓基金幫我炒股
  • 就好比Client藉助了基金,即使不需要了解一個東西,我們也可以直接去使用該東西

外觀模式結構

外觀(Facade)模式包含以下主要角色:

  • 外觀(Facade)角色:為多個子系統對外提供一個共同的介面。
  • 子系統(Sub System)角色:實現系統的部分功能,客戶可以透過外觀角色訪問它。

外觀模式案例

我們同樣以一個簡單的案例來解釋外觀模式:

具體分析:

/*

【例】智慧家電控制

小明的爺爺已經60歲了,一個人在家生活:每次都需要開啟燈、開啟電視、開啟空調;睡覺時關閉燈、關閉電視、關閉空調;操作起來都比較麻煩。所以小明給爺爺買了智慧音響,可以透過語音直接控制這些智慧家電的開啟和關閉。

Client : 顧客

SmartApplicancesFacade : 智慧音響 外觀角色

Light,TV,AirCondition : 傢俱 子系統角色

*/

/* 程式碼展示 */

//智慧音響
public class SmartAppliancesFacade {

    // 外觀角色聚合所有子系統角色
    private Light light;
    private TV tv;
    private AirCondition airCondition;

    // 構造方法中進行初始化
    public SmartAppliancesFacade() {
        light = new Light();
        tv = new TV();
        airCondition = new AirCondition();
    }

    // 透過簡單命令使外觀角色一鍵控制所有子系統角色
    public void say(String message) {
        if(message.contains("開啟")) {
            on();
        } else if(message.contains("關閉")) {
            off();
        } else {
            System.out.println("我還聽不懂你說的!!!");
        }
    }

    //起床後一鍵開電器
    private void on() {
        System.out.println("起床了");
        light.on();
        tv.on();
        airCondition.on();
    }

    //睡覺一鍵關電器
    private void off() {
        System.out.println("睡覺了");
        light.off();
        tv.off();
        airCondition.off();
    }
}

//燈類
public class Light {
    public void on() {
        System.out.println("開啟了燈....");
    }

    public void off() {
        System.out.println("關閉了燈....");
    }
}

//電視類
public class TV {
    public void on() {
        System.out.println("開啟了電視....");
    }

    public void off() {
        System.out.println("關閉了電視....");
    }
}

//控制類
public class AirCondition {
    public void on() {
        System.out.println("開啟了空調....");
    }

    public void off() {
        System.out.println("關閉了空調....");
    }
}

//測試類
public class Client {
    public static void main(String[] args) {
        //建立外觀物件
        SmartAppliancesFacade facade = new SmartAppliancesFacade();
        //客戶端直接與外觀物件進行互動
        facade.say("開啟家電");
        facade.say("關閉家電");
    }
}

外觀模式分析

我們首先給出外觀模式的適用場景:

  • 對分層結構系統構建時,使用外觀模式定義子系統中每層的入口點可以簡化子系統之間的依賴關係。
  • 當一個複雜系統的子系統很多時,外觀模式可以為系統設計一個簡單的介面供外界訪問。
  • 當客戶端與多個子系統之間存在很大的聯絡時,引入外觀模式可將它們分離,從而提高子系統的獨立性和可移植性。

然後我們給出外觀模式的優點:

  • 降低了子系統與客戶端之間的耦合度,使得子系統的變化不會影響呼叫它的客戶類。
  • 對客戶遮蔽了子系統元件,減少了客戶處理的物件數目,並使得子系統使用起來更加容易。

最後給出外觀模式的缺點:

  • 不符合開閉原則,修改很麻煩

組合模式

下面我們來介紹組合模式

組合模式簡述

首先我們來簡單介紹一下組合模式:

  • 組合模式又名部分整體模式,是用於把一組相似的物件當作一個單一的物件。
  • 組合模式依據樹形結構來組合物件,用來表示部分以及整體層次。
  • 這種型別的設計模式屬於結構型模式,它建立了物件組的樹形結構。

組合模式結構

組合模式主要包含三種角色:

  • 抽象根節點(Component):定義系統各層次物件的共有方法和屬性,可以預先定義一些預設行為和屬性。
  • 樹枝節點(Composite):定義樹枝節點的行為,儲存子節點,組合樹枝節點和葉子節點形成一個樹形結構。
  • 葉子節點(Leaf):葉子節點物件,其下再無分支,是系統層次遍歷的最小單位。

組合模式案例

我們首先給出案例的示例圖:

然後我們給出上述案例的類圖:

具體分析:

/*

【例】軟體選單

如下圖,我們在訪問別的一些管理系統時,經常可以看到類似的選單。一個選單可以包含選單項(選單項是指不再包含其他內容的選單條目),也可以包含帶有其他選單項的選單,因此使用組合模式描述選單就很恰當,我們的需求是針對一個選單,列印出其包含的所有選單以及選單項的名稱。

MenuComponent: 檔案抽象類,裡面會定義所有可能出現的方法,對於相同方法書寫全部內容,對於非全包含的方法,採用throw異常的書寫方法

Menu:資料夾具體類,由於資料夾比檔案多一些方法,所以Menu會繼承父類的共性方法,然後重寫僅自己包含的方法

MenuItem: 檔案具體類,由於是檔案,方法較少,所以直接繼承父類即可,針對一些無法實現的功能父類已經為我們封裝了異常處理

*/

/* 程式碼展示 */

// 選單元件  不管是選單還是選單項,都應該繼承該類
public abstract class MenuComponent {

    // 檔名 + 檔案層級
    protected String name;
    protected int level;

    // 新增選單(只有資料夾有,所以以異常形式進行,當檔案繼承該類時直接繼承該方法,導致檔案無法新增選單,下列方法同)
    public void add(MenuComponent menuComponent){
        throw new UnsupportedOperationException();
    }

    // 移除選單
    public void remove(MenuComponent menuComponent){
        throw new UnsupportedOperationException();
    }

    // 獲取指定的子選單
    public MenuComponent getChild(int i){
        throw new UnsupportedOperationException();
    }

    // 獲取選單名稱(共性方法)
    public String getName(){
        return name;
    }

    public void print(){
        throw new UnsupportedOperationException();
    }
}

// 資料夾(重寫只有自己存在的方法)
public class Menu extends MenuComponent {

    private List<MenuComponent> menuComponentList;

    public Menu(String name,int level){
        this.level = level;
        this.name = name;
        menuComponentList = new ArrayList<MenuComponent>();
    }

    @Override
    public void add(MenuComponent menuComponent) {
        menuComponentList.add(menuComponent);
    }

    @Override
    public void remove(MenuComponent menuComponent) {
        menuComponentList.remove(menuComponent);
    }

    @Override
    public MenuComponent getChild(int i) {
        return menuComponentList.get(i);
    }

    @Override
    public void print() {

        for (int i = 1; i < level; i++) {
            System.out.print("--");
        }
        System.out.println(name);
        for (MenuComponent menuComponent : menuComponentList) {
            menuComponent.print();
        }
    }
}

// 檔案(全部繼承,部分方法以異常形式處理)
public class MenuItem extends MenuComponent {

    public MenuItem(String name,int level) {
        this.name = name;
        this.level = level;
    }

    @Override
    public void print() {
        for (int i = 1; i < level; i++) {
            System.out.print("--");
        }
        System.out.println(name);
    }
}

組合模式分類

在使用組合模式時,根據抽象構件類的定義形式,我們可將組合模式分為兩種形式:

  • 透明組合模式
  • 安全組合模式

我們首先來介紹透明組合模式:

  • 透明組合模式中,抽象根節點角色中宣告瞭所有用於管理成員物件的方法,比如在示例中 MenuComponent 宣告瞭add,remove, getChild 方法,這樣做的好處是確保所有的構件類都有相同的介面。透明組合模式也是組合模式的標準形式。

  • 透明組合模式的缺點是不夠安全,因為葉子物件和容器物件在本質上是有區別的,葉子物件不可能有下一個層次的物件,即不可能包含成員物件,因此為其提供 add()、remove() 等方法是沒有意義的,這在編譯階段不會出錯,但在執行階段如果呼叫這些方法可能會出錯(如果沒有提供相應的錯誤處理程式碼)

然後我們來介紹安全組合模式:

  • 在安全組合模式中,在抽象構件角色中沒有宣告任何用於管理成員物件的方法,而是在樹枝節點 Menu 類中宣告並實現這些方法。
  • 安全組合模式的缺點是不夠透明,因為葉子構件和容器構件具有不同的方法,且容器構件中那些用於管理成員物件的方法沒有在抽象構件類中定義,因此客戶端不能完全針對抽象程式設計,必須有區別地對待葉子構件和容器構件。

組合模式分析

首先我們給出組合模式的適用場景:

  • 組合模式正是應樹形結構而生,所以組合模式的使用場景就是出現樹形結構的地方。
  • 比如:檔案目錄顯示,多級目錄呈現等樹形結構資料的操作。

最後我們給出組合模式的優點:

  • 組合模式可以清楚地定義分層次的複雜物件,表示物件的全部或部分層次,它讓客戶端忽略了層次的差異,方便對整個層次結構進行控制。
  • 客戶端可以一致地使用一個組合結構或其中單個物件,不必關心處理的是單個物件還是整個組合結構,簡化了客戶端程式碼。
  • 在組合模式中增加新的樹枝節點和葉子節點都很方便,無須對現有類庫進行任何修改,符合“開閉原則”。
  • 組合模式為樹形結構的物件導向實現提供了一種靈活的解決方案,透過葉子節點和樹枝節點的遞迴組合,可以形成複雜的樹形結構,但對樹形結構的控制卻非常簡單。

享元模式

最後我們來介紹享元模式

享元模式簡述

首先我們來簡單介紹一下享元模式:

  • 運用共享技術來有效地支援大量細粒度物件的複用。
  • 它透過共享已經存在的物件來大幅度減少需要建立的物件數量、避免大量相似物件的開銷,從而提高系統資源的利用率。

享元模式結構

享元模式中存在以下兩種狀態:

  • 內部狀態:即不會隨著環境的改變而改變的可共享部分。

  • 外部狀態:指隨環境改變而改變的不可以共享的部分。享元模式的實現要領就是區分應用中的這兩種狀態,並將外部狀態外部化。

享元模式的主要有以下角色:

  • 抽象享元角色(Flyweight):通常是一個介面或抽象類,在抽象享元類中宣告瞭具體享元類公共的方法,這些方法可以向外界提供享元物件的內部資料(內部狀態),同時也可以透過這些方法來設定外部資料(外部狀態)。
  • 具體享元(Concrete Flyweight)角色 :它實現了抽象享元類,稱為享元物件;在具體享元類中為內部狀態提供了儲存空間。通常我們可以結合單例模式來設計具體享元類,為每一個具體享元類提供唯一的享元物件。
  • 非享元(Unsharable Flyweight)角色 :並不是所有的抽象享元類的子類都需要被共享,不能被共享的子類可設計為非共享具體享元類;當需要一個非共享具體享元類的物件時可以直接透過例項化建立。
  • 享元工廠(Flyweight Factory)角色 :負責建立和管理享元角色。當客戶物件請求一個享元物件時,享元工廠檢査系統中是否存在符合要求的享元物件,如果存在則提供給客戶;如果不存在的話,則建立一個新的享元物件。

享元模式案例

我們以一個簡單的案例來介紹享元模式:

具體分析:

/*

【例】俄羅斯方塊

下面的圖片是眾所周知的俄羅斯方塊中的一個個方塊,如果在俄羅斯方塊這個遊戲中,每個不同的方塊都是一個例項物件,這些物件就要佔用很多的記憶體空間,下面利用享元模式進行實現。

AbstractBox : 抽象方塊,定義了方塊的共性屬性和行為,其中getShape就是共性方法,其中color就屬於外部狀態,根據外部因素產生變化

*/

/* 程式碼展示 */

// 抽象享元角色
public abstract class AbstractBox {
    public abstract String getShape();

    public void display(String color) {
        System.out.println("方塊形狀:" + this.getShape() + " 顏色:" + color);
    }
}

// 具體享元角色
public class IBox extends AbstractBox {

    @Override
    public String getShape() {
        return "I";
    }
}

public class LBox extends AbstractBox {

    @Override
    public String getShape() {
        return "L";
    }
}

public class OBox extends AbstractBox {

    @Override
    public String getShape() {
        return "O";
    }
}

// 工廠類,負責管理享元角色,由於工廠只有一個,這裡以單例模式書寫工廠類,採用內部類懶漢式單例模式
public class BoxFactory {

    // 存放享元物件,若不存在加入,若存在直接使用
    private static HashMap<String, AbstractBox> map;

    // 初始化
    private BoxFactory() {
        map = new HashMap<String, AbstractBox>();
        AbstractBox iBox = new IBox();
        AbstractBox lBox = new LBox();
        AbstractBox oBox = new OBox();
        map.put("I", iBox);
        map.put("L", lBox);
        map.put("O", oBox);
    }

    // get方法
    public static final BoxFactory getInstance() {
        return SingletonHolder.INSTANCE;
    }

    // 單例物件建立
    private static class SingletonHolder {
        private static final BoxFactory INSTANCE = new BoxFactory();
    }

    public AbstractBox getBox(String key) {
        return map.get(key);
    }
}

享元模式分析

首先我們給出享元模式的適用場景:

  • 一個系統有大量相同或者相似的物件,造成記憶體的大量耗費。
  • 物件的大部分狀態都可以外部化,可以將這些外部狀態傳入物件中。
  • 在使用享元模式時需要維護一個儲存享元物件的享元池,而這需要耗費一定的系統資源,因此,應當在需要多次重複使用享元物件時才值得使用享元模式。

然後我們給出享元模式的優點:

  • 極大減少記憶體中相似或相同物件數量,節約系統資源,提供系統效能
  • 享元模式中的外部狀態相對獨立,且不影響內部狀態

最後給出享元模式的缺點:

  • 為了使物件可以共享,需要將享元物件的部分狀態外部化,分離內部狀態和外部狀態,使程式邏輯複雜

結束語

關於結構型模式我們就介紹到這裡,後面我會繼續更新二十三種設計模式,希望能給你帶來幫助~

附錄

該文章屬於學習內容,具體參考B站黑馬程式設計師的Java設計模式詳解

這裡附上影片連結:1.設計模式-結構型模式概述_嗶哩嗶哩_bilibili

相關文章