2014年1月8日 星期三

C# Design Pattern - Flyweight Pattern 享元模式

享元模式(英語:Flyweight Pattern)是一種軟體設計模式。它使用共享物件,用來儘可能減少記憶體使用量以及分享資訊給儘可能多的相似物件;它適合用於當大量物件只是重複因而導致無法令人接受的使用大量記憶體。通常物件中的部分狀態是可以分享。常見做法是把它們放在外部資料結構,當需要使用時再將它們傳遞給享元。
典型的享元模式的例子為文書處理器中以圖形結構來表示字元。一個做法是,每個字形有其字型外觀, 字模 metrics, 和其它格式資訊,但這會使每個字元就耗用上千位元組。取而代之的是,每個字元參照到一個共享字形物件,此物件會被其它有共同特質的字元所分享;只有每個字元(文件中或頁面中)的位置才需要另外儲存。

在軟體發展過程,如果需要重複使用某個物件的時候,如果重複地使用new創建這個物件的話,這樣在記憶體就需要多次地去申請記憶體空間了,這樣可能會出現記憶體使用越來越多的情況,這樣的問題是非常嚴重,然而享元模式可以解決這個問題,下面具體看看享元模式是如何去解決這個問題的。

優點:
  • 降低了系統中物件的數量,從而降低了系統中細細微性物件給記憶體帶來的壓力。

缺點:
  • 為了使物件可以共用,需要將一些狀態外部化,這使得程式的邏輯更複雜,使系統複雜化。
  • 享元模式將享元物件的狀態外部化,而讀取外部狀態使得執行時間稍微變長。
物件導向很好地解決了系統抽象性的問題,同時在大多數情況下,也不會損及系統的性能。但是,在某些特殊的應用下,由於物件的數量太大,採用物件導向會給系統帶來難以承受的記憶體。像是,圖形應用、文書處理時的字元等等。如何在避免大量細細微性物件問題的同時,讓外部客戶程式仍然能夠透明地使用物件導向的方式來進行操作,就是開發時必須要克服的問題。



Flyweight Pattern可以適用在下列的情況之下:

  •  一個系統有大量的物件。
  • 這些物件耗費大量的記憶體。
  • 這些物件的狀態中的大部分都可以外部化。
  • 這些物件可以按照內蘊狀態分成很多的組,當把外蘊物件從物件中剔除時,每一個組都可以僅用一個物件代替。
  • 軟體系統不依賴於這些物件的身份。

享元模式需要維護一個記錄了系統已有的所有享元的表,而這需要耗費資源。因此,應當在有足夠多的享元實例可供共用時才值得使用享元模式。

Flyweight可以理解為共用元物件(細細微性物件)的意思,提到Flyweight模式都會一般都會用編輯器例子來說明。考慮這樣一個文字處理軟體,它需要處理的物件可能有單個的字元,由字元組成的段落以及整篇文檔,根據物件導向的設計思想和Composite模式,不管是字元還是段落,文檔都應該作為單個的對象去看待,這裡只考慮單個的字元,不考慮段落及文檔等物件,於是可以很容易的得到下面的結構圖:



想像一下,在一篇文檔中,字元的數量遠不止幾百個這麼簡單,可能上千上萬,記憶體中就同時存在了上千上萬個Charactor物件,這樣的記憶體開銷是可想而知的。進一步分析可以發現,雖然需要的Charactor實例非常多,但這些只不過是狀態不同而已,也就是說這些實例的狀態數量是很少的。所以並不需要這麼多的獨立的Charactor實例,而只需要為每一種Charactor狀態創建一個實例,讓整個字元處理軟體共用這些實例就可以了。示意圖:



A,B,C三個字元是共用的,也就是說如果文檔中任何地方需要這三個字元,只需要使用共用的這三個實例就可以了。然而發現單純的這樣共用也是有問題的。雖然文檔中的用到了很多的A字元,雖然字元的symbol等是相同的,它可以共用;但是它們的pointSize卻是不相同的,即字元在文檔中中的大小是不相同的,這個狀態不可以共用。為解決這個問題,首先我們將不可共用的狀態從類裡面剔除,暫時去掉pointSize這個狀態,結構圖如下所示:



public abstract class Charactor
{
    protected char _symbol;
    protected int _width;
    protected int _height;
    protected int _ascent;
    protected int _descent;
    public abstract void Display();
}

public class CharactorA : Charactor
{
    public CharactorA()
    {
        this._symbol = 'A';
        this._height = 100;
        this._width = 120;
        this._ascent = 70;
        this._descent = 0;
    }

    public override void Display()
    {
        Console.WriteLine(this._symbol);
    }
}

public class CharactorB : Charactor
{
    public CharactorB()
    {
        this._symbol = 'B';
        this._height = 100;
        this._width = 140;
        this._ascent = 72;
        this._descent = 0;
    }

    public override void Display()
    {
        Console.WriteLine(this._symbol);
    }
}

public class CharactorC : Charactor
{
    public CharactorC()
    {
        this._symbol = 'C';
        this._height = 100;
        this._width = 160;
        this._ascent = 74;
        this._descent = 0;
    }

    public override void Display()
    {
        Console.WriteLine(this._symbol);
    }

}

在類別裡面剩下的狀態都可以共用了,之後要做的工作就是控制Charactor類的創建過程,如果已經存在了“A”字元這樣的實例,就不需要再創建,直接返回實例;如果沒有,則創建一個新的實例。如果把這項工作交給Charactor類,即Charactor類別在負責它自身職責的同時也要負責管理Charactor實例的管理工作,這在一定程度上有可能違背類的單一職責原則,因此,需要一個單獨的類來做這項工作,引入CharactorFactory類,結構圖如下:





public class CharactorFactory
{
    private Hashtable charactors = new Hashtable();
    public CharactorFactory()
    {
        charactors.Add("A", new CharactorA());
        charactors.Add("B", new CharactorB());
        charactors.Add("C", new CharactorC());
    }

    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;
                case "C": charactor = new CharactorC(); break;
            }

            charactors.Add(key, charactor);
        }

        return charactor;
    }

}

到這裡完全解決了可以共用的狀態,下面的工作就是處理剛才被我們剔除出去的那些不可共用的狀態,因為雖然將那些狀態移除了,但是Charactor物件仍然需要這些狀態,被剝離後這些物件根本就無法工作,所以需要將這些狀態外部化。首先用比較簡單的解決方案就是對於不能共用的那些狀態,不需要去在Charactor類中設置,而直接在客戶程式碼中進行設置,類結構圖如下:



public class Program
{
    public static void Main()
    {
        Charactor ca = new CharactorA();
        Charactor cb = new CharactorB();
        Charactor cc = new CharactorC();

    }
    public void ChangeSize() { }

}

如果有多個用戶端程式使用的話,會出現大量的重複性的邏輯,不利於代碼的複用和維護,另外把這些狀態和行為移到客戶程式裡面破壞了封裝性的原則。再次轉變我們的實現思路,可以確定的是這些狀態仍然屬於Charactor物件,所以它還是應該出現在Charactor類中,對於不同的狀態可以採取在客戶程式中通過參數化的方式傳入。類結構圖如下:


public abstract class Charactor
{
    protected char _symbol;
    protected int _width;
    protected int _height;
    protected int _ascent;
    protected int _descent;
    protected int _pointSize;
    public abstract void SetPointSize(int size);
    public abstract void Display();
}



public class CharactorA : Charactor
{
    public CharactorA()
    {
        this._symbol = 'A';
        this._height = 100;
        this._width = 120;
        this._ascent = 70;
        this._descent = 0;
    }

    public override void SetPointSize(int size)
    {
        this._pointSize = size;
    }

    public override void Display()
    {
        Console.WriteLine(this._symbol +
        "pointsize:" + this._pointSize);
    }
}

public class CharactorB : Charactor
{
    public CharactorB()
    {
        this._symbol = 'B';
        this._height = 100;
        this._width = 140;
        this._ascent = 72;
        this._descent = 0;
    }

    public override void SetPointSize(int size)
    {
        this._pointSize = size;
    }

    public override void Display()
    {
        Console.WriteLine(this._symbol +
        "pointsize:" + this._pointSize);
    }
}

public class CharactorC : Charactor
{
    public CharactorC()
    {
        this._symbol = 'C';
        this._height = 100;
        this._width = 160;
        this._ascent = 74;
        this._descent = 0;
    }

    public override void SetPointSize(int size)
    {
        this._pointSize = size;
    }

    public override void Display()
    {
        Console.WriteLine(this._symbol +
        "pointsize:" + this._pointSize);
    }
}

public class CharactorFactory
{
    private Hashtable charactors = new Hashtable();
    public CharactorFactory()
    {
        charactors.Add("A", new CharactorA());
        charactors.Add("B", new CharactorB());
        charactors.Add("C", new CharactorC());
    }

    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;
                case "C": charactor = new CharactorC(); break;
            }

            charactors.Add(key, charactor);
        }

        return charactor;
    }
}

public class Program
{
    public static void Main()
    {
        CharactorFactory factory = new CharactorFactory();
        CharactorA ca = (CharactorA)factory.GetCharactor("A");
        ca.SetPointSize(12);
        ca.Display();

        CharactorB cb = (CharactorB)factory.GetCharactor("B");
        ca.SetPointSize(10);
        ca.Display();

        CharactorC cc = (CharactorC)factory.GetCharactor("C");
        ca.SetPointSize(14);
        ca.Display();
    }

}

可以看到這樣的實現明顯優於第一種實現思路,Flyweight模式實現了優化資源的這樣一個目的。在這個過程中,還有如下幾點需要說明:

  1. 引入CharactorFactory是個關鍵,在這裡創建物件已經不是new一個Charactor物件那麼簡單,而必須用工廠方法封裝起來。
  2. 在這個例子中把Charactor物件作為Flyweight物件是否準確值的考慮,這裡只是為了說明Flyweight模式,至於在實際應用中,哪些物件需要作為Flyweight物件是要經過很好的計算得知。
  3. 區分內外部狀態很重要,這是Flyweight物件能做到享元的關鍵所在。

-雲遊山水為知已逍遙一生而忘齡- 電腦神手

沒有留言:

張貼留言