4. Builder


Posted by WayneCheng on 2021-01-11

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

1. 關於 Builder

2. UML

3. 將 UML 轉為程式碼

4. 情境


測試環境:

OS:Windows 10
IDE:Visual Studio 2019


1. 關於 Builder

Separate the construction of a complex object from its representation so that the same construction process can create different representations.

by Gang of Four

  • 將復雜物件的傳入參數由建構子中抽離,以便相同的建構子可以依據不同的傳入參數而有不同樣貌

Builder(建造者)屬於創建型(Creational Patterns),當遇到初始化步驟複雜的類別時,使用 Builder 來包裝初始化邏輯,呼叫端依據需求選擇相對應的 Builder 實作,以減低使用的複雜度,當遇到需要於傳入參數加上約束條件時,可使用 Builder 來處理相關邏輯。

優點:

  • 符合 單一職責原則(Single Responsibility Principle)
  • 可簡化傳入參數過多問題,並將複雜邏輯抽離

缺點:

  • 會增加許多 class 造成程式複雜度增加

2. UML

Class 間關聯:

  • Director 可包含 Builder
  • ConcreteBuilder 繼承 Builder
  • ConcreteBuilder 依賴 Product

Class:

  • Director:定義建造的流程細節
  • Builder:建造者的抽象類別或介面
  • ConcreteBuilder:建造者的實作,用來取得建構子中的複雜參數 & 構造Product
  • Product:正在構造的複雜物件

3. 將 UML 轉為程式碼

正在構造的複雜物件

/// <summary>
/// 正在構造的複雜物件
/// </summary>
public class Product
{
    private List<string> _parts = new List<string>();

    public void Add(string part)
    {
        _parts.Add(part);
    }

    public void Show()
    {
        Console.WriteLine("\nProduct Parts -------");

        foreach (string part in _parts)
        {
            Console.WriteLine(part);
        }
    }
}

建造者方法的介面

/// <summary>
/// 建造者方法的介面
/// </summary>
public interface IBuilder
{
    void BuildPartA();
    void BuildPartB();
    Product GetResult();
}

建造者方法的實作

/// <summary>
/// 建造者方法的實作 1
/// </summary>
public class ConcreteBuilder1 : IBuilder
{
    private Product _product = new Product();

    public void BuildPartA()
    {
        _product.Add("PartA1");
    }

    public void BuildPartB()
    {
        _product.Add("PartB1");
    }

    public Product GetResult()
    {
        return _product;
    }
}

/// <summary>
/// 建造者方法的實作 2
/// </summary>
public class ConcreteBuilder2 : IBuilder
{
    private Product _product = new Product();

    public void BuildPartA()
    {
        _product.Add("PartA2");
    }

    public void BuildPartB()
    {
        _product.Add("PartB2");
    }

    public Product GetResult()
    {
        return _product;
    }
}

定義建造的流程細節

/// <summary>
/// 定義建造的流程細節
/// </summary>
public class Director
{
    public void Construct(IBuilder builder)
    {
        builder.BuildPartA();
        builder.BuildPartB();
    }
}
  1. 建立director
  2. 建立builder並傳入director中,透過director定義構造流程
  3. 透過builder建立product
static void Main(string[] args)
{
    Default.Director director = new Default.Director();

    var builder1 = new Default.ConcreteBuilder1();
    var builder2 = new Default.ConcreteBuilder2();

    director.Construct(builder1);
    var product1 = builder1.GetResult();
    product1.Show();

    director.Construct(builder2);
    var product2 = builder2.GetResult();
    product2.Show();

    Console.ReadLine();
}

執行結果

Product Parts -------
PartA1
PartB1

Product Parts -------
PartA2
PartB2

4. 情境

我們接到了一個付款的需求

  • 需要能支援現有的兩種付款方式(現金、ApplePay)
  • 要依據付款方式提供折扣
  • 為了因應周年慶,還需要依據付款方式加上贈品、紅利點數功能
  • 且未來可能會有更多不同的付款方式

處理折扣、贈品、紅利點數等複雜動作

/// <summary>
/// 正在構造的複雜物件
/// </summary>
public class Product
{
    private List<string> _parts = new List<string>();

    private double _discount { get; set; }

    public void SetDiscount(double discount)
    {
        _discount = discount;
    }

    public void Add(string part)
    {
        _parts.Add(part);
    }

    public void Pay(int amount)
    {
        Console.WriteLine("\n-------");

        foreach (string part in _parts)
        {
            Console.WriteLine(part);
        }

        Console.WriteLine($"應收金額:{(int)(amount * _discount)} 元");
    }
}

建造者方法的介面

/// <summary>
/// 建造者方法的介面
/// </summary>
public interface IBuilder
{
    // 折扣
    void Discount();
    // 紅利點數
    void RewardPoints();
    // 贈品
    void Giveaway();

    Product GetPayment();
}

依據現金 & ApplePay 實作折扣、贈品、紅利點數

/// <summary>
/// 建造者方法的現金實作
/// </summary>
public class CashBuilder : IBuilder
{
    private Product _product = new Product();

    public void Discount()
    {
        _product.Add("Cash 不打折");
        _product.SetDiscount(1);
    }

    public void RewardPoints()
    {
        _product.Add("Cash 集點");
    }

    public void Giveaway()
    {
        _product.Add("Cash 送馬克杯");
    }

    public Product GetPayment()
    {
        return _product;
    }
}

/// <summary>
/// 建造者方法的 ApplePay 實作
/// </summary>
public class ApplePayBuilder : IBuilder
{
    private Product _product = new Product();

    public void Discount()
    {
        _product.Add("ApplePay 九折");
        _product.SetDiscount(0.9);
    }

    public void RewardPoints()
    {
        _product.Add("ApplePay 集點");
    }

    public void Giveaway()
    {
        _product.Add("ApplePay 無贈品");
    }

    public Product GetPayment()
    {
        return _product;
    }
}

定義建造的流程細節

/// <summary>
/// 定義建造的流程細節
/// </summary>
public class Director
{
    public void Construct(IBuilder builder)
    {
        // 加入折扣
        builder.Discount();
        // 加入紅利點數
        builder.RewardPoints();
        // 加入贈品
        builder.Giveaway();
    }
}
  1. 建立director
  2. 建立現金 & ApplePay builder並傳入director中,透過director定義構造流程
  3. 透過builder建立現金 & ApplePay Payment
static void Main(string[] args)
{
    Situation.Director director = new Situation.Director();

    var cashBuilder = new Situation.CashBuilder();
    var applePayBuilder = new Situation.ApplePayBuilder();

    director.Construct(cashBuilder);
    var cash = cashBuilder.GetPayment();
    cash.Pay(100);

    director.Construct(applePayBuilder);
    var applePay = applePayBuilder.GetPayment();
    applePay.Pay(100);

    Console.ReadLine();
}

執行結果

-------
Cash 不打折
Cash 集點
Cash 送馬克杯
應收金額:100 元

-------
ApplePay 九折
ApplePay 集點
ApplePay 無贈品
應收金額:90 元

完整程式碼

GitHub:Creational_03_Builder


總結

由於語言特性的關係,C# 可以使用多載的方式減少 Constructor 參數,在情境較為複雜的情況下可以使用 Builder,而較為單純的情況時則可使用多載的方式處理。


參考資料

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

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


#designpattern #CSharp







Related Posts

[python] Keyword-Only Arguments 的使用時機

[python] Keyword-Only Arguments 的使用時機

原子習慣:書本導讀 - 簡介與初心

原子習慣:書本導讀 - 簡介與初心

D51_W6 直播檢討、W7 開始

D51_W6 直播檢討、W7 開始


Comments