享元模式(FlyWeight)
“享”取“共享”之意,“元”取“單元”之意。
意圖
運用共享技術,有效的支持大量細粒度的對象。
意圖解析
面向對象的程序設計中,一切皆是對象,這也就意味着系統的運行將會依賴大量的對象。
試想,如果對象的數量過多,勢必會增加系統負擔,導致運行的代價過高。
下面看兩個小例子理解下
1.)有一首歌曲叫做《大舌頭》
其中有一句歌詞“說說說說 說你愛我 我我我我 說不出口”
2.)有一個文本編輯器軟件,對於每一個字符使用對象進行表示
當打開一篇有很多重復字符的、數萬字的文章時,你會使用幾個對象進行表示?
如果仍舊采用每個字符占用一個對象,系統勢必崩潰,必然需要共享對象
上面的兩個例子中,都涉及到重復對象的概念
而享元模式的意圖就是如此,將
重復的對象進行共享以達到支持大量細粒度對象的目的。
如果不進行共享,如例2中描述的那樣,一篇數萬字符的文章將會產生數萬個對象,這將是一場可怕的災難。
flyweight意為輕量級
在我們當前的場景下,寓意為通過共享技術,輕量級的---也就是內存占用更小
本質就是“共享”所以中文翻譯過來多稱之為享元
簡言之,享元模式就是要“共享對象”
對於Java語言,我們熟悉的String,就是享元模式的運用
String是不可變對象,一旦創建,將不會改變
在JVM內部,String對象都是共享的
如果一個系統中的兩個String對象,包含的字符串相同,只會創建一個String對象提供給兩個引用
從而實現String對象的共享(new 的對象是兩個不同的)
享元模式又不僅僅是簡單的“共享對象”
上面的兩個小例子中,對於文字中的重復字符
可以通過共享對象的方式,對某些對象進行共享,從而減少內存開銷。
考慮下圖中的情景,這里面所有的“你”字,到底是不是同樣的?
- 是,因為全部都是漢字“你”
- 不是,因為盡管都是漢字“你”,但是他們的字體,顏色,字號,卻又明顯不同,所以不是同樣的
如果將字體、顏色、字號,作為“你”這個漢字的狀態
是不是可以認為:他們都是一樣的漢字,但是他們卻又具有不同的狀態?
其實享元模式不僅僅用來解決大量重復對象的共享問題,還能夠用來解決相似對象的問題。
享元對象能夠共享的關鍵在於:區分對象的
內部狀態和
外部狀態
內部狀態是存儲在享元對象內部的,並且不會隨環境的變化而有所改變。
比如上面的漢字“你”,無論在任何情況下,漢字“你”,始終是“你”,不會變成“她”
所以說享元模式解決共享問題,本質是共享內部狀態
外部狀態是隨外部環境變化而變化,不能共享的狀態。
享元對象的外部狀態通常由客戶端保存,在必要的時候在傳遞到享元對象內部
比如上面漢字“你”的字體、顏色、字號就是外部狀態。
小結
享元模式就是為了避免系統中出現大量相同或相似的對象,同時又不影響客戶端程序通過面向對象的方式對這些對象進行操作
享元模式通過共享技術,實現相同或相似對象的重用
比如文編編輯器讀取文本
在邏輯上每一個出現的字符都有一個對象與之對應,然而在物理上它們卻共享同一個享元對象
在享元模式中,存儲這些共享實例對象的地方通常叫做享元池(Flyweight Pool)
享元模式可以結合String的
intern()方法一起進行理解
通過區分了內部狀態和外部狀態,就可以將相同內部狀態的對象存儲在池中,池中的對象可以實現共享
需要的時候將對象從池中取出,實現對象的復用
通過向取出的對象注入不同的外部狀態,進而得到一些列相似的對象
而這些看似各異的對象在內存中,僅僅存儲了一份,大大節省了空間,所以說很自然的命名為“flyweight”輕量級
享元工廠
通過對意圖的認識,可以認為,
享元模式其實就是對於“程序中會出現的大量重復或者相似對象”的一種“重構”
當然,你應該是在設計之初就想到這個問題,而不是真的出現問題后再去真的重構
比如,你想要設計“字符”這種對象時,就應該考慮到他的“大量””重復““相似”的特點
所以需要分析出字符的內部狀態,與外部狀態
上面也提到對於享元對象,通過享元池進行管理
對於池的管理通常使用工廠模式,借助於工廠類對享元池進行管理
用戶需要對象時,通過工廠類獲取
工廠提供一個存儲在享元池中的已創建的對象實例,或者創建一個新的實例
示例代碼
針對於上面的例子,漢字“你”作為內部狀態,可以進行共享
“顏色”作為外部狀態,由客戶端保存傳遞
創建字符類 Character、漢字字符類ChineseCharacter、顏色類Color以及工廠類CharacterFactory
Color含有顏色屬性,通過構造方法設置,getter方法獲取
package flyweight; public class Color { public String Color; public Color(String color) { this.Color = color; } public String getColor() { return Color; } }
Character 抽象的字符類,用於描述字符
package flyweight; public abstract class Character { public abstract String getValue(); public void display(Color color) { System.out.println("字符: " + getValue() + " ,顏色: " + color.getColor()); } }
漢字字符類,為了簡化,直接設置value為漢字“你”
package flyweight; public class ChineseCharacter extends Character { @Override public String getValue() { return "你"; } }
CharacterFactory字符工廠類
通過單例模式創建工廠
內部HashMap用於存儲字符,並且提供獲取方法
為了簡化程序,初始就創建了一個漢字字符“你”存儲於字符中
package flyweight; import java.util.HashMap; public class CharacterFactory { /** * 單例模式 餓漢式創建 */ private static CharacterFactory instance = new CharacterFactory(); /** * 使用HashMap管理享元池 */ private HashMap<String, Object> hm = new HashMap<>(); private CharacterFactory() { Character character = new ChineseCharacter(); hm.put("你", character); } /** * 單例全局訪問接口獲取工廠 */ public static CharacterFactory getInstance() { return instance; } /** * 根據key獲取池中的對象 */ public Character getCharacter(String key) { return (Character) hm.get(key); } }
測試代碼
示例中,我們通過工廠,從享元池中獲取了三個漢字字符“你”。
通過 == 可以看得出來,他們都是同一個對象
在分別調用他們的display方法時,在客戶端(此處為我們的Test main方法)中創建,並且傳遞給享元對象
通過方法參數的形式進行外部狀態的設置。
CharacterFactory 單例模式,返回自身實例
CharacterFactory內部維護Character的享元池
Character 依賴Color
ChineseCharacter是Character的實現類
結構
將上面的示例轉換為標准的享元模式的名稱
抽象享元角色 FlyWeight
所有具體享元類的超類,為這些類規定了需要實現的公共接口
外部狀態可以通過業務邏輯方法的參數形式傳遞進來
具體享元角色ConcreteFlyWeight
實現抽象享元角色所規定的的接口
需要保存內部狀態,而且,內部狀態必須與外部狀態無關
從而才能使享元對象可以在系統內共享
享元工廠角色 FlyWeightFactory
負責創建和管理享元角色,也就是維護享元池
必須保證享元對象可以被系統適當的共享
接受客戶端的請求
如果有適當符合要求的享元對象,則返回
如果沒有一個適當的享元對象,則創建
客戶端角色維護了對所有享元對象的引用
需要保存維護享元對象的外部狀態,然后通過享元對象的業務邏輯方法作為參數形式傳遞
分類
單純享元模式
在上面的結構中,如果所有的ConcreteFlyWeight都可以被共享
也就是所有的FlyWeight子類都可以被共享,那就是所有的享元對象都可以被共享
這種形式被稱之為
單純享元模式
單純享元代碼
package flyweight.simple; public abstract class FlyWeight { /** * 抽象的業務邏輯方法,接受外部狀態作為參數 */ abstract public void operation(String outerState); }
package flyweight.simple; public class ConcreteFlyWeight extends FlyWeight { private String innerState = null; public ConcreteFlyWeight(String innerState) { this.innerState = innerState; } /** * 外部狀態作為參數傳遞 */ @Override public void operation(String outerState) { System.out.println("innerState = " + innerState + " outerState = " + outerState); } }
package flyweight.simple; import java.util.HashMap; public class FlyWeightFactory { /** * 單例模式 餓漢式創建 */ private static FlyWeightFactory instance = new FlyWeightFactory(); /** * 使用HashMap管理享元池 */ private HashMap<String, Object> hm = new HashMap<>(); private FlyWeightFactory() { } /** * 單例全局訪問接口獲取工廠 */ public static FlyWeightFactory getInstance() { return instance; } /** * 根據innerState獲取池中的對象 * 存在返回,不存在創建並返回 */ public FlyWeight getFylWeight(String innerState) { if(hm.containsKey(innerState)){ return (FlyWeight) hm.get(innerState); }else{ FlyWeight flyWeight = new ConcreteFlyWeight(innerState); hm.put(innerState,flyWeight); return flyWeight; } } }
package flyweight.simple; public class Test { public static void main(String[] args){ FlyWeightFactory flyWeightFactory = FlyWeightFactory.getInstance(); FlyWeight flyWeight1 = flyWeightFactory.getFylWeight("First"); FlyWeight flyWeight2 = flyWeightFactory.getFylWeight("Second"); FlyWeight flyWeight3 = flyWeightFactory.getFylWeight("First"); System.out.println(flyWeight1); System.out.println(flyWeight2); System.out.println(flyWeight3); System.out.println(); flyWeight1.operation("outer state XXX"); flyWeight2.operation("outer state YYY"); flyWeight3.operation("outer state ZZZ"); } }
復合享元模式
與單純享元模式對應的是復合享元模式
單純享元模式中,所有的享元對象都可以共享
復合享元模式中,則並不是所有的ConcreteFlyWeight都可以被共享
也就是說:
不是所有的享元對象都可以被共享
實際上,並不是所有的FlyWeight子類都需要被共享
FlyWeight接口使的可以進行共享,但是沒有任何必要 強制必須共享
實踐中,UnsharedConcreteFlyWeight對象通常將ConcreteFlyWeight對象作為子節點
與單純享元模式相比,僅僅是擁有了不可共享的具體子類
而且,這個子類往往是應用了組合模式,將ConcreteFlyWeight對象作為子節點
復合享元角色UnsharedConcreteFlyWeight復合享元角色,也就是不可共享的,也被稱為 不可共享的享元對象
但是一個復合享元對象可以分解為多個本身是單純享元對象的組合
這些單純的享元對象就又是可以共享的
復合享元代碼
將簡單模式中的示例代碼進行改造
FlyWeight不變
package flyweight.composite; public abstract class FlyWeight { /** * 抽象的業務邏輯方法,接受外部狀態作為參數 */ abstract public void operation(String outerState); }
ConcreteFlyWeight不變
package flyweight.composite; public class ConcreteFlyWeight extends FlyWeight { private String innerState = null; public ConcreteFlyWeight(String innerState) { this.innerState = innerState; } /** * 外部狀態作為參數傳遞 */ @Override public void operation(String outerState) { System.out.println("innerState = " + innerState + " outerState = " + outerState); } }
新增加不共享的子類也就是組合的享元子類
內部使用list 維護單純享元模式對象,提供add方法進行添加
提供operation操作
package flyweight.composite; import java.util.ArrayList; import java.util.List; public class UnsharedConcreateFlyWeight extends FlyWeight { private String innerState = null; public UnsharedConcreateFlyWeight(String innerState) { this.innerState = innerState; } private List<FlyWeight> list = new ArrayList<>(); public void add(FlyWeight flyWeight) { list.add(flyWeight); } @Override public void operation(String outerState) { for (FlyWeight flyWeight:list) { flyWeight.operation(outerState); } } }
FlyWeightFactory工廠類進行改造
新增加public UnsharedConcreateFlyWeight getCompositeFylWeight(String state)
用於獲得組合享元對象
package flyweight.composite; import java.util.HashMap; public class FlyWeightFactory { /** * 單例模式 餓漢式創建 */ private static FlyWeightFactory instance = new FlyWeightFactory(); /** * 使用HashMap管理享元池 */ private HashMap<String, Object> hm = new HashMap<>(); /** * 管理復合享元對象 */ private HashMap<String, Object> compositeHm = new HashMap<>(); private FlyWeightFactory() { } /** * 單例全局訪問接口獲取工廠 */ public static FlyWeightFactory getInstance() { return instance; } /** * 根據innerState獲取池中的對象 * 存在返回,不存在創建並返回 */ public FlyWeight getFylWeight(String innerState) { if(hm.containsKey(innerState)){ return (FlyWeight) hm.get(innerState); }else{ FlyWeight flyWeight = new ConcreteFlyWeight(innerState); hm.put(innerState,flyWeight); return flyWeight; } } /** * 根據innerState獲取池中的對象 * 存在返回,不存在創建並返回 */ public UnsharedConcreateFlyWeight getCompositeFylWeight(String state) { if(compositeHm.containsKey(state)){ return (UnsharedConcreateFlyWeight) compositeHm.get(state); }else{ UnsharedConcreateFlyWeight flyWeight = new UnsharedConcreateFlyWeight(state); compositeHm.put(state,flyWeight); return flyWeight; } } }
測試類也進行改造
package flyweight.composite; public class Test { public static void main(String[] args){ FlyWeightFactory flyWeightFactory = FlyWeightFactory.getInstance(); FlyWeight flyWeight1 = flyWeightFactory.getFylWeight("First"); FlyWeight flyWeight2 = flyWeightFactory.getFylWeight("Second"); FlyWeight flyWeight3 = flyWeightFactory.getFylWeight("First"); System.out.println(flyWeight1); System.out.println(flyWeight2); System.out.println(flyWeight3); System.out.println("###########################################"); flyWeight1.operation("outer state XXX"); flyWeight2.operation("outer state YYY"); flyWeight3.operation("outer state ZZZ"); System.out.println("###########################################"); UnsharedConcreateFlyWeight compositeFlyWeight = flyWeightFactory.getCompositeFylWeight("composite"); compositeFlyWeight.add(flyWeight1); compositeFlyWeight.add(flyWeight2); compositeFlyWeight.operation("composite out state OOO"); } }
測試程序在原來的基礎上,新獲得了一個組合享元對象
然后將兩個單純享元對象添加到組合享元對象中
然后調用operation,通過打印信息可以看得出來
不同的單純享元對象,他們卻有了一致的外部狀態
所以使用復合享元模式的一個常用目的就是:
多個內部狀態不同的單純享元對象,擁有一致的外部狀態
這種場景下,就可以考慮使用復合享元模式
使用場景
如果有下列情況,則可以考慮使用享元模式
- 應用程序中使用了大量的對象
- 大量的對象明顯增加了程序的存儲運行開銷
- 對象可以提取出內部狀態,並且可以分離外部狀態
使用享元模式有一點需要特別注意:應用程序運行不依賴這些對象的身份
換句話說這些對象是不做區分的,適用於“在客戶端眼里,他們都是一樣的”這種場景
比如單純的使用對象的方法,而不在意對象是否是創建而來的,否則如果客戶端鑒別對象的身份(equals),當他們是同一個對象時將會出現問題
總結
享元模式的核心就是共享
共享就需要找准內部狀態,以及分離外部狀態,
外部狀態由客戶端維護,在必要時候,通過參數的形式注入到享元對象中
在有大量重復或者相似對象的場景下,都可以考慮到享元模式
而且為了達到共享的目的,需要通過
工廠對象進行控制
只有通過工廠來維護享元池才能達到共享的目的,如果任意創建使用則勢必不能很好地共享
享元模式大大的減少了對象的創建,降低了系統所需要的內存空間
但是由於
將狀態分為內部狀態和外部狀態,而外部狀態是分離的,那么狀態的讀取必然會增大開銷
所以說
享元模式是時間換空間
如果確定需要使用享元模式,如果對於多個內部狀態不同的享元對象,希望他們擁有一致性的外部狀態
那么就可以考慮復合享元模式,復合享元模式是與合成模式的結合。