12. Flyweight


Posted by WayneCheng on 2021-01-19

關於 Flyweight 本篇將討論以下幾個問題

1. 關於 Flyweight

2. UML

3. 將 UML 轉為程式碼

4. 情境


測試環境:

OS:Windows 10
IDE:Visual Studio 2019


1. 關於 Flyweight

Use sharing to support large numbers of fine-grained objects efficiently.

by Gang of Four

  • 用來有效率的共享大量細粒度的對象

Flyweight(享元)屬於結構型(Structural Patterns),當遇到系統中頻繁創建且只會異動狀態而不改變物件本身的小物件時,可藉由 Flyweight 來緩存物件,提供不同呼叫端重複使用,達到節省記憶體的目的。

優點:

  • 藉由共用相似物件來達到節省記憶體

缺點:

  • 使用 Flyweight 的物件存於緩存中,就算沒使用也不會被 GC 回收

2. UML

Class 間關聯:

  • FlyweightFactory 可包含 Flyweight
  • Client 關聯 FlyweightFactory & ConcreteFlyweight
  • ConcreteFlyweight 繼承 Flyweight

Class:

  • Client:呼叫端
  • FlyweightFactory:以工廠方法實作享元工廠
  • Flyweight:享元的抽象類別或介面
  • ConcreteFlyweight:享元實作

3. 將 UML 轉為程式碼

享元介面

/// <summary>
/// 享元介面
/// </summary>
public interface IFlyweight
{
    void Operation(int extrinsicState);
}

享元工廠

/// <summary>
/// 享元工廠
/// </summary>
public class FlyweightFactory
{
    private Hashtable flyweights = new Hashtable();

    public FlyweightFactory()
    {
        flyweights.Add("One", new ConcreteFlyweight());
        flyweights.Add("Two", new ConcreteFlyweight());
        flyweights.Add("Three", new ConcreteFlyweight());
    }

    public IFlyweight GetFlyweight(string key)
    {
        return ((IFlyweight)flyweights[key]);
    }
}

享元實作

/// <summary>
/// 享元實作
/// </summary>
public class ConcreteFlyweight : IFlyweight
{
    public void Operation(int extrinsicState)
    {
        Console.WriteLine($"ConcreteFlyweight: {extrinsicState}");
    }
}
  1. 建立享元工廠factory
  2. 取得享元工廠中已建立實體
  3. 操作享元物件
static void Main(string[] args)
{
    int extrinsicstate = 0;

    Default.FlyweightFactory factory = new Default.FlyweightFactory();

    Default.IFlyweight flyweight_1 = factory.GetFlyweight("One");
    flyweight_1.Operation(++extrinsicstate);

    Default.IFlyweight flyweight_2 = factory.GetFlyweight("Two");
    flyweight_2.Operation(++extrinsicstate);

    Default.IFlyweight flyweight_3 = factory.GetFlyweight("Three");
    flyweight_3.Operation(++extrinsicstate);

    Console.ReadLine();
}

執行結果

ConcreteFlyweight: 1
ConcreteFlyweight: 2
ConcreteFlyweight: 3

4. 情境

我們接到了一個提供線上商城客戶結帳時轉換匯率的需求

  • 線上商城客戶很多,每個客戶都要 new 匯率 class 很吃記憶體
  • 匯率固定提供部分貨幣類別,介面相同

貨幣類別 Enum

/// <summary>
/// 貨幣類別
/// </summary>
public enum CurrencyType
{
    TWD,
    JPY,
    USD,
    EUR,
}

實作匯率工廠,並在建構子提供對應匯率

/// <summary>
/// 匯率工廠
/// </summary>
public class ExchangeRatesFactory
{
    private Hashtable flyweights = new Hashtable();

    public ExchangeRatesFactory()
    {
        flyweights.Add(CurrencyType.TWD, new ExchangeRates((decimal)1));
        flyweights.Add(CurrencyType.JPY, new ExchangeRates((decimal)0.273));
        flyweights.Add(CurrencyType.USD, new ExchangeRates((decimal)28.235));
        flyweights.Add(CurrencyType.EUR, new ExchangeRates((decimal)34.35));
    }

    public IExchangeRates GetConverter(CurrencyType currencyType)
    {
        return ((IExchangeRates)flyweights[currencyType]);
    }
}

轉換介面

/// <summary>
/// 轉換介面
/// </summary>
public interface IExchangeRates
{
    public decimal Converter(int amount);
}

傳換實作

/// <summary>
/// 傳換實作
/// </summary>
public class ExchangeRates : IExchangeRates
{
    private decimal _CashRate { get; }
    public ExchangeRates (decimal cashRate)
    {
        _CashRate = cashRate;
    }
    public decimal Converter(int amount)
    {
        var newAmount = amount / _CashRate;
        return decimal.Parse(newAmount.ToString("#0.00"));
    }
}
  1. 建立享元工廠factory
  2. 取得享元物件(匯率轉換)
  3. 透過享元物件計算金額
static void Main(string[] args)
{
    Situation.ExchangeRatesFactory factory = new Situation.ExchangeRatesFactory();
    int amount = 100;

    Situation.IExchangeRates converter_JPY = factory.GetConverter(Situation.CurrencyType.JPY);
    var jpy = converter_JPY.Converter(amount);

    Console.WriteLine($"TWD 轉換至 JPY:{amount} → {jpy}");

    Situation.IExchangeRates converter_USD = factory.GetConverter(Situation.CurrencyType.USD);
    var usd = converter_USD.Converter(amount);

    Console.WriteLine($"TWD 轉換至 USD:{amount} → {usd}");

    Situation.IExchangeRates converter_EUR = factory.GetConverter(Situation.CurrencyType.EUR);
    var eur = converter_EUR.Converter(amount);

    Console.WriteLine($"TWD 轉換至 EUR:{amount} → {eur}");

    Console.ReadLine();
}

執行結果

TWD 轉換至 JPY:100 → 366.30
TWD 轉換至 USD:100 → 3.54
TWD 轉換至 EUR:100 → 2.91

完整程式碼

GitHub:Structural_06_Flyweight


總結

Flyweight 屬於結構型,重點是重複使用而並非創建,所以創建的部分使用工廠來處理。 Flyweight 的物件是全部呼叫端共用,若是可修改內部狀態,則可能會造成 A 處理到一半時狀態被 B 修改,而造成 A 的結果與預期不同,這問題也許開發測試期間未必會發現,等到實際上線時才發現已經亂成一團了。


參考資料

  1. Design Patterns
  2. 大話設計模式
  3. dofactory
  4. Refactoring.Guru

新手上路,若有錯誤還請告知,謝謝


#designpattern #CSharp







Related Posts

前端工具 Gulp 與 Webpack 比較

前端工具 Gulp 與 Webpack 比較

Nengo AI 入門

Nengo AI 入門

將 ChatGPT LineBot 部屬在 Google Scripts 上

將 ChatGPT LineBot 部屬在 Google Scripts 上


Comments