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


結構型模式(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 獲取


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM