文章出處

設計模式之享元模式

1         享元模式的日常應用

面向對象的思想確實很好地解決了抽象性的問題,以至于在面向對象的眼中,萬事萬物一切皆對象。不可避免的是,采用面向對象的編程方式,可能會增加一些資源和性能上的開銷。不過,在大多數情況下,這種影響還不是太大,所以,它帶來的空間和性能上的損耗相對于它的優點而言,基本上不用考慮。但是,在某些特殊情況下,大量細粒度對象的創建、銷毀以及存儲所造成的資源和性能上的損耗,可能會在系統運行時形成瓶頸。那么我們該如何去避免產生大量的細粒度對象,同時又不影響系統使用面向對象的方式進行操作呢?享元設計模式提供了一個比較好的解決方案。

公共交換電話網的使用方式就是生活中常見的享元模式的例子。公共交換電話網中的一些資源,例如撥號音發生器、振鈴發生器和撥號接收器,都是必須由所有用戶共享的,不可能為每一個人都配備一套這樣的資源,否則公共交換電話網的資源開銷也太大了。當一個用戶拿起聽筒打電話時,他根本不需要知道到底使用了多少資源,對用戶而言所有的事情就是有撥號音,撥打號碼,撥通電話就行了。所以,就有很有人會共用一套資源,非常節省,這就是享元模式的基本思想。

假如我們要開發一個類似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也有明顯區別,每一個腳本文件執行開始,將會裝入所有需要的資源;執行結束后,又將占用的資源就立即全部釋放,所以它基本上不會產生類似的性能問題,它的字符串處理的設計,自然也使用不到享元模式。

http://blog.csdn.net/wanghao72214/article/details/4046182


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()