【設計模式】第十一篇:來一起瞅瞅享元模式
今天一起來看一個新的設計模式,那就是享元模式,關於此模式,常見的就是 “專案外包”、
以及 “五子棋” 這樣兩個例子,我們下面就選擇使用 “專案外包” 這個例子引入去講
一 故事引入
(一) 故事背景
程式設計師小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)所以請確認例項足夠多的時候才值得去使用享元模式。