程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> .NET設計模式(13):享元模式(Flyweight Pattern)

.NET設計模式(13):享元模式(Flyweight Pattern)

編輯:關於.NET

本文試圖通過一個簡單的字符處理的例子,運用重構的手段,一步步帶你走進Flyweight模式,在這個過程中我們一同思考、探索、權衡,通過比較而得出好的實現方式,而不是給你最終的一個完美解決方案。

主要內容:

1.Flyweight模式解說

2..NET中的Flyweight模式

3.Flyweight模式的實現要點

……

概述

面向對象的思想很好地解決了抽象性的問題,一般也不會出現性能上的問題。但是在某些情況下,對象的數量可能會太多,從而導致了運行時的代價。那麼我們如何去避免大量細粒度的對象,同時又不影響客戶程序使用面向對象的方式進行操作?

意圖

運用共享技術有效地支持大量細粒度的對象。[GOF 《設計模式》]

結構圖

圖1 Flyweight模式結構圖

生活中的例子

享元模式使用共享技術有效地支持大量細粒度的對象。公共交換電話網(PSTN)是享元的一個例子。有一些資源例如撥號音發生器、振鈴發生器和撥號接收器是必須由所有用戶共享的。當一個用戶拿起聽筒打電話時,他不需要知道使用了多少資源。對於用戶而言所有的事情就是有撥號音,撥打號碼,撥通電話。

圖2 使用撥號音發生器例子的享元模式對象圖

Flyweight模式解說

Flyweight在拳擊比賽中指最輕量級,即“蠅量級”,這裡翻譯為“享元”,可以理解為共享元對象(細粒度對象)的意思。提到Flyweight模式都會一般都會用編輯器例子來說明,這裡也不例外,但我會嘗試著通過重構來看待Flyweight模式。考慮這樣一個字處理軟件,它需要處理的對象可能有單個的字符,由字符組成的段落以及整篇文檔,根據面向對象的設計思想和Composite模式,不管是字符還是段落,文檔都應該作為單個的對象去看待,這裡只考慮單個的字符,不考慮段落及文檔等對象,於是可以很容易的得到下面的結構圖:

圖3

示意性實現代碼:

// "Charactor"
public abstract class Charactor
{
  //Fields
  protected char _symbol;

  protected int _width;

  protected int _height;

  protected int _ascent;

  protected int _descent;

  protected int _pointSize;

  //Method
  public abstract void Display();
}

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

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

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

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

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

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

好了,現在看到的這段代碼可以說是很好地符合了面向對象的思想,但是同時我們也為此付出了沉重的代價,那就是性能上的開銷,可以想象,在一篇文檔中,字符的數量遠不止幾百個這麼簡單,可能上千上萬,內存中就同時存在了上千上萬個Charactor對象,這樣的內存開銷是可想而知的。進一步分析可以發現,雖然我們需要的Charactor實例非常多,這些實例之間只不過是狀態不同而已,也就是說這些實例的狀態數量是很少的。所以我們並不需要這麼多的獨立的Charactor實例,而只需要為每一種Charactor狀態創建一個實例,讓整個字符處理軟件共享這些實例就可以了。看這樣一幅示意圖:

圖4

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

圖5

示意性實現代碼:

// "Charactor"
public abstract class Charactor
{
  //Fields
  protected char _symbol;

  protected int _width;

  protected int _height;

  protected int _ascent;

  protected int _descent;

  //Method
  public abstract void Display();
}

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

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

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

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

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

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

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

圖6

示意性實現代碼:

// "CharactorFactory"
public class CharactorFactory
{
  // Fields
  private Hashtable charactors = new Hashtable();

  // Constructor
  public CharactorFactory()
  {
    charactors.Add("A", new CharactorA());
    charactors.Add("B", new CharactorB());
    charactors.Add("C", new CharactorC());
  }

  // Method
  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;
  }
}

到這裡已經完全解決了可以共享的狀態(這裡很丑陋的一個地方是出現了switch語句,但這可以通過別的辦法消除,為了簡單期間我們先保持這種寫法)。下面的工作就是處理剛才被我們剔除出去的那些不可共享的狀態,因為雖然將那些狀態移除了,但是Charactor對象仍然需要這些狀態,被我們剝離後這些對象根本就無法工作,所以需要將這些狀態外部化。首先會想到一種比較簡單的解決方案就是對於不能共享的那些狀態,不需要去在Charactor類中設置,而直接在客戶程序代碼中進行設置,類結構圖如下:

圖7

示意性實現代碼:

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

    //顯示字符

    //設置字符的大小ChangeSize();
  }

  public void ChangeSize()
  {
    //在這裡設置字符的大小
  }
}

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

圖8

示意性實現代碼:

// "Charactor"
public abstract class Charactor
{
  //Fields
  protected char _symbol;

  protected int _width;

  protected int _height;

  protected int _ascent;

  protected int _descent;

  protected int _pointSize;

  //Method
  public abstract void SetPointSize(int size);
  public abstract void Display();
}

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

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

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

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

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

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

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

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

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

// "CharactorFactory"
public class CharactorFactory
{
  // Fields
  private Hashtable charactors = new Hashtable();

  // Constructor
  public CharactorFactory()
  {
    charactors.Add("A", new CharactorA());
    charactors.Add("B", new CharactorB());
    charactors.Add("C", new CharactorC());
  }

  // Method
  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();

    // Charactor "A"
    CharactorA ca = (CharactorA)factory.GetCharactor("A");
    ca.SetPointSize(12);
    ca.Display();

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

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

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

1.引入CharactorFactory是個關鍵,在這裡創建對象已經不是new一個Charactor對象那麼簡單,而必須用工廠方法封裝起來。

2.在這個例子中把Charactor對象作為Flyweight對象是否准確值的考慮,這裡只是為了說明Flyweight模式,至於在實際應用中,哪些對象需要作為Flyweight對象是要經過很好的計算得知,而絕不是憑空臆想。

3.區分內外部狀態很重要,這是享元對象能做到享元的關鍵所在。

到這裡,其實我們的討論還沒有結束。有人可能會提出如下問題,享元對象(Charactor)在這個系統中相對於每一個內部狀態而言它是唯一的,這跟單件模式有什麼區別呢?這個問題已經很好回答了,那就是單件類是不能直接被實例化的,而享元類是可以被實例化的。事實上在這裡面真正被設計為單件的應該是享元工廠(不是享元)類,因為如果創建很多個享元工廠的實例,那我們所做的一切努力都是白費的,並沒有減少對象的個數。修改後的類結構圖如下:

圖9

示意性實現代碼:

// "CharactorFactory"
public class CharactorFactory
{
  // Fields
  private Hashtable charactors = new Hashtable();

  private CharactorFactory instance;
  // Constructor
  private CharactorFactory()
  {
    charactors.Add("A", new CharactorA());
    charactors.Add("B", new CharactorB());
    charactors.Add("C", new CharactorC());
  }

  // Property
  public CharactorFactory Instance
  {
    get
    {
      if (instance != null)
      {
        instance = new CharactorFactory();
      }
      return instance;
    }
  }

  // Method
  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;
  }
}

.NET框架中的Flyweight

Flyweight更多時候的時候一種底層的設計模式,在我們的實際應用程序中使用的並不是很多。在.NET中的String類型其實就是運用了Flyweight模式。可以想象,如果每次執行string s1 = “abcd”操作,都創建一個新的字符串對象的話,內存的開銷會很大。所以.NET中如果第一次創建了這樣的一個字符串對象s1,下次再創建相同的字符串s2時只是把它的引用指向“abcd”,這樣就實現了“abcd”在內存中的共享。可以通過下面一個簡單的程序來演示s1和s2的引用是否一致:

public class Program
{
  public static void Main(string[] args)
  {
    string s1 = "abcd";
    string s2 = "abcd";

    Console.WriteLine(Object.ReferenceEquals(s1,s2));

    Console.ReadLine();
  }
}

可以看到,輸出的結果為True。但是大家要注意的是如果再有一個字符串s3,它的初始值為“ab”,再對它進行操作s3 = s3 + “cd”,這時雖然s1和s3的值相同,但是它們的引用是不同的。關於String的詳細情況大家可以參考SDK,這裡不再討論了。

效果及實現要點

1.面向對象很好的解決了抽象性的問題,但是作為一個運行在機器中的程序實體,我們需要考慮對象的代價問題。Flyweight設計模式主要解決面向對象的代價問題,一般不觸及面向對象的抽象性問題。

2.Flyweight采用對象共享的做法來降低系統中對象的個數,從而降低細粒度對象給系統帶來的內存壓力。在具體實現方面,要注意對象狀態的處理。

3.享元模式的優點在於它大幅度地降低內存中對象的數量。但是,它做到這一點所付出的代價也是很高的:享元模式使得系統更加復雜。為了使對象可以共享,需要將一些狀態外部化,這使得程序的邏輯復雜化。另外它將享元對象的狀態外部化,而讀取外部狀態使得運行時間稍微變長。

適用性

當以下所有的條件都滿足時,可以考慮使用享元模式:

1、一個系統有大量的對象。

2、這些對象耗費大量的內存。

3、這些對象的狀態中的大部分都可以外部化。

4、這些對象可以按照內蘊狀態分成很多的組,當把外蘊對象從對象中剔除時,每一個組都可以僅用一個對象代替。

5、軟件系統不依賴於這些對象的身份,換言之,這些對象可以是不可分辨的。

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

總結

Flyweight模式解決的是由於大量的細粒度對象所造成的內存開銷的問題,它在實際的開發中並不常用,但是作為底層的提升性能的一種手段卻很有效。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved