設計模式-享元模式

HOLD ON!發表於2020-10-07

定義

運用共享技術有效地支援大量細粒度的物件。

適用場景

例如,資料庫連線,執行緒的建立開銷都比較大,並且建立頻率也非常高,因此就需要用到資料庫連線池技術和執行緒池技術來共享資料庫連線和執行緒。再例如,應用系統中通常存在最多的就是字串,並且往往會大量重複,如果每次都建立新的字串,可能會導致記憶體溢位、GC阻塞等效能問題,因此也就有了字串駐留池技術。應用場景雖然天差地別,但是無論哪一種場景,往往都會具備如下兩個特點:

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

目的

而享元模式正是為了應對上述問題,並達到如下兩個目的而存在的:

  • 減少建立物件的數量;
  • 物件全域性共享。

示例

其實,說到享元模式,我們最先應該提到的就是活字印刷術,因為它就是享元模式在生活中的一種最佳實踐。我們知道,出版一本哪怕百萬字的著作,其實常用漢字也不過三千多個,這其中會有大量重複。傳統的雕版印刷,每次印刷都需要先花大量的時間刻雕版,並且還不能重複使用,但是活字印刷就將共享和複用的特點發揮到了極致,省去了大量的時間。

其實,這種例子生活中並不罕見,例如,圖書館借書,共享單車,共享雨傘,共享馬紮等哪個不是享元模式思想的體現?因為享元模式的核心思想正是共享

我們下面還是以活字印刷舉例,通過程式碼的方式來實現一個印刷HELLO WORLD的例子加以說明。

模式演進

首先,我們先把字模刻出來:

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";
    }
}

將每個字模都實現成如上所示的單例,再看看呼叫的地方:

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類圖

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

  • 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方法換成高併發環境下的查詢介面,再去看看執行流程。發現了吧?就是我們每天都在寫的程式碼。
用心發現,享元模式真的是無處不在!

原始碼連結

相關文章