超詳細-七種常見結構型模式的描述總結與程式碼分析

農夫三拳有點疼~發表於2020-04-18

結構型模式(Structural Pattern)用於將類或物件結合在一起形成更強大的結構,就像搭積木,可以通過簡單的積木組合出複雜、功能強大的模型。

結構型模式 重要程度
介面卡模式(Adapter) ⭐⭐⭐⭐
橋接模式(Bridge) ⭐⭐⭐
組合模式(Composite) ⭐⭐⭐⭐
裝飾者模式(Decorator) ⭐⭐⭐
外觀模式(Facade) ⭐⭐⭐⭐⭐
享元模式(Flyweight)
代理模式(Proxy) ⭐⭐⭐⭐

一、介面卡模式(Adapter)

生活中,充電插頭有兩腳的、三腳的,還有圓形的,如果想使這些插頭都能工作,就需要一個多功能介面卡

基本介紹

介面卡模式(Adapter Pattern)屬於結構性模式,它可以將某個類的介面轉換為客戶端期望的另一個介面表示,主要目的是相容性,讓原本因介面不匹配不能一起工作的兩個類可以協同工作,其別名為包裝器(Wrapper)。介面卡模式主要分為三類:類介面卡模式物件介面卡模式介面介面卡模式

工作原理

  1. 讓原本介面不相容的類可以相容
  2. 從使用者的角度看不到被適配者,是解耦的
  3. 使用者呼叫介面卡轉化出來的目標介面方法,介面卡去再呼叫被適配者的相關介面方法

類介面卡模式

實現原理

Adapter 類繼承 src 類,實現 dst 介面,完成 src 對 dst 的適配。

案例

插座(Voltage220V)的輸出電壓是220V,充電插頭(Voltage5V)輸出電壓是5V,這時候就需要一個介面卡(VoltageAdapter)轉換電壓,才能給手機(Phone)充電

程式碼實現

電源輸出電壓為220V

public class Voltage220V {
    public int output220V() {
        int src = 220;
        System.out.println("電源輸出" + src + "V");
        return src;
    }
}

充電器輸出電壓為5V

public interface Voltage5V {
    int output5V();
}

介面卡需要將220V轉為5V

public class VoltageAdapter extends Voltage220V implements Voltage5V {
    @Override
    public int output5V() {
        int src = super.output220V();
        int dst = src / 44;
        System.out.println("轉換為" + dst + "V");
        return dst;
    }
}

手機接收5V電壓,判斷電壓是否為5V

public class Phone {
    public static void charging(Voltage5V voltage5V){
        int v = voltage5V.output5V();
        if(v == 5){
            System.out.println("接收電壓為5V,正常充電");
        }else if(v > 5){
            System.out.println("電壓高於5V,無法充電");
        }
    }
}

測試方法

@Test
public void test01(){
    System.out.println("====類介面卡模式====");
    Phone.charging(new VoltageAdapter());
}

執行結果

====類介面卡模式====
電源輸出220V
轉換為5V
接收電壓為5V,正常充電

分析

  • 由於 Java 是單繼承機制,所以類介面卡模式有一定的侷限性
  • src 類的方法再 Adapter 中都會暴露出來,增加了使用的成本
  • 由於繼承了 src 類,所以它可以重寫父類方法,使 Adapter 的靈活性增強了

物件介面卡模式

實現原理

基本的思路和類的介面卡模式相同,只是將 Adapter 類做修改,使用聚合關係替代繼承關係

程式碼實現

沿用前面的程式碼,新建一個介面卡,只是將原來的 Adapter 繼承 src 類換為聚合的關係

public class VoltageAdapter2 implements Voltage5V {

    private Voltage220V voltage220V;

    public VoltageAdapter2(){
        this.voltage220V = new Voltage220V();
    }

    @Override
    public int output5V() {
        int src = this.voltage220V.output220V();
        int dst = src / 44;
        return dst;
    }
}

測試方法

@Test
public void test02(){
    System.out.println("====物件介面卡模式====");
    Phone.charging(new VoltageAdapter2(new Voltage220V()));
}

執行結果

====物件介面卡模式====
電源輸出220V
轉換為5V
接收電壓為5V,正常充電

介面介面卡模式

介面介面卡模式也可稱為預設介面卡模式,當不需要實現介面的全部方法時,可先設計一個抽象類實現介面,併為該介面的每個方法都提供一個預設實現,那麼該抽象類的子類就可以有選擇的覆蓋父類的某些方法來實現需求。

適用於一個介面不想使用其所有的方法的情況

程式碼實現

寫一個介面,裡面定義一些方法

public interface InterfaceMethod {
    void m1();
    void m2();
    void m3();
    void m4();
}

一個抽象類,實現該介面

public abstract class AbstractAdapter implements InterfaceMethod {
    @Override
    public void m1() {
    }

    @Override
    public void m2() {
    }

    @Override
    public void m3() {
    }

    @Override
    public void m4() {
    }
}

測試方法

@Test
public void test(){
    //使用匿名內部類的方式
    AbstractAdapter adapter = new AbstractAdapter() {
        @Override
        public void m1() {
            System.out.println("我要用m1方法");
        }
    };
    adapter.m1();
}

執行結果

我要用m1方法

三種介面卡模式總結

  • 三種命名方式是根據 src 是以怎樣的形式給到 Adapter (在Adapter裡的形式)來命名的。
    • 類介面卡:以類給到,在 Adapter 裡,就是將 src 當做類,繼承

    • 物件介面卡:以物件給到,在 Adapter 裡, 將 src 作為一個物件,持有

    • 介面介面卡:以介面給到,在 Adapter 裡,將 src 作為一個介面,實現

  • Adapter模式最大的作用還是將原本不相容的介面融合在一起工作。

二、橋接模式(Bridge)

基本介紹

  • 橋接模式是一種結構型設計模式。
  • 將實現與抽象放在兩個不同的類層次中,使兩個層次可以獨立改變。
  • 基於類的最小設計原則,通過封裝、聚合、繼承等行為讓不同的類承擔不同的職責。
  • 它的主要特點是把抽象與行為實現分離,從而可以保持各部分的獨立性以及應對它們的功能擴充套件。

模式結構

橋接模式包含如下角色:

  • Abstraction:抽象類
  • RefinedAbstraction:擴充抽象類
  • Implementor:實現類介面
  • ConcreteImplementor:具體實現類

簡單案例

我們以手機為例,手機有品牌(諾基亞、摩托羅拉)和樣式(摺疊式、直立式),我們需要生產不同的品牌和樣式,比如摺疊式諾基亞、直立式摩托羅拉... ...

「實現類介面」 - 手機品牌,都有開機和關機的功能

public interface PhoneBrand {
    void open();
    void close();
}

「具體實現類」 - 手機品牌 Nokia 和 Moto

public class Nokia implements PhoneBrand {
    @Override
    public void open() {
        System.out.println("諾基亞開機...");
    }

    @Override
    public void close() {
        System.out.println("諾基亞關機...");
    }
}
public class Moto implements PhoneBrand {
    @Override
    public void open() {
        System.out.println("摩托羅拉開機...");
    }

    @Override
    public void close() {
        System.out.println("摩托羅拉關機...");
    }
}

「抽象類」 - 手機類,以聚合的方式與品牌產生聯絡,充當著“橋”的角色

public abstract class AbsPhone{

    private PhoneBrand brand;

    public AbsPhone(PhoneBrand brand) {
        this.brand = brand;
    }

    protected void open(){
        brand.open();
    }

    protected void close(){
        brand.close();
    }
}

「擴充抽象類」 - 摺疊式手機 和 直立式手機

public class FoldingPhone extends AbsPhone{

    public FoldingPhone(PhoneBrand brand) {
        super(brand);
    }

    @Override
    protected void open() {
        System.out.print("摺疊式 - ");
        super.open();
    }

    @Override
    protected void close() {
        System.out.print("摺疊式 - ");
        super.close();
    }
}
public class UpRightPhone extends AbsPhone{

    public UpRightPhone(PhoneBrand brand) {
        super(brand);
    }

    @Override
    protected void open() {
        System.out.print("直立式 - ");
        super.open();
    }

    @Override
    protected void close() {
        System.out.print("直立式 - ");
        super.close();
    }
}

測試

@Test
public void test(){
    AbsPhone p1 = new FoldingPhone(new Nokia());
    p1.open();
    p1.close();
    System.out.println();
    AbsPhone p2 = new UpRightPhone(new Moto());
    p2.open();
    p2.close();
}

結果

摺疊式 - 諾基亞開機...
摺疊式 - 諾基亞關機...

直立式 - 摩托羅拉開機...
直立式 - 摩托羅拉關機...

如果我們想建立其他型別的手機,只需要改變建立方式即可。

模式分析

  1. 實現了抽象和實現部分的分離,從而極大的提供了系統的靈活性,這有助於系統進行分層設計,從而產生更好的結構化系統。
  2. 對於系統的高層部分,只需要知道抽象部分和實現部分的介面就可以了,其它的部分由具體業務來完成。
  3. 橋接模式替代多層繼承方案,可以減少子類的個數,降低系統的管理和維護成本。
  4. 橋接模式的引入增加了系統的理解和設計難度,由於聚合關聯關係建立在抽象層,要求開發者針對抽象進行設計和程式設計。
  5. 橋接模式要求正確識別出系統中兩個獨立變化的維度,因此其使用範圍有一定的侷限性,即需要有這樣的應用場景。

橋接模式在 JDBC 中的應用

在 Java 中我們通常使用 JDBC 連線資料庫,但是資料庫的種類有很多(MySQL、Oracle...),它們的連線方式、協議都不盡相同,很顯然不能為每種資料庫都寫一個介面,這樣就違背了精簡設計原則,於是Java設計師就提供一套介面給廠商們自己實現,一套介面給使用者呼叫。

我們在使用 JDBC 的時候需要寫這樣的程式碼

Class.forName("資料庫驅動名");
Connection conn = DriverManager.getConnection("資料庫url", "使用者名稱", "密碼");

其過程是這樣的:

  • Class.forName() 的時候,通過反射機制,將 .class 檔案載入進Java虛擬機器記憶體中,Driver 類初始化,執行以下程式碼,向 DriverManager 中註冊一個驅動。DriverManager是個 Driver 容器,管理不同的 Driver

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
    
  • 我們獲取連線時,DriverManager 就會根據驅動返回一個相應的資料庫連線

    @CallerSensitive
    public static Connection getConnection(String url,
        java.util.Properties info) throws SQLException {
        return (getConnection(url, info, Reflection.getCallerClass()));
    }
    

實際應用場景

對於那些不希望使用繼承或因為多層次繼承導致系統類的個數急劇增加的系統,橋接模式尤為適用。

  • 銀行轉賬系統
    • 轉賬分類:網上轉賬,櫃檯轉賬,AMT 轉賬
    • 轉賬使用者型別:普通使用者,銀卡使用者,金卡使用者...
  • 訊息管理
    • 訊息型別:即時訊息,延時訊息
    • 訊息分類:手機簡訊,郵件訊息,QQ 訊息...

三、組合模式(Composite)

基本介紹

1、組合模式(Composite Pattern)又叫部分整體模式,他建立了物件組的樹形結構,將物件組合成樹狀結構以表示「整體 - 部分」的層次關係。

2、組合模式使得使用者對單個物件和組合物件的訪問具有一致性,即:組合能讓客戶以一致的方式處理個別物件以及組合物件

模式結構

Component(抽象構件):定義參加組合物件的公有方法和屬性,可以定義一些預設的行為和屬性。

Composite(容器構件):樹枝物件,它的作用是組合樹枝結點和葉子結點形成一個樹形結構。

Leaf(葉子構件):葉子構件的下面沒有其他分支,也就是遍歷的最小單位。


組合模式有兩種實現:安全模式和透明模式,其結構如下圖所示

  • 安全組合模式:在抽象構件角色中沒有宣告任何用於管理成員物件的方法,而是在容器構件 Composite 類中宣告並實現這些方法。
  • 透明組合模式:抽象構建角色中宣告瞭所有用於管理成員物件的方法,對其它構件公開透明。

簡單案例

要求:在頁面展示出公司的部門組成(一個公司有多個部門,每個部門有多個小組);

這是一種很明顯的樹形結構,因此可以用組合模式解決

「抽象構件」:OrganizationComponent

public abstract class OrganizationComponent {
    private String name;

    public OrganizationComponent(String name) {
        this.name = name;
    }

    protected void add(OrganizationComponent component) {
        throw new UnsupportedOperationException("不支援新增操作");
    }

    protected void remove(OrganizationComponent component) {
        throw new UnsupportedOperationException("不支援刪除操作");
    }

    protected abstract void print();


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

「容器構件」:Company、Department

public class Company extends OrganizationComponent {
    private List<OrganizationComponent> components = new ArrayList<>();

    public Company(String name) {
        super(name);
    }

    @Override
    protected void add(OrganizationComponent component) {
        components.add(component);
    }

    @Override
    protected void remove(OrganizationComponent component) {
        components.remove(component);
    }

    @Override
    protected void print() {
        System.out.println("======="+getName()+"=======");
        for (OrganizationComponent component : components) {
            component.print();
        }
    }

    @Override
    public String getName() {
        return super.getName();
    }
}
public class Department extends OrganizationComponent {
    private List<OrganizationComponent> components = new ArrayList<>();

    public Department(String name) {
        super(name);
    }

    @Override
    protected void add(OrganizationComponent component) {
        components.add(component);
    }

    @Override
    protected void remove(OrganizationComponent component) {
        components.remove(component);
    }

    @Override
    protected void print() {
        System.out.println("======="+getName()+"=======");
        for (OrganizationComponent component : components) {
            component.print();
        }
    }

    @Override
    public String getName() {
        return super.getName();
    }
}

「葉子構件」:Group,葉子構件不沒有子節點了,所以不需要新增、刪除之類的方法

public class Group extends OrganizationComponent {
    public Group(String name) {
        super(name);
    }

    @Override
    protected void print() {
        System.out.println(getName());
    }

    @Override
    public String getName() {
        return super.getName();
    }
}

「測試類」:Client

public class Client {
    @Test
    public void test01(){
        OrganizationComponent company = new Company("阿里巴巴");

        OrganizationComponent department1 = new Department("市場部");
        OrganizationComponent department2 = new Department("技術部");

        OrganizationComponent group1 = new Group("市場一組");
        OrganizationComponent group2 = new Group("市場二組");
        OrganizationComponent group3 = new Group("技術一組");
        OrganizationComponent group4 = new Group("技術二組");

        //新增部門
        company.add(department1);
        company.add(department2);
        //新增小組
        department1.add(group1);
        department1.add(group2);
        department2.add(group3);
        department2.add(group4);

        //列印結果
        company.print();
    }
}

「執行結果」

=======阿里巴巴=======
=======市場部=======
市場一組
市場二組
=======技術部=======
技術一組
技術二組

在 HashMap 中的應用

在 Java(jdk 1.8為例) 的集合類 HashMap 中,抽象構件是 Map,容器構件是 HashMap,葉子構件是 Node

進入原始碼可以看見,在 Map 中定義了許多公共方法

HashMap 實現了 Map,並對一些方法重寫,而且 HashMap 中有一個靜態內部類 Node,它就充當了葉子構件的角色,Node 中去除了 put、putAll 等方法,下面也沒有子結點了

使用:

@Test
public void test02(){
    Map<String, String> map = new HashMap<>();
    map.put("k1", "v1");
    map.put("k2", "v2");
    System.out.println(map);
}

當我們 put 一個鍵值對的時候,在 HashMap 內部會呼叫 putVal 方法,將鍵值對封裝為 Node。

總結

1、簡化客戶端操作。客戶端只需要面對一致的物件而不用考慮整體部分或者節點葉子的問題。

2、具有較強的擴充套件性。當我們要更改組合物件時,我們只需要調整內部的層次關係,客戶端不用做出任何改動。

3、方便建立出複雜的層次結構。客戶端不用理會組合裡面的組成細節,容易新增節點或者葉子從而建立出複雜的樹形結構。

4、需要遍歷組織機構,或者處理的物件具有樹形結構時,非常適合使用組合模式。

5、要求較高的抽象性。如果節點和葉子有很多差異性的話,比如很多方法和屬性都不一樣,不適合使用組合模式。

四、裝飾者模式(Decorator)

基本

裝飾者模式屬於結構型模式,它可以動態的將新功能附加到物件上,同時又不改變其結構。在物件功能擴充套件方面,它比繼承更有彈性,裝飾者模式也體現了開閉原則(OCP)。

模式結構

裝飾者和被裝飾者有相同的超型別,因為裝飾者和被裝飾者必須是一樣的型別,利用繼承是為了達到型別的匹配,而不是利用繼承獲取行為

  • Component:裝飾者和被裝飾者共同的父類,是一個介面或者抽象類,用來定義基本行為
  • ConcreteComponent:定義具體物件,即被裝飾者
  • Decorator:抽象裝飾者,繼承自 Component,從外類來擴充套件 ConcreteComponent。對於 ConcreteComponent來說,不需要知道Decorator的存在,Decorator 是一個介面或抽象類
  • ConcreteDecorator:具體裝飾者,用於擴充套件 ConcreteComponent

舉例說明

在咖啡店客人想點一杯加兩份糖一份牛奶的摩卡咖啡,各個商品的價格如下,我們需要根據使用者點的咖啡、加的配料,動態的計算價格

商品 價格
拿鐵咖啡(LatteCoffee) 4.5
摩卡咖啡(MochaCoffe) 5.5
糖(Sugar) 1.0
牛奶(Milk) 2.0

「實體類」 Coffee

public abstract class Coffee{
    public String des = "咖啡"; //描述
    private float price = 0.0f; //價格

    protected abstract float cost(); //計算費用
    
    //省略getter setter方法
}

「被裝飾者」LatteCoffee

public class LatteCoffee extends Coffee{
    public LatteCoffee() {
        setDes("拿鐵咖啡");
        setPrice(4.5f);
    }

    @Override
    protected float cost() {
        return getPrice();
    }
}

「被裝飾者」MochaCoffee

public class MochaCoffee extends Coffee {
    public MochaCoffee() {
        setDes("摩卡咖啡");
        setPrice(5.5f);
    }

    @Override
    protected float cost() {
        return getPrice();
    }
}

「抽象裝飾者」Decorator

public class Decorator extends Coffee {

    private Coffee coffee;

    public Decorator(Coffee drink) {
        this.coffee = drink;
    }

    @Override
    protected float cost() {
        return getPrice() + coffee.cost();
    }

    @Override
    public String getDes() {
        return coffee.getDes() + "加" + super.getDes();
    }
}

「具體裝飾者」SugarDecorator

public class SugarDecorator extends Decorator{
    public SugarDecorator(Coffee coffee) {
        super(coffee);
        setDes("糖");
        setPrice(1.0f);
    }
}

「具體裝飾者」MilkDecorator

public class MilkDecorator extends Decorator{
    public MilkDecorator(Coffee coffee) {
        super(coffee);
        setDes("牛奶");
        setPrice(2.0f);
    }
}

「測試類」Client

public class Client {
    /**
     * 點一杯 加兩份糖一份牛奶的摩卡咖啡
     */
    @Test
    public void test01() {
        Coffee order = new MochaCoffee();
        System.out.println(order.getDes() + ",價格:" + order.cost());
        //加兩份糖
        order = new SugarDecorator(new SugarDecorator(order));
        System.out.println(order.getDes() + ",價格:" + order.cost());
        //加一份牛奶
        order = new MilkDecorator(order);
        System.out.println(order.getDes() + ",價格:" + order.cost());
    }
}

「結果」result

摩卡咖啡,價格:5.5
摩卡咖啡加糖加糖,價格:7.5
摩卡咖啡加糖加糖加牛奶,價格:9.5

在 Java IO 流中的應用

在上圖所示的關係中

  • 實體類是 InputStream
  • 被裝飾者是FileInputStream、StringBufferInputStream、ByteArrayInputStream
  • 抽象裝飾者是FilterInputStream
  • 具體裝飾者是BufferInputStream、DataInputStream、LineNumberInputStream

具體使用如下:

BufferedInputStream bis = new BufferedInputStream(new FileInputStream("G:\\a.txt"));

裝飾者模式總結

1、利用繼承設計子類,只能在編譯時靜態決定,並且所有子類都會繼承相同的行為;利用組合擴充套件物件,就可以在執行時動態的進行擴充套件。

2、裝飾者和被裝飾者物件有相同的超型別,所以在任何需要原始物件(被裝飾者)的場合,都可以用裝飾過的物件代替原始物件。

3、可以用一個或多個裝飾者包裝一個物件(被裝飾者)。

4、裝飾者可以在所委託的裝飾者行為之前或之後加上自己的行為,以達到特定的目的。

5、被裝飾者可以在任何時候被裝飾,所以可以在執行時動態地、不限量地用你喜歡的裝飾者來裝飾物件。

6、裝飾者會導致出現很多小物件,如果過度使用,會讓程式變得複雜。

五、外觀模式(Facade)

基本介紹

外觀模式(Facade Pattern):外部與一個子系統的通訊必須通過一個統一的外觀物件進行,它為子系統中的一組介面提供一個統一的高層介面,使子系統更容易被使用

外觀模式又稱為門面模式,它是一種物件結構型模式。

模式結構

1、Client(客戶端):呼叫者

2、Facade(外觀類):即上述所講的高層介面

3、SubSystem(子系統):被呼叫者

舉例說明

想要使用電腦,你只需要按一下開機鍵(客戶端),電腦的各個部件(子系統)就開始工作了,你不需要關心硬碟如何啟動的,CPU怎麼運轉的等等,一切都交給內部程式(外觀類)處理。

編寫簡單的程式模擬一下

「SubSystem」:電腦的幾個部件 CPU、記憶體、硬碟

public class Cpu {
    //使用「單例模式--餓漢式」建立物件
    private static Cpu instance = new Cpu();

    private Cpu() {
    }

    public static Cpu getInstance() {
        return instance;
    }

    public void start() {
        System.out.println("CPU啟動");
    }

    public void stop() {
        System.out.println("CPU停止工作");
    }
}
public class Memory {
    private static Memory instance = new Memory();

    private Memory() {
    }

    public static Memory getInstance() {
        return instance;
    }

    public void start() {
        System.out.println("記憶體啟動");
    }

    public void stop() {
        System.out.println("記憶體停止工作");
    }
}
public class HardDisk {
    private static HardDisk instance = new HardDisk();

    private HardDisk() {
    }

    public static HardDisk getInstance() {
        return instance;
    }

    public void start() {
        System.out.println("硬碟啟動");
    }

    public void stop() {
        System.out.println("硬碟停止工作");
    }
}

「Facade」:電腦,統一管理開機關機中硬體的啟動與停止

public class Computer {
    private Cpu cpu;
    private Memory memory;
    private HardDisk hardDisk;

    public Computer() {
        this.cpu = Cpu.getInstance();
        this.memory = Memory.getInstance();
        this.hardDisk = HardDisk.getInstance();
    }

    /**
     * 開機
     */
    public void boot(){
        cpu.start();
        memory.start();
        hardDisk.start();
    }

    /**
     * 關機
     */
    public void shutdown(){
        cpu.stop();
        memory.stop();
        hardDisk.stop();
    }
}

「Client」:電源鍵,可控制開機、關機

public class Client {
    Computer computer = new Computer();

    @Test
    public void boot(){
        computer.boot();
    }

    @Test
    public void shutdown(){
        computer.shutdown();
    }
}

模式分析

優點:

  • 實現了客戶端與子系統的低耦合,使得子系統的變化不會影響客戶端,只需要調整外觀類即可。
  • 對客戶端遮蔽子系統,減少了客戶端處理的物件數目,操作變得更簡單。
  • 降低了大型軟體系統中的編譯依賴性,並簡化了系統在不同平臺之間的移植過程,因為編譯一個子系統一般不需要編譯所有其他的子系統。一個子系統的修改對其他子系統沒有任何影響,而且子系統內部變化也不會影響到外觀物件。

缺點:

  • 不能很好的限制客戶端對子系統的使用,如果對其做了太多限制會降低可變性和靈活性。
  • 在不引入「抽象外觀類」的情況下,如果增加新的子系統,需要修改外觀類程式碼,違背了「開閉原則」

適用場景

  • 當要為一個複雜子系統提供一個簡單介面時可以使用外觀模式。該介面可以滿足大多數使用者的需求,而且使用者也可以越過外觀類直接訪問子系統。
  • 客戶程式與多個子系統之間存在很大的依賴性。引入外觀類將子系統與客戶以及其他子系統解耦,可以提高子系統的獨立性和可移植性。
  • 在層次化結構中,可以使用外觀模式定義系統中每一層的入口,層與層之間不直接產生聯絡,而通過外觀類建立聯絡,降低層之間的耦合度。

六、享元模式(Flyweight)

基本介紹

享元模式(Flyweight Pattern)也叫蠅量模式,運用共享技術有效地支援大量細粒度物件的複用。常用於系統底層開發,解決系統效能問題。例如資料庫連線池,裡面都是建立好的連線物件,如果有我們需要的,直接拿來用,避免重新建立,可以解決重複物件對記憶體造成浪費的問題

內部狀態和外部狀態

享元模式提出了細粒度和共享物件,這裡就涉及了內部狀態和外部狀態的概念,即可以把物件的資訊分為兩個部分:內部狀態和外部狀態

內部狀態(Intrinsic State):可以共享的相同內容

外部狀態(Extrinsic State):需要外部環境來設定的不能共享的內容

舉個例子,圍棋理論上有 361 個位置可以放棋子,每盤棋可能會產生兩三百個棋子物件,由於記憶體有限,一臺伺服器很難支援更多玩家進行圍棋對戰,如果用享元模式來處理棋子,將棋子的顏色(黑與白)作為內部狀態,棋子的位置(不確定)作為外部狀態,就可以將棋子物件減少到兩個例項(黑棋、白棋),這樣就可以很好的解決記憶體開銷問題。

模式結構

  • Flyweight:抽象享元類
  • ConcreteFlyweight:具體享元類
  • UnsharedConcreteFlyweight:非共享具體享元類
  • FlyweightFactory:享元工廠類

舉例說明

一個開發團隊接了這樣的專案,客戶希望做一個產品展示網站,但網站需要有多種釋出形式,每個使用者可以以新聞形式釋出、以部落格形式釋出、以微信公眾號形式釋出...

「抽象享元類」

public abstract class AbstractWebsite {
    public abstract void publish(User user);
}

「非共享具體享元類」

public class User {
    private String name;

    public User(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

「具體享元類」

public class ConcreteWebsite extends AbstractWebsite {
    /**
     * 釋出型別
     */
    private String type = "";

    public ConcreteWebsite(String type) {
        this.type = type;
    }

    /**
     * 釋出
     */
    @Override
    public void publish(User user) {
        System.out.println("使用者「"+user.getName()+"」釋出的網站形式為「" + type+"」");
    }
}

「享元工廠類」

public class WebsiteFactory {

    /**
     * 以 HashMap 作為物件池
     */
    private Map<String, ConcreteWebsite> pool = new HashMap<>();

    /**
     * 從物件池中返回指定型別的物件,沒有則建立
     */
    public AbstractWebsite getWebsite(String type) {
        if (!pool.containsKey(type)) {
            pool.put(type, new ConcreteWebsite(type));
        }
        return pool.get(type);
    }

    /**
     * 計算物件池中物件的個數
     */
    public int count() {
        return pool.size();
    }
}

「測試類」

public class Client {
    @Test
    public void test(){
        WebsiteFactory factory = new WebsiteFactory();

        AbstractWebsite website1 = factory.getWebsite("新聞");
        website1.publish(new User("張三"));
        website1.publish(new User("李四"));

        AbstractWebsite website2 = factory.getWebsite("部落格");
        website2.publish(new User("王五"));
        website2.publish(new User("趙六"));

        AbstractWebsite website3 = factory.getWebsite("公眾號");
        website3.publish(new User("陳七"));
        website3.publish(new User("胡八"));

        System.out.println("物件的個數:" + factory.count());
    }
}

「執行結果」

使用者「張三」釋出的網站形式為「新聞」
使用者「李四」釋出的網站形式為「新聞」
使用者「王五」釋出的網站形式為「部落格」
使用者「趙六」釋出的網站形式為「部落格」
使用者「陳七」釋出的網站形式為「公眾號」
使用者「胡八」釋出的網站形式為「公眾號」
物件的個數:3

享元模式在Integer中的應用

首先我們看一段程式碼,執行結果是什麼?

public class IntegerSource {
    public static void main(String[] args) {
        Integer v1 = 127;
        Integer v2 = 127;
        System.out.println("v1等於v2? " + (v1 == v2));
        Integer v3 = 128;
        Integer v4 = 128;
        System.out.println("v3等於v4? " + (v3 == v4));
    }
}
答案
v1等於v2? true
v3等於v4? false

分析:檢視 Integer 原始碼,找到 valueOf 方法,可以看到,如果 i 在某個範圍內,就不會產生新的物件,直接從快取陣列中獲取,點進 IntegerCache 裡就會發現 low = -128 high = 127,因此,我們可以理解為這個陣列就是「內部狀態」

public static Integer valueOf(int i) {
    //low = -128 , high = 127
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        //IntegerCache.cache是一個常量陣列:static final Integer cache[];
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

模式分析

優點:

  • 可以極大減少記憶體中物件的數量,使得相同物件或相似物件在記憶體中只儲存一份。

  • 享元模式的外部狀態相對獨立,而且不會影響其內部狀態,從而使得享元物件可以在不同的環境中被共享。

缺點:

  • 享元模式使得系統更加複雜,需要分離出內部狀態和外部狀態,這使得程式的邏輯複雜化。
  • 為了使物件可以共享,享元模式需要將享元物件的狀態外部化,而讀取外部狀態使得執行時間變長。

適用場景:

  • 一個系統有大量相同或者相似的物件,由於這類物件的大量使用,造成記憶體的大量耗費。

  • 物件的大部分狀態都可以外部化,可以將這些外部狀態傳入物件中。

  • 使用享元模式需要維護一個儲存享元物件的享元池,而這需要耗費資源,因此,應當在多次重複使用享元物件時才值得使用享元模式。

七、代理模式(Proxy)

代理模式介紹

代理模式提供了對目標物件額外的訪問方式,即通過代理物件訪問目標物件,這樣可以在不修改原目標物件的前提下,提供額外的功能操作,擴充套件目標物件的功能。

代理模式分為三類:

  • 靜態代理
  • 動態代理
  • Cglib 代理

靜態代理(不推薦使用)

介紹

要求目標物件和代理物件實現同一個介面,呼叫的時候呼叫代理物件的方法,從而達到增強的效果

優點:

可以在不修改目標物件的前提下,增強目標物件方法的功能(所有代理模式都可以實現,因此不推薦使用此方法)

缺點:

① 冗餘。目標物件和代理物件實現同一個介面,會產生過多的代理類。

② 不易維護。當介面方法增加,目標物件與代理物件都要進行修改。

程式碼實現

場景:廠家生產了商品,但是沒有足夠的精力、人力去銷售,這時候就需要一個代理商幫他售賣,但是代理商需要從中抽取 20% 的利潤。

公共介面

public interface IProducer {
    void sale(float money);
}

被代理物件

public class Producer implements IProducer {
    @Override
    public void sale(float money) {
        System.out.println("賣出產品,廠家獲得" + money + "元");
    }
}

代理物件

public class ProxyProducer implements IProducer{

    private IProducer producer;

    public ProxyProducer(IProducer producer) {
        this.producer = producer;
    }

    @Override
    public void sale(float money) {
        producer.sale(money * 0.8f);
    }
}

測試類

public class Client {
    @Test
    public void test(){
        IProducer producer = new Producer();
        ProxyProducer proxyProducer = new ProxyProducer(producer);
        proxyProducer.sale(1000f);
    }
}

執行結果

賣出產品,廠家獲得800.0元

動態代理

介紹

動態代理也稱:JDK 代理、介面代理,需要目標物件實現介面,否則不能用動態代理,利用 JDK 的 API(java.lang.reflect.Proxy),動態地在記憶體中構建代理物件

靜態代理和動態代理的區別:

  • 靜態代理在編譯時就已經實現,編譯完後的代理類是一個實際的 class 檔案
  • 動態代理實在執行時動態生成的,編譯後沒有實際的 class 檔案,而是在執行時動態的生成類位元組碼,並載入到 JVM 中

程式碼實現

以靜態代理的情景為例,我們只需要修改代理物件的程式碼,代理物件不需要實現公共介面了。

public class ProxyProducer {
    /**
     * 維護一個目標物件
     */
    private Object target;

    public ProxyProducer(Object target) {
        this.target = target;
    }

    public Object getProxyInstance() {
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new InvocationHandler() {
                    /**
                     * 執行被代理物件的任何介面方法都會經過這裡
                     * @param proxy 代理物件的引用
                     * @param method 當前執行的方法
                     * @param args 當前執行方法的引數
                     * @return 和被代理物件具有相同的返回值
                     * @throws Throwable
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        //代理過程中執行一些方法
                        float money = (float) args[0] * 0.8f;
                        //反射機制呼叫目標物件的方法
                        Object invoke = method.invoke(target, money);
                        return invoke;
                    }
                });
    }
}

Cglib 代理

介紹

Cglib 代理也叫子類代理,目標物件不需要實現任何介面,它是在記憶體中構建一個子類物件從而實現對目標物件功能的擴充套件。

Cglib 是一個強大的高效能的程式碼生成包,它可以在執行期間擴充套件 Java 類與實現 Java 介面,它廣泛地被許多 AOP 的框架使用,例如 Spring AOP,用於實現方法攔截。

Cglib 包底層實通過使用位元組碼處理框架 ASM 來轉換位元組碼並生成新的類。

在 AOP 程式設計中選擇哪種代理模式?

  • 目標物件需要實現介面,用 JDK 代理
  • 目標物件不需要實現介面,用 Cglib 代理

程式碼實現

使用之前需要匯入相關 jar 包,可去 maven 倉庫下載

被代理物件,無需實現介面

public class Producer {
    public void sale(float money) {
        System.out.println("賣出產品,廠家獲得" + money + "元");
    }
}

代理物件

public class ProxyProducer implements MethodInterceptor {
    /**
     * 維護一個目標物件
     */
    private Object target;

    public ProxyProducer(Object target) {
        this.target = target;
    }

    /**
     * 為目標物件生成代理物件
     */
    public Object getProxyInstance(){
        //建立一個工具類
        Enhancer enhancer = new Enhancer();
        //設定父類
        enhancer.setSuperclass(target.getClass());
        //設定回撥函式
        enhancer.setCallback(this);
        //建立子類物件(代理物件)
        return enhancer.create();
    }

    /**
     * 會攔截被代理物件的所有方法
     * @param obj 增強物件
     * @param method 被代理物件的方法
     * @param args 被代理物件方法的引數
     * @param methodProxy 代理物件
     */
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("obj:" + obj.getClass());
        Object returnValue = null;
        float money = (float) args[0] * 0.8f;
        if("sale".equals(method.getName())){
            returnValue = method.invoke(target, money);
        }
        return returnValue;
    }
}

測試類

public class Client {
    @Test
    public void test() {
        Producer producer = new Producer();
        Producer proxyInstance = (Producer) new ProxyProducer(producer).getProxyInstance();
        proxyInstance.sale(1000f);
    }
}

? 以上所有程式碼和筆記均可在 我的GitHub 獲取

相關文章