1. 程式人生 > 實用技巧 >【設計模式】第十一篇:來一起瞅瞅享元模式

【設計模式】第十一篇:來一起瞅瞅享元模式

今天一起來看一個新的設計模式,那就是享元模式,關於此模式,常見的就是 “專案外包”、
以及 “五子棋” 這樣兩個例子,我們下面就選擇使用 “專案外包” 這個例子引入去講

一 故事引入

(一) 故事背景

程式設計師小B,幫助客戶 A 做了一個展示一些產品內容的網站,通過 A 的 推薦,客戶 B 、客戶C 也想要做這樣一個網站,但是就是形式有一些變化

  • 有的客戶希望是新聞釋出形式的
  • 有的客戶希望是部落格形式的
  • 有的客戶希望是公眾號形式的等等

而且他們都希望能夠降低一些費用,但是每一個空間部署著一個網站,所以租借空間的費用是固定的,同時程式設計師小B 並不想從自己的勞動報酬中縮減費用

(二) 思考解決方案

(1) 最簡單的傳統方案

先說最簡單能想到的方案,直接把網站程式碼複製幾份,然後每一個都租借一個空間,然後對程式碼進行定製修改。注:這裡還沒考慮優化或者省錢

我們用一個 WebSite 類來模擬一個網站的模板,所有型別可以通過對 name 賦值然後呼叫 use 方法進行修改

public class WebSite {
    private String name = "";

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

    public void use(){
        System.out.println("當前網站分類: " + name);
    }
}

如果按照剛才的思路,是這樣操作的

public class Test {
    public static void main(String[] args) {
        WebSite webSite1 = new WebSite("部落格");
        webSite1.use();

        WebSite webSite2 = new WebSite("部落格");
        webSite2.use();

        WebSite webSite3 = new WebSite("部落格");
        webSite3.use();

        WebSite webSite4 = new WebSite("新聞釋出");
        webSite4.use();

        WebSite webSite5 = new WebSite("公眾號");
        webSite5.use();

        WebSite webSite6 = new WebSite("公眾號");
        webSite6.use();
    }
}

執行結果:

當前網站分類: 部落格
當前網站分類: 部落格
當前網站分類: 部落格
當前網站分類: 新聞釋出
當前網站分類: 公眾號
當前網站分類: 公眾號

(2) 存在的問題及改進思路

  • ① 假設虛擬空間在同一臺伺服器上,做上述內容,需要例項化 6 個 WebSite,而其本質又沒有很大的差別,所以對於伺服器的資源浪費很大

  • ② 網站結構相似度很高,基本全是重複的程式碼

對於這種重複性很高的內容,首先我們要做到將其抽象出來,重複建立例項在設計模式中肯定是不太明智的,我們想要做到多個客戶,共享同一個例項。這樣不管是程式碼還是伺服器資源利用,都會改善很多

一個不算特別恰當的例子:例如外賣平臺中的一個一個商家店鋪,是不是可以理解為平臺中的一個小店鋪,小網站,其中通過例如店鋪 ID 等內容來區分不同店鋪,但是其每一家店鋪整體的模板和樣子是差不多的。

我們下面要做的就是,將大量相似內容抽象成一個網站模板類,然後把一些特定的內容,通過引數移到例項的外面,呼叫的時候再指定,這樣可以大幅度減少單個例項的數目。

(3) 享元模式初步改進

建立一個抽象的 WebSite 類

public abstract class WebSite {
    public abstract void use();
}

接下來是具體實現,建立其子類,和前面一樣,所有型別可以通過對 type賦值然後呼叫 use 方法進行修改

public class ConcreteWebSite extends WebSite {

    // 網站釋出形式
    private String type = "";

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

    @Override
    public void use() {
        System.out.println("當前網站分類: " + type);
    }
}

建立一個工廠類,用於建立,返回一個指定的網站例項

這一個類,首先用一個 HashMap 模擬一種連線池的概念,因為我們既然想要達到不重複建立例項的效果,就需要通過一些邏輯判斷,判斷 Map 中是否存在這個例項,如果有就直接返回,如果沒有就建立一個新的,同樣型別 type 是在呼叫時,顯式的指定的。

後面補充了一個獲取網站分類總數的方法,用來測試的時候,看一下是不是沒有重複建立例項

import java.util.HashMap;

/**
 * 網站工廠類,根據需要返回
 */
public class WebSiteFactory {
    // 模擬一個連線池
    private HashMap<String, ConcreteWebSite> pool = new HashMap<>();

    /**
     * 獲取網站:根據傳入的型別,返回網站,無則建立,有則直接返回
     *
     * @param type
     * @return
     */
    public WebSite getWebSiteCategory(String type) {
        if (!pool.containsKey(type)) {
            // 建立一個網站,放到池種
            pool.put(type, new ConcreteWebSite(type));
        }
        return (WebSite) pool.get(type);
    }

    /**
     * 獲取網站分類總數
     */
    public int getWebSiteCount() {
        return pool.size();
    }

}

測試一下

public class Test {
    public static void main(String[] args) {
        // 建立一個工廠
        WebSiteFactory factory = new WebSiteFactory();

        // 給客戶建立一個部落格型別的網站
        WebSite webSite1  = factory.getWebSiteCategory("部落格");
        webSite1.use();

        // 給客戶建立一個部落格型別的網站
        WebSite webSite2  = factory.getWebSiteCategory("部落格");
        webSite2.use();

        // 給客戶建立一個部落格型別的網站
        WebSite webSite3  = factory.getWebSiteCategory("部落格");
        webSite3.use();

        // 給客戶建立一個新聞釋出型別的網站
        WebSite webSite4  = factory.getWebSiteCategory("新聞釋出");
        webSite4.use();

        // 給客戶建立一個公眾號型別的網站
        WebSite webSite5  = factory.getWebSiteCategory("公眾號");
        webSite5.use();

        // 給客戶建立一個公眾號型別的網站
        WebSite webSite6  = factory.getWebSiteCategory("公眾號");
        webSite6.use();

        // 檢視一下連線池中的例項數
        System.out.println("例項數:" + factory.getWebSiteCount());
    }
}

執行結果:

當前網站分類: 部落格
當前網站分類: 部落格
當前網站分類: 部落格
當前網站分類: 新聞釋出
當前網站分類: 公眾號
當前網站分類: 公眾號
例項數:3

(4) 享元模式再改進-區分內外部狀態

上面的程式碼,使用工廠代替了直接例項化的方式,工廠中,主要通過一個池的概念,實現了共享物件的目的,但是其實我們會發現,例如建立三個部落格型別的網站,但是好像這三個網站就是一模一樣的,但是不同的客戶,其中部落格網站中的資料肯定是不同的,這就是我們還沒有區分內部外部的狀態

內部狀態:物件共享出來的資訊,儲存在享元物件內部並且不會隨環境改變的共享部分

外部狀態:物件用來標記的一個內容,隨環境會改變,不可共享

打個比方,五子棋只有黑白兩色,總不能下多少子,就建立多少個例項吧,所以我們把顏色看做內部狀態,有黑白兩種顏色。而各個棋子的位置並不相同,當我們落子後這個位置資訊才會被傳入,所以位置資訊就是外部狀態

那麼對於“外包網站”的例子中,很顯然,不同的客戶網站資料就是一個外部狀態,下面來修改一下

首先新增一個 User 類,後面會將其引入作為外部狀態

public class User {
    private String name;

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

    public String getName() {
        return name;
    }
}

修改抽象類和子類,通過引數的方式引入 User 這個外部狀態

抽象類

public abstract class WebSite {
    public abstract void use(User user);
}

子類

public class ConcreteWebSite extends WebSite {

    // 網站釋出形式
    private String type = "";

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

    @Override
    public void use(User user) {
        System.out.println("【網站分類】: " + type + " 【客戶】: " + user.getName());
    }
}

工廠類不變,最後修改測試類

public class Test {
    public static void main(String[] args) {
        // 建立一個工廠
        WebSiteFactory factory = new WebSiteFactory();

        // 給客戶建立一個部落格型別的網站
        WebSite webSite1  = factory.getWebSiteCategory("部落格");
        webSite1.use(new User("客戶A"));

        // 給客戶建立一個部落格型別的網站
        WebSite webSite2  = factory.getWebSiteCategory("部落格");
        webSite2.use(new User("客戶B"));

        // 給客戶建立一個部落格型別的網站
        WebSite webSite3  = factory.getWebSiteCategory("部落格");
        webSite3.use(new User("客戶C"));

        // 給客戶建立一個新聞釋出型別的網站
        WebSite webSite4  = factory.getWebSiteCategory("新聞釋出");
        webSite4.use(new User("客戶A"));

        // 給客戶建立一個公眾號型別的網站
        WebSite webSite5  = factory.getWebSiteCategory("公眾號");
        webSite5.use(new User("客戶A"));

        // 給客戶建立一個公眾號型別的網站
        WebSite webSite6  = factory.getWebSiteCategory("公眾號");
        webSite6.use(new User("客戶B"));

        // 檢視一下連線池中的例項數
        System.out.println("例項數:" + factory.getWebSiteCount());
        
    }
}

執行結果:

【網站分類】: 部落格 【客戶】: 客戶A
【網站分類】: 部落格 【客戶】: 客戶B
【網站分類】: 部落格 【客戶】: 客戶C
【網站分類】: 新聞釋出 【客戶】: 客戶A
【網站分類】: 公眾號 【客戶】: 客戶A
【網站分類】: 公眾號 【客戶】: 客戶B
例項數:3

可以看出來,雖然有 6 個客戶,但是實際上只有三個例項,同樣再增加幾十個,也最多隻會有三個例項

二 享元模式概念

(一) 概念

定義:享元(Flyweight)模式運用共享技術來有效地支援大量細粒度物件的複用。

它通過共享已經存在的物件來大幅度減少需要建立的物件數量、避免大量相似類的開銷,從而提高系統資源的利用率。

享元模式又叫做蠅量模式,所以英文為 Flyweight

(二) 結構圖

注:方法引數和返回值沒細細弄,主要為了說明結構

  • 抽象享元角色(Flyweight):是所有的具體享元類的超類或介面,非享元的外部狀態以引數的形式通過方法傳入。
  • 具體享元(Concrete Flyweight)角色:實現抽象享元角色中所規定的介面。
  • 非享元(Unsharable Flyweight) 角色:是不共享的外部狀態,它以引數的形式注入具體享元的相關方法中,這也意味著,享元模式並不強制共享
  • 享元工廠(Flyweight Factory)角色:負責建立和管理享元角色。
    • 當客戶物件請求一個享元物件時,享元工廠檢査系統中是否存在符合要求的享元物件
      • 如果存在則提供給客戶
      • 如果不存在的話,則建立一個新的享元物件

(二) 簡述優缺點

優點:相同物件只需要儲存一份,降低了系統中記憶體的數量,減少了系統記憶體的壓力

缺點:程式複雜性增大,同時讀取享元模式的外部狀態會使得執行時間稍微變長

(三) 應用場景

享元模式其中也需要一個工廠進行控制,所以就好像是在工廠方法模式的基礎上,增加了一個快取機制,也就是通過一個 “池” 的概念,避免了大量相同的物件建立,大大降低了記憶體空間的消耗。

那麼應用場景如下:

  • 一個程式使用了大量相似或者相同的物件,且造成了很大的開銷的時候
  • 大部分物件,可以根據內部狀態分組,且可將不同部分外部化,這樣每一個組只需儲存一個內部狀態。
    • 例如上面的部落格,新聞,公眾號站形式就是三種組,每個組只需要傳入使用者資料這個外部狀態即可
  • 因為使用享元模式,需要一個儲存享元的資料結構(例如上面的 Hashmap)所以請確認例項足夠多的時候才值得去使用享元模式。