設計模式之享元模式
面向對象的思想確實很好地解決了抽象性的問題,以至于在面向對象的眼中,萬事萬物一切皆對象。不可避免的是,采用面向對象的編程方式,可能會增加一些資源和性能上的開銷。不過,在大多數情況下,這種影響還不是太大,所以,它帶來的空間和性能上的損耗相對于它的優點而言,基本上不用考慮。但是,在某些特殊情況下,大量細粒度對象的創建、銷毀以及存儲所造成的資源和性能上的損耗,可能會在系統運行時形成瓶頸。那么我們該如何去避免產生大量的細粒度對象,同時又不影響系統使用面向對象的方式進行操作呢?享元設計模式提供了一個比較好的解決方案。
公共交換電話網的使用方式就是生活中常見的享元模式的例子。公共交換電話網中的一些資源,例如撥號音發生器、振鈴發生器和撥號接收器,都是必須由所有用戶共享的,不可能為每一個人都配備一套這樣的資源,否則公共交換電話網的資源開銷也太大了。當一個用戶拿起聽筒打電話時,他根本不需要知道到底使用了多少資源,對用戶而言所有的事情就是有撥號音,撥打號碼,撥通電話就行了。所以,就有很有人會共用一套資源,非常節省,這就是享元模式的基本思想。
假如我們要開發一個類似MS Word的字處理軟件,下面分析一下將如何來實現。對于這樣一個字處理軟件,它需要處理的對象既有單個字符,又有由字符組成的段落以及整篇文檔,根據面向對象的設計思想,不管是字符、段落還是文檔都應該作為單個的對象去看待。我們暫不考慮段落和文檔對象,只考慮單個的字符,于是可以很容易的得到下面的結構圖:
Java代碼:
//抽象的字符類
public abstract class Charactor{
//屬性
protected char letter;
protected int fontsize;
//顯示方法
public abstract void display();
}
//具體的字符類A
public class CharactorA extends Charactor{
//構造函數
public CharactorA(){
this.letter = 'A';
this.fontsize = 12;
}
//顯示方法
public void display(){
try{
System.out.println(this.letter);
}catch(Exception err){
}
}
}
//具體的字符類B
public class CharactorB extends Charactor{
//構造函數
public CharactorB(){
this.letter = 'B';
this.fontsize = 12;
}
//顯示方法
public void display(){
try{
System.out.println(this.letter);
}catch(Exception err){
}
}
}
.Net代碼:
//抽象的字符類
public abstract class Charactor{
//屬性
protected char letter;
protected int fontsize;
//顯示方法
public abstract void display();
}
//具體的字符類A
public class CharactorA : Charactor{
//構造函數
public CharactorA(){
this.letter = 'A';
this.fontsize = 12;
}
//顯示方法
public override void display(){
Console.WriteLine(this.letter);
}
}
//具體的字符類B
public class CharactorB: Charactor{
//構造函數
public CharactorB(){
this.letter = 'B';
this.fontsize= 14;
}
//顯示方法
public override void display(){
Console.WriteLine(this.letter);
}
}
我們的這段代碼完全符合面向對象的思想,但是卻為此搭上了太多的性能損耗,代價很昂貴。
一篇文檔的字符數量很可能達到成千上萬,甚至更多,那么在內存中就會同時存在大量的Charactor對象,這時候的內存開銷可想而知。
我們對內存中的對象稍加分析就能發現,雖然內存中Character實例很多,但是里面有很多實例差不多是相同的,比如CharactorA類的實例就有可能出現過很多次,這些不同的CharactorA的實例之間只有部分狀態不同而已。那么,我們是不是可以只創建一份CharactorA的實例,然后讓整個系統共享這個實例呢?直接使用顯然是行不通的。比如一份文檔中使用了很多的字符A,雖然它們的屬性letter相同,都是'A',但是它們的fontsize卻不相同的,即字符大小并不相同。顯然,對于實例中的相同狀態是可以共享的,不同的狀態就不能共享了。
為了解決這個問題,我們可以變換一下思路:首先將不可共享的狀態從類里面剔除出去,即去掉fontsize這個屬性,這時候我們再寫一下代碼:
Java代碼:
//抽象的字符類
public abstract class Charactor{
//屬性
protected char letter;
//顯示方法
public abstract void display();
}
//具體的字符類A
public class CharactorA extends Charactor{
//構造函數
public CharactorA(){
this.letter = 'A';
}
//顯示方法
public void display(){
try{
System.out.println(this.letter);
}catch(Exception err){
}
}
}
//具體的字符類B
public class CharactorB extends Charactor{
//構造函數
public CharactorB(){
this.letter = 'B';
}
//顯示方法
public void display(){
try{
System.out.println(this.letter);
}catch(Exception err){
}
}
}
.Net代碼:
//抽象的字符類
public abstract class Charactor{
//屬性
protected char letter;
//顯示方法
public abstract void display();
}
//具體的字符類A
public class CharactorA : Charactor{
//構造函數
public CharactorA(){
this.letter = 'A';
}
//顯示方法
public override void display(){
Console.WriteLine(this.letter);
}
}
//具體的字符類B
public class CharactorB: Charactor{
//構造函數
public CharactorB(){
this.letter = 'B';
}
//顯示方法
public override void display(){
Console.WriteLine(this.letter);
}
}
經過這次重構,類里面剩余的狀態就可以共享了,下面我們要做的工作就是要控制Charactor類的創建過程。如果已經存在了“A”字符這樣的實例,就不需要再創建,直接返回實例;如果沒有,則創建一個新的實例,這跟單例模式的做法有點類似了。在單例模式中是由類自身維護一個唯一的實例,享元模式則引入一個單獨的工廠類CharactorFactory來完成這項工作:
Java代碼:
public class CharactorFactory{
private Hashtable<String,Charactor> charactors = new Hashtable<String,Charactor>();
//構造函數
public CharactorFactory(){
charactors.put("A", new CharactorA());
charactors.put("B", new CharactorB());
}
//獲得指定字符實例
public Charactor getCharactor(String key){
Charactor charactor = (Charactor)charactors.get(key);
if (charactor == null){
if(key.equals("A")){
charactor = new CharactorA();
}else if(key.equals("B")){
charactor = new CharactorB();
}
charactors.put(key, charactor);
}
return charactor;
}
}
.Net代碼:
//享元類工廠
public class CharactorFactory{
private Hashtable charactors = new Hashtable();
//構造函數
public CharactorFactory(){
charactors.Add("A", new CharactorA());
charactors.Add("B", new CharactorB());
}
//獲得指定字符實例
public Charactor getCharactor(String key){
Charactor charactor = charactors[key] as Charactor;
if (charactor == null){
switch (key){
case "A": charactor = new CharactorA(); break;
case "B": charactor = new CharactorB(); break;
}
charactors.Add(key, charactor);
}
return charactor;
}
}
經過本次重構,已經可以使用同一個實例來存儲可共享的狀態,下面還需要做的工作就是要處理被剔除出去的那些不可共享的狀態。缺少了這些不可共享的狀態,Charactor對象就無法正常工作。
2 解決對象中不可共享狀態的問題
我們先考慮一種比較簡單的解決方案:對于不能共享的狀態,不要在Charactor類中設置,而是由客戶程序在自己的代碼中進行設置:
Java代碼:
//客戶程序
public class ClinetTest{
public static void main(String[] args){
Charactor a = new CharactorA();
Charactor b = new CharactorB();
//顯示字符A
display(a,12);
//顯示字符B
display(b,14);
}
//設置字符的大小
public void display(Charactor objChar, int nSize){
try{
System.out.println("字符:" + objChar.letter + ",大小:" + nSize);
}catch(Exception err){
}
}
}
.Net代碼:
//客戶程序
public class ClinetTest{
public static void Main(String[] args){
Charactor a = new CharactorA();
Charactor b = new CharactorB();
//顯示字符A
display(a,12);
//顯示字符B
display(b,14);
}
//設置字符的大小
public void display(Charactor objChar, int nSize){
Console.WriteLine("字符:" + objChar.letter + ",大小:" + nSize);
}
}
按照這樣的實現思路,可以發現如果有多個客戶端程序使用的話,會出現大量的重復性的邏輯,就像上面這段代碼中的display方法一樣,需要所有的客戶端都提供,因此,這段代碼已經出現了臭味,非常不利于代碼的復用和維護。另外,把這些狀態和行為移到客戶程序里面破壞了面向對象中封裝的原則。
所以,我們再次轉變我們的實現思路,把這些不可共享的狀態仍然保留在Charactor對象中,把不同的狀態通過參數化的方式,由客戶程序注入。以下代碼是我們最終實現的一個版本:
Java代碼:
//抽象的字符類
public abstract class Charactor{
//屬性
protected char letter;
protected int fontsize;
//顯示方法
public abstract void display();
//設置字體大小
public abstract void setFontSize(int fontsize);
}
//具體的字符類A
public class CharactorA extends Charactor{
//構造函數
public CharactorA(){
this.letter = 'A';
this.fontsize = 12;
}
//顯示方法
public void display(){
try{
System.out.println(this.letter);
}catch(Exception err){
}
}
//設置字體大小
public void setFontSize(int fontsize){
this.fontsize = fontsize;
}
}
//具體的字符類B
public class CharactorB extends Charactor{
//構造函數
public CharactorB(){
this.letter = 'B';
this.fontsize = 12;
}
//顯示方法
public void display(){
try{
System.out.println(this.letter);
}catch(Exception err){
}
}
//設置字體大小
public void setFontSize(int fontsize){
this.fontsize = fontsize;
}
}
//客戶程序
public class ClinetTest{
public static void main(String[] args){
Charactor a = new CharactorA();
Charactor b = new CharactorB();
//設置字符A的大小
a.setFontSize(12);
//顯示字符B
a.display();
//設置字符B的大小
b.setFontSize(14);
//顯示字符B
b.display();
}
}
.Net代碼:
//抽象的字符類
public abstract class Charactor{
//屬性
protected char letter;
protected int fontsize;
//顯示方法
public abstract void display();
//設置字體大小
public abstract void setFontSize(int fontsize);
}
//具體的字符類A
public class CharactorA : Charactor{
//構造函數
public CharactorA(){
this.letter = 'A';
this.fontsize = 12;
}
//顯示方法
public override void display(){
Console.WriteLine(this.letter);
}
//設置字體大小
public override void setFontSize(int fontsize){
this.fontsize = fontsize;
}
}
//具體的字符類B
public class CharactorB: Charactor{
//構造函數
public CharactorB(){
this.letter = 'B';
this.fontsize= 14;
}
//顯示方法
public override void display(){
Console.WriteLine(this.letter);
}
//設置字體大小
public override void setFontSize(int fontsize){
this.fontsize = fontsize;
}
}
//客戶程序
public class ClinetTest{
public static void Main(String[] args){
Charactor a = new CharactorA();
Charactor b = new CharactorB();
//設置字符A的大小
a.setFontSize(12);
//顯示字符B
a.display();
//設置字符B的大小
b.setFontSize(14);
//顯示字符B
b.display();
}
}
可以看到這樣的實現明顯優于第一種實現思路,這就是享元模式的基本思想。我們通過享元模式實現了節省存儲資源的目的。
3 什么是享元模式
享元的英文是Flyweight,它是一個來自于體育方面的專業用語,在拳擊、摔跤和舉重比賽中特指最輕量的級別。把這個單詞移植到軟件工程里面,也是用來表示特別小的對象,即細粒度對象。至于為什么我們把Flyweight翻譯為“享元”,可以理解為共享元對象,也就是共享細粒度對象。享元模式就是通過使用共享的方式,達到高效地支持大量的細粒度對象。它的目的就是節省占用的空間資源,從而實現系統性能的改善。
我們把享元對象的所有狀態分成兩類,其實前面的例子中letter和fontsize屬性在運行時,就形成了兩類不同的狀態。
享元對象的第一類狀態稱為內蘊狀態(Internal State)。它不會隨環境改變而改變,存儲在享元對象內部,因此內蘊狀態是可以共享的,對于任何一個享元對象來講,它的值是完全相同的。我們例子中Character類的letter屬性,它代表的狀態就是內蘊狀態。
享元對象的第二類狀態稱為外蘊狀態(External State)。它會隨環境的改變而改變,因此是不可以共享的狀態,對于不同的享元對象來講,它的值可能是不同的。享元對象的外蘊狀態必須由客戶端保存,在享元對象被創建之后,需要使用的時候再傳入到享元對象內部。我們例子中Character類的fontsize屬性,它代表的狀態就是外蘊狀態。
所以享元的外蘊狀態與內蘊狀態是兩類相互獨立的狀態,彼此沒有關聯。
我們按照前面的分析,給出享元模式的類圖:
享元模式類圖
l 抽象享元類(Flyweight)
它是所有具體享元類的超類。為這些類規定出需要實現的公共接口,那些需要外蘊狀態(Exte的操作可以通過方法的參數傳入。抽象享元的接口使得享元變得可能,但是并不強制子類實行共享,因此并非所有的享元對象都是可以共享的。
l 具體享元類(ConcreteFlyweight)
具體享元類實現了抽象享元類所規定的接口。如果有內蘊狀態的話,必須負責為內蘊狀態提供存儲空間。享元對象的內蘊狀態必須與對象所處的周圍環境無關,從而使得享元對象可以在系統內共享。有時候具體享元類又稱為單純具體享元類,因為復合享元類是由單純具體享元角色通過復合而成的。
l 不能共享的具體享元類(UnsharableFlyweight)
不能共享的享元類,又叫做復合享元類。一個復合享元對象是由多個單享元對象組成,這些組成的對象是可以共享的,但是復合享元類本身并不能共享。
l 享元工廠類(FlyweightFactoiy)
享元工廠類負責創建和管理享元對象。當一個客戶端對象請求一個享元對象的時候,享元工廠需要檢查系統中是否已經有一個符合要求的享元對象,如果已經有了,享元工廠角色就應當提供這個已有的享元對象;如果系統中沒有適當的享元對象的話,享元工廠角色就應當創建一個新的合適的享元對象。
l 客戶類(Client)
客戶類需要自行存儲所有享元對象的外蘊狀態。
4 實現和使用享元模式需要注意的問題
面向對象雖然很好地解決了抽象性的問題,但是對于一個實際運行的軟件系統,我們還需要考慮面向對象的代價問題,享元模式解決的就是面向對象的代價問題。享元模式采用對象共享的做法來降低系統中對象的個數,從而降低細粒度對象給系統帶來的內存壓力。
在具體實現方面,我們要注意對象狀態的處理,一定要正確地區分對象的內蘊狀態和外蘊狀態,這是實現享元模式的關鍵所在。
享元模式的優點在于它大幅度地降低內存中對象的數量。為了做到這一點,享元模式也付出了一定的代價:
1、享元模式為了使對象可以共享,它需要將部分狀態外部化,這使得系統的邏輯變得復雜。
2、享元模式將享元對象的部分狀態外部化,而讀取外部狀態使得運行時間會有所加長。
另外,我們還有一個比較關心的問題:到底系統需要滿足什么樣的條件才能使用享元模式。對于這個問題,我們總結了以下幾條:
1、一個系統中存在著大量的細粒度對象;
2、這些細粒度對象耗費了大量的內存。
3、這些細粒度對象的狀態中的大部分都可以外部化;
4、這些細粒度對象可以按照內蘊狀態分成很多的組,當把外蘊對象從對象中剔除時,每一個組都可以僅用一個對象代替。
5、軟件系統不依賴于這些對象的身份,換言之,這些對象可以是不可分辨的。
滿足以上的這些條件的系統可以使用享元對象。最后,使用享元模式需要維護一個記錄了系統已有的所有享元的哈希表,也稱之為對象池,而這也需要耗費一定的資源。因此,應當在有足夠多的享元實例可供共享時才值得使用享元模式。如果只能夠節省百八十個對象的話,還是沒有必要引入享元模式的,畢竟性價比不高。
5 什么情況下使用享元模式
享元模式在一般的項目開發中并不常用,而是常常應用于系統底層的開發,以便解決系統的性能問題。
Java和.Net中的String類型就是使用了享元模式。如果在Java或者.NET中已經創建了一個字符串對象s1,那么下次再創建相同的字符串s2的時候,系統只是把s2的引用指向s1所引用的具體對象,這就實現了相同字符串在內存中的共享。如果每次執行s1=“abc”操作的時候,都創建一個新的字符串對象的話,那么內存的開銷會很大。
如果大家有興趣的話,可以用下面的程序進行測試,就會知道s1和s2的引用是否一致:
Java代碼:
String s1 = "測試字符串1";
String s2 = "測試字符串1";
//“==”用來判斷兩個對象是否是同一個,equals判斷字符串的值是否相等
if( s1 == s2 ){
System.out.println("兩者一致");
}else{
System.out.println("兩者不一致");
}
.Net代碼:
String s1 = "測試字符串1";
String s2 = "測試字符串1";
if( Object.ReferenceEquals(s1, s2) ){
Console.WriteLine("兩者一致");
}else{
Console.WriteLine("兩者不一致");
}
程序運行后,輸出的結果為“兩者一致”,這說明String類的設計采用了享元模式。如果s1的內容發生了變化,比如執行了s1 += "變化"的語句,那么s1與s2的引用將不再一致。
至于Php作為一種弱類型語言,它的字符串類型是一種基本類型,不是對象。另外,它的執行方式與Java和.Net也有明顯區別,每一個腳本文件執行開始,將會裝入所有需要的資源;執行結束后,又將占用的資源就立即全部釋放,所以它基本上不會產生類似的性能問題,它的字符串處理的設計,自然也使用不到享元模式。
文章列表