13. 設計模式-享元模式

定義

運用共享技術有效地支持大量細粒度的對象。git

適用場景

例如,數據庫鏈接,線程的建立開銷都比較大,而且建立頻率也很是高,所以就須要用到數據庫鏈接池技術和線程池技術來共享數據庫鏈接和線程。再例如,應用系統中一般存在最多的就是字符串,而且每每會大量重複,若是每次都建立新的字符串,可能會致使內存溢出、GC阻塞等性能問題,所以也就有了字符串駐留池技術。應用場景雖然天差地別,可是不管哪種場景,每每都會具有以下兩個特色:github

  • 系統會用到大量相同或類似的對象;
  • 對象建立比較耗時。

目的

而享元模式正是爲了應對上述問題,並達到以下兩個目的而存在的:數據庫

  • 減小建立對象的數量;
  • 對象全局共享。

示例

其實,說到享元模式,咱們最早應該提到的就是活字印刷術,由於它就是享元模式在生活中的一種最佳實踐。咱們知道,出版一本哪怕百萬字的著做,其實經常使用漢字也不過三千多個,這其中會有大量重複。傳統的雕版印刷,每次印刷都須要先花大量的時間刻雕版,而且還不能重複使用,可是活字印刷就將共享和複用的特色發揮到了極致,省去了大量的時間。
image
其實,這種例子生活中並不罕見,例如,圖書館借書,共享單車,共享雨傘,共享馬紮等哪一個不是享元模式思想的體現?由於享元模式的核心思想正是共享緩存

咱們下面仍是以活字印刷舉例,經過代碼的方式來實現一個印刷HELLO WORLD的例子加以說明。併發

模式演進

首先,咱們先把字模刻出來:ide

public abstract class Typeface
{
    public abstract string Print();
}

public class DTypeface : Typeface
{
    public override string Print()
    {
        return "D";
    }
}

public class ETypeface : Typeface
{
    public override string Print()
    {
        return "E";
    }
}

...

上面是簡單的示意,其餘字母以此類推,表示一個個的字模。經過這些字模咱們就能夠印刷出版了。高併發

static void Main(string[] args)
{
    Typeface h = new HTypeface();
    Typeface e = new ETypeface();
    Typeface l = new LTypeface();
    Typeface o = new OTypeface();
    Typeface w = new WTypeface();
    Typeface r = new RTypeface();
    Typeface d = new DTypeface();

    Console.WriteLine($"{h.Print()}{e.Print()}{l.Print()}{l.Print()}{o.Print()} {w.Print()}{o.Print()}{r.Print()}{l.Print()}{d.Print()}");
}

可是很遺憾,雖然印刷成功了,可是這些字模並不能全局共享,說到底仍是一次性的,換一個地方還得從新建立一次。不過說到全局共享,咱們前面好像就有一種模式能夠辦到,沒錯,就是單例模式。咱們不妨先用單例模式試試看:性能

public class ETypeface : Typeface
{
    private static readonly Typeface _instance = new ETypeface();

    private ETypeface() { }

    public static Typeface Instance => _instance;

    public override string Print()
    {
        return "E";
    }
}

將每一個字模都實現成如上所示的單例,再看看調用的地方:spa

static void Main(string[] args)
{
    Console.WriteLine($"{HTypeface.Instance.Print()}" +
        $"{ETypeface.Instance.Print()}{LTypeface.Instance.Print()}" +
        $"{LTypeface.Instance.Print()}{OTypeface.Instance.Print()}");
}

印刷成功了,而且也全局共享了。不過中國漢字何其多,所有實現成單例,類爆炸了,一個系統中成千上萬個單例,想一想均可怕。不過好在處理類爆炸咱們是有經驗的,沒錯,就是合併:線程

public class TypefaceFactory
{
    private static readonly Typeface _h = new HTypeface();
    private static readonly Typeface _e = new ETypeface();
    private static readonly Typeface _l = new LTypeface();
    private static readonly Typeface _o = new OTypeface();

    public static Typeface H => _h;
    public static Typeface E => _e;
    public static Typeface L => _l;
    public static Typeface O => _o;
}

咱們額外定義一個類,把全部單例字模都合併進去,不過咱們這時靜態屬性若是還叫Instance就命名衝突了,直接以字母命名好了,這樣咱們就把全部單例都消滅了。雖然有所改善,不過字模太多的問題依然嚴峻,由於這個類中會封裝成千上萬的字模,而且隨時可能更改,這致使這個類極不穩定。不過好在全部字模都繼承自同一個基類,所以,咱們能夠用一個字典存儲,而且經過一個靜態方法獲取字模:

public class TypefaceFactory
{
    private static readonly IDictionary<Type, Typeface> _typefaces 
        = new Dictionary<Type, Typeface>();

    public static Typeface GetTypeface<TTypeface>() where TTypeface : Typeface
    {
        Type type = typeof(TTypeface);
        if (_typefaces.ContainsKey(type))
        {
            return _typefaces[type];
        }

        Typeface typeface = Activator.CreateInstance(typeof(TTypeface)) as Typeface;
        _typefaces.Add(type, typeface);
        return typeface;
    }
}

這樣的話就好多了,能夠管理大量細粒度的對象,而且也能夠全局共享了,知足了咱們的需求,不知你們有沒有發現,這裏很是像簡單工廠模式,只不過這裏用到了一個靜態字典作緩存,並不是每次都全新建立對象,其實這就是享元模式。

UML類圖

再來抽象一下,看看享元模式的類圖:
image

  • FlyweightFactory:享元工廠,用來建立並管理Flyweight對象
  • Flyweight:享元類的基類或接口
  • ConcreteFlyweight:具體的Flyweight子類
  • UnsharedConcreteFlyweight:不須要共享的Flyweight子類

在本例中,UnsharedConcreteFlyweight並無用到,可是做爲享元模式中的一個角色確實是存在的,只是不可共享而已。例如,字模中有規範漢字,也有非規範漢字,可是出版刊物必須使用規範漢字,而不能使用非規範漢字。不過,咱們軟件開發中會較少用到,由於,既然用不到,就不必去實現了。

優缺點

優勢

  • 節省內存空間,由於全局共享一個或者少數幾個對象而已;
  • 提升效率,由於不用每次都進行費時的初始化操做。

缺點

增長了系統的複雜度,其實咱們經過線程池和數據庫鏈接池就不難發現,確實複雜了不少。

改進

其實,到這裏咱們並無結束,若是還記得單例模式的話,咱們知道這種實現是存在併發問題的,沒錯,既然一樣是用靜態字段作共享,那麼這裏一樣存在這併發問題,不過這裏併發的是一個代碼段,而不是簡單的一個字段,所以就不能簡單的經過Lazy關鍵字解決了,這裏必須使用雙檢鎖:

public class TypefaceFactory
{
    private static readonly IDictionary<Type, Typeface> _typefaces 
        = new Dictionary<Type, Typeface>();

    private static readonly object _locker = new object();
    public static Typeface GetTypeface<TTypeface>() where TTypeface : Typeface
    {
        Type type = typeof(TTypeface);
        if (!_typefaces.ContainsKey(type))
        {
            lock (_locker)
            {
                if (!_typefaces.ContainsKey(type))
                {
                    Typeface typeface = Activator.CreateInstance(typeof(TTypeface)) as Typeface;
                    _typefaces.Add(type, typeface);
                }
            }
        }

        return _typefaces[type];
    }
}

好了,此次完美了。

不過呢,不知你們有沒有疑惑,從上面演進步驟看,享元模式好像是單例模式和簡單工廠模式的綜合運用,爲何享元模式會歸類到結構型模式而不是建立型模式呢?其實,緣由很簡單,從表面上看,好像享元模式的享元工廠也在負責建立對象,但實際上,享元模式最主要的目的是對象的管理而不是建立,例如,咱們還能夠經過以下方式實現享元模式:

public class TypefaceFactory
{
    private static readonly IDictionary<string, Typeface> _typefaces
        = new Dictionary<string, Typeface>();


    private static readonly object _locker = new object();

    public static void SetTypeface(string key, Typeface typeface)
    {
        if (!_typefaces.ContainsKey(key))
        {
            lock (_locker)
            {
                if (!_typefaces.ContainsKey(key))
                {
                    _typefaces.Add(key, typeface);
                }
            }
        }
    }

    public static Typeface GetTypeface(string key)
    {
        if (_typefaces.ContainsKey(key))
        {
            return _typefaces[key];
        }

        return null;
    }
}

看到了嗎?這裏就把對象的建立交給了客戶端完成,而享元工廠只負責對象的管理,並不負責對象建立了。

與單例模式的區別

  • 享元模式是共享大量類的大量實例,而單例是一個類一個實例;
  • 單例模式針對的是對象的建立,而享元模式針對的是對象的管理;
  • 單例模式不能單首創建,而享元模式中的類能夠單首創建。

與簡單工廠模式的區別

  • 享元模式在簡單工廠模式的基礎上加入了緩存;
  • 簡單工廠模式的做用僅僅是建立對象,而享元模式雖然也建立對象,但其主要做用是管理和共享對象。

總結

享元模式實現起來很是靈活,它更重要體現的是一種思想,它不只在生活中被普遍運用,在軟件開發過程當中也被普遍運用。不妨把上述享元工廠再換一個場景,例如把靜態字典換成Redis,再把GetTypeface方法換成高併發環境下的查詢接口,再去看看執行流程。發現了吧?就是咱們天天都在寫的代碼。
用心發現,享元模式真的是無處不在!

源碼連接
更多內容,歡迎關注公衆號:
image

相關文章
相關標籤/搜索