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

結構型模式(Structural Pattern)用於將類或對象結合在一塊兒造成更強大的結構,就像搭積木,能夠經過簡單的積木組合出複雜、功能強大的模型。java

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

1、適配器模式(Adapter)

生活中,充電插頭有兩腳的、三腳的,還有圓形的,若是想使這些插頭都能工做,就須要一個多功能適配器git

基本介紹

適配器模式(Adapter Pattern)屬於結構性模式,它能夠將某個類的接口轉換爲客戶端指望的另外一個接口表示,主要目的是兼容性,讓本來因接口不匹配不能一塊兒工做的兩個類能夠協同工做,其別名爲包裝器(Wrapper)。適配器模式主要分爲三類:類適配器模式對象適配器模式接口適配器模式github

工做原理

  1. 讓本來接口不兼容的類能夠兼容
  2. 從用戶的角度看不到被適配者,是解耦的
  3. 用戶調用適配器轉化出來的目標接口方法,適配器去再調用被適配者的相關接口方法

類適配器模式

實現原理

Adapter 類繼承 src 類,實現 dst 接口,完成 src 對 dst 的適配。web

案例

插座(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模式最大的做用仍是將本來不兼容的接口融合在一塊兒工做。

2、橋接模式(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 消息...

3、組合模式(Composite)

基本介紹

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

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

模式結構

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。

總結

一、簡化客戶端操做。客戶端只須要面對一致的對象而不用考慮總體部分或者節點葉子的問題。

二、具備較強的擴展性。當咱們要更改組合對象時,咱們只須要調整內部的層次關係,客戶端不用作出任何改動。

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

四、須要遍歷組織機構,或者處理的對象具備樹形結構時,很是適合使用組合模式。

五、要求較高的抽象性。若是節點和葉子有不少差別性的話,好比不少方法和屬性都不同,不適合使用組合模式。

4、裝飾者模式(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"));

裝飾者模式總結

一、利用繼承設計子類,只能在編譯時靜態決定,而且全部子類都會繼承相同的行爲;利用組合擴展對象,就能夠在運行時動態的進行擴展。

二、裝飾者和被裝飾者對象有相同的超類型,因此在任何須要原始對象(被裝飾者)的場合,均可以用裝飾過的對象代替原始對象。

三、能夠用一個或多個裝飾者包裝一個對象(被裝飾者)。

四、裝飾者能夠在所委託的裝飾者行爲以前或以後加上本身的行爲,以達到特定的目的。

五、被裝飾者能夠在任什麼時候候被裝飾,因此能夠在運行時動態地、不限量地用你喜歡的裝飾者來裝飾對象。

六、裝飾者會致使出現不少小對象,若是過分使用,會讓程序變得複雜。

5、外觀模式(Facade)

基本介紹

外觀模式(Facade Pattern):外部與一個子系統的通訊必須經過一個統一的外觀對象進行,它爲子系統中的一組接口提供一個統一的高層接口,使子系統更容易被使用

外觀模式又稱爲門面模式,它是一種對象結構型模式。

模式結構

一、Client(客戶端):調用者

二、Facade(外觀類):即上述所講的高層接口

三、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();
    }
}

模式分析

優勢:

  • 實現了客戶端與子系統的低耦合,使得子系統的變化不會影響客戶端,只須要調整外觀類便可。
  • 對客戶端屏蔽子系統,減小了客戶端處理的對象數目,操做變得更簡單。
  • 下降了大型軟件系統中的編譯依賴性,並簡化了系統在不一樣平臺之間的移植過程,由於編譯一個子系統通常不須要編譯全部其餘的子系統。一個子系統的修改對其餘子系統沒有任何影響,並且子系統內部變化也不會影響到外觀對象。

缺點:

  • 不能很好的限制客戶端對子系統的使用,若是對其作了太多限制會下降可變性和靈活性。
  • 在不引入「抽象外觀類」的狀況下,若是增長新的子系統,須要修改外觀類代碼,違背了「開閉原則」

適用場景

  • 當要爲一個複雜子系統提供一個簡單接口時可使用外觀模式。該接口能夠知足大多數用戶的需求,並且用戶也能夠越過外觀類直接訪問子系統。
  • 客戶程序與多個子系統之間存在很大的依賴性。引入外觀類將子系統與客戶以及其餘子系統解耦,能夠提升子系統的獨立性和可移植性。
  • 在層次化結構中,可使用外觀模式定義系統中每一層的入口,層與層之間不直接產生聯繫,而經過外觀類創建聯繫,下降層之間的耦合度。

6、享元模式(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);
}

模式分析

優勢:

  • 能夠極大減小內存中對象的數量,使得相同對象或類似對象在內存中只保存一份。

  • 享元模式的外部狀態相對獨立,並且不會影響其內部狀態,從而使得享元對象能夠在不一樣的環境中被共享。

缺點:

  • 享元模式使得系統更加複雜,須要分離出內部狀態和外部狀態,這使得程序的邏輯複雜化。
  • 爲了使對象能夠共享,享元模式須要將享元對象的狀態外部化,而讀取外部狀態使得運行時間變長。

適用場景:

  • 一個系統有大量相同或者類似的對象,因爲這類對象的大量使用,形成內存的大量耗費。

  • 對象的大部分狀態均可之外部化,能夠將這些外部狀態傳入對象中。

  • 使用享元模式須要維護一個存儲享元對象的享元池,而這須要耗費資源,所以,應當在屢次重複使用享元對象時才值得使用享元模式。

7、代理模式(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 獲取

相關文章
相關標籤/搜索