0、提綱
目錄:
1、由 HTTP 協(xié)議 聯(lián)想到 對享元模式的思考
2、引入禮盒問題,作為享元模式的逆向思考
3、享元模式的實現(xiàn)
4、享元模式總結(jié)
5、感謝幫助勘誤的簡書作者們
需要查看其它設(shè)計模式描述可以查看我的文章《設(shè)計模式開篇》。
1、由 HTTP 協(xié)議 聯(lián)想到 對享元模式的思考
了解享元模式有一段時間了,但到目前為止還沒有自己應(yīng)用過,所以也就一直沒有提筆寫享元模式,怕不能寫出享元模式的精髓。
昨天在讀《HTTP權(quán)威指南》的時候,書中提到了數(shù)據(jù)聚集的 Nagle 算法。
TCP 有一個數(shù)據(jù)流接口,應(yīng)用程序可以通過它將任意尺寸的數(shù)據(jù)放入 TCP 棧中(即使一次只放一個一個字節(jié)也可以)。但是每個 TCP 段中都至少裝載了40個字節(jié)的標記和首部。這意味著如果發(fā)送大量包含少量數(shù)據(jù)的 TCP 分組,網(wǎng)絡(luò)性能就會嚴重下降。

Nagle 算法試圖在發(fā)送一個分組之前,將 TCP 數(shù)據(jù)綁定在一起,以提高網(wǎng)絡(luò)效率。Nagle 算法鼓勵發(fā)送全尺寸(LAN 上分組最大約1500字節(jié),WAN 上分組則是幾百字節(jié))的段。只有當其他分組都被確認過后,Nagle 算法才允許發(fā)送非全尺寸的分組。這意味著當其它分組在傳輸過程中,就將已接收到的數(shù)據(jù)緩存起來。只有當掛起的分組被確認,或緩存中積累了足夠發(fā)送一個全尺寸的數(shù)據(jù)時,才允許將緩存的數(shù)據(jù)發(fā)送出去。

拓展:Nagle 算法在現(xiàn)代的服務(wù)器上并不推薦使用,因為它解決了細粒度對象的同時也帶入了不少新的問題。最顯著的是因為引入延遲確認算法,導(dǎo)致的自身被延遲1-200毫秒的情況。
對比上一張圖,差一點:移除了兩個 TCP 分組容器(節(jié)省約80字節(jié)),由三次TCP 段應(yīng)答降至一次應(yīng)答。當然這些都是在 TCP 層次上優(yōu)化,似乎與本期主題(享元模式)扯不上關(guān)系,但細細品味實則不然。
Nagle 算法通過緩沖[buffer](注意不是緩存[cache])數(shù)據(jù)應(yīng)用延遲確認算法,將多個細粒度對象組合成較大的對象放入一個TCP 分段中,以減少創(chuàng)建多余的 TCP 分段。
文章用較多的篇幅說明 Nagle 算法,似乎有些本末倒置。我們暫先忽略Nagle 算法利用緩沖區(qū)將細粒度數(shù)據(jù)拼裝成粗粒度數(shù)據(jù)的過程,著重關(guān)心其結(jié)果 。
如上圖中的三個包含內(nèi)容長度為1字節(jié)的 TCP 分段,采用 Nagle 算法后將三個 TCP 分段的公共首部提取到一個TCP分段中(其實內(nèi)部的 TCP 校驗碼是需要重新計算的),然后將內(nèi)容歸并放入提取出的 TCP 分段中,達到優(yōu)化性能的效用。
注意: 雖然已經(jīng)講了很多,但不得不糾正的一點是 —— Nagle 算法并不是享元模式實現(xiàn),但它確確實實解決了細粒度對象問題。這對正確理解享元模式,起到了不少啟發(fā)作用。
2、引入禮盒問題,作為享元模式的逆向思考

我們換個視角來看待包裹與包裹中的內(nèi)容,生活中的典型例子是出禮品包裝盒與禮物。你可以再沒有想好要買什么禮物之前,先買一個漂亮的禮盒(如果我們用心準備禮物的話),用心的你選擇了一只鋼筆作為禮物。
我們很用心的將禮物放進禮盒中系好絲帶 ~~(背景音樂響起) 完整的禮物出現(xiàn)了!
注意:直到目前為止,我們都在拿組合的思想說事。無論是TCP 分段(標記首部&內(nèi)容),還是禮物(禮盒+鋼筆)。因為我們是先有細粒度的對象,再使用細粒度的對象組合成一個粗粒度對象。而享元模式的思想則恰恰相反,它將粗粒度對象劃分成細粒度對象。
3、享元模式的實現(xiàn)
享元模式可以優(yōu)化大量包含重復(fù)數(shù)據(jù)的細粒度對象的場景,典型的場景比如:權(quán)限控制。
比如下表,假設(shè)每字符代表一種權(quán)限,而一長串則表示一串組合權(quán)限?,F(xiàn)在需要你把下面這張表 mapping 成數(shù)據(jù)結(jié)構(gòu),你會怎么干呢?
| 權(quán)限 | 用戶 |
|---|---|
| 聽說讀寫練看 | robert |
| 聽讀練看 | staff |
| 聽讀練看寫 | jeff |
| 聽 | john |
一般情況下我們會這么干(如果權(quán)限只是有限的字符串這樣干并沒有什么壞處)。
public class User{
public String name;
public String permissions;
}
(甲方的新需求):明確告知每個權(quán)限串會非常的長,大約幾 MB大小(為了切換思考角度容易一些,我不得不這樣夸張的說)。
這種時候顯然上面的User結(jié)構(gòu)就無法支撐了,因為我們擁有一大串的User列表。把他們都加載到內(nèi)存中,會造成很大的開銷,這個時候我們不得不思考更好的解決方案。
我們觀察到權(quán)限串有大量的重復(fù)單元,如上表中的每個用戶都有聽這個權(quán)限。所以我們不妨將permissions拆分成:聽、說、讀、寫、練、看的單個權(quán)限。然后user引用對應(yīng)權(quán)限的對象即可。
public class User{
public String name;
public Permission[] permissions;
public User(String name,Permission... permissions){
this.name = name;
this.permissions = permission;
}
}
public class Permission{
public Permission(String permission){
this.permission = permission;
}
public String permission;
}
public class Main{
public static void main(String[] args){
// 提供權(quán)限
Permission hear = new Permission("聽");
Permission say = new Permission("說");
Permission read = new Permission("讀");
Permission write = new Permission("寫");
Permission view = new Permission("看");
Permission work = new Permission("練");
// 創(chuàng)建用戶
User robert = new User("robert",hear,say,read,write,view,work);
User staff = new User("staff",hear,read,view,work);
User jeff = new User("jeff",hear,read,write,view,work);
User john = new User("john",hear);
}
}
注意:代碼并不是享元模式的實現(xiàn)結(jié)構(gòu)。但我認為已經(jīng)足夠說明問題,相比于第一個方案(String)permissions,下面的方案:權(quán)限部分已經(jīng)做到細粒度的復(fù)用,而那些的確無法復(fù)用的(如:用戶與權(quán)限的綁定關(guān)系)也的確無法復(fù)用了。 當然第二個方案,有太多可優(yōu)化的地方(比如建個權(quán)限工廠)就不再這里體現(xiàn)了。
4、享元模式總結(jié)
享元模式實質(zhì)上將粗粒度的對象拆分成:動 與 靜 的部分。
動:意味著 變化、聯(lián)結(jié)、聯(lián)系,是無法被分割出來的關(guān)系。
靜:意味著 重復(fù)、可模板化的內(nèi)容。
將那些不會變化的內(nèi)容提取出來,也即共享模式的本質(zhì):共享細粒度對象。
是的,全文沒有提到一點如何實現(xiàn)享元模式的結(jié)構(gòu),如果你有需要不妨Google 之。我相信在領(lǐng)悟了其思想的前提下,看任何一篇享元模式實現(xiàn)的文章都是很容易讀懂的。
5、感謝幫助勘誤的簡書作者們
1、感謝簡書作者 遠伯 的勘誤
- 糾正了 Nagle 算法是基于 buffer 而非 cache 的數(shù)據(jù)延遲算法。
- 提出 Nagle 的模式與享元模式思想 略有差異的事實。
2、感謝簡書作者九彩拼盤的勘誤
- 由享元模式聯(lián)想到 配合與邏輯的 抽象層次的享元理解。