10. Decorator


Posted by WayneCheng on 2021-01-17

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

1. 關於 Decorator

2. UML

3. 將 UML 轉為程式碼

4. 情境


測試環境:

OS:Windows 10
IDE:Visual Studio 2019


1. 關於 Decorator

Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

by Gang of Four

  • 動態的將職責附加至物件
  • Decorator 為子類別提供靈活的擴展

Decorator(裝飾者)屬於結構型(Structural Patterns),當遇到需要對物件附加一個以上的處理邏輯,可使用 Decorator 依據需求將原始物件依序處理後達到目的,且不需要另外處理時也可直接呼叫原始物件來使用。如同俄羅斯娃娃,有著相同的外觀(接口),只有大小不同(處理),依據需求取得所需大小即可。

優點:

  • 符合 開閉原則(Open Closed Principle)

缺點:

  • 違反 介面隔離原則(Interface Segregation Principle)

2. UML

Class 間關聯:

  • ConcreteComponent & Decorator 繼承 Component
  • ConcreteDecorator A / B 繼承 Decorator
  • Decorator 可包含 Component

Class:

  • Component:被裝飾物件的抽象類別
  • ConcreteComponent:實作被裝飾物件
  • Decorator:裝飾者抽象類別,繼承被裝飾者抽象類別
  • ConcreteDecorator:實作裝飾者,可附加至被裝飾物件的處理邏輯

3. 將 UML 轉為程式碼

被裝飾物件的抽象類別

/// <summary>
/// 被裝飾物件的抽象類別
/// </summary>
public abstract class Component
{
    public abstract void Operation();
}

實作被裝飾物件

/// <summary>
/// 實作被裝飾物件
/// </summary>
public class ConcreteComponent : Component
{
    public override void Operation()
    {
        Console.WriteLine("ConcreteComponent.Operation()");
    }
}

裝飾者抽象類別,繼承被裝飾者抽象類別

/// <summary>
/// 裝飾者抽象類別,繼承被裝飾者抽象類別
/// </summary>
public abstract class Decorator : Component
{
    protected Component component;

    public void SetComponent(Component component)
    {
        this.component = component;
    }

    public override void Operation()
    {
        if (component != null)
        {
            component.Operation();
        }
    }
}

實作裝飾者 A & B

/// <summary>
/// 實作裝飾者 A
/// </summary>
public class ConcreteDecoratorA : Decorator
{
    public override void Operation()
    {
        base.Operation();
        Console.WriteLine("ConcreteDecoratorA.Operation()");
    }
}

/// <summary>
/// 實作裝飾者 B
/// </summary>
public class ConcreteDecoratorB : Decorator
{
    public override void Operation()
    {
        base.Operation();
        AddedBehavior();
        Console.WriteLine("ConcreteDecoratorB.Operation()");
    }

    void AddedBehavior()
    {
        // Do something...
    }
}
  1. 建立被裝飾者component
  2. 建立裝飾者decorator
  3. 被裝飾者component套用裝飾者 A decoratorA
  4. 再套用裝飾者 B decoratorB
  5. 呼叫Operation()執行
static void Main(string[] args)
{
    Default.ConcreteComponent component = new Default.ConcreteComponent();
    Default.ConcreteDecoratorA decoratorA = new Default.ConcreteDecoratorA();
    Default.ConcreteDecoratorB decoratorB = new Default.ConcreteDecoratorB();

    decoratorA.SetComponent(component);
    decoratorB.SetComponent(decoratorA);

    decoratorB.Operation();

    Console.ReadLine();
}

執行結果

ConcreteComponent.Operation()
ConcreteDecoratorA.Operation()
ConcreteDecoratorB.Operation()

4. 情境

我們接到了一個要能配合行銷策略安排各種折扣(e.g. 周年慶全面九折、滿千折百無上限)的需求

  • 已知折扣 1. 周年慶全面九折 2. 滿千折百無上限,且未來可能會再擴充
  • 折扣要可以疊加

計算售價抽象類別

/// <summary>
/// 計算售價抽象類別
/// </summary>
public abstract class Calculation
{
    public abstract int Price();
}

計算售價實作

/// <summary>
/// 計算售價實作
/// </summary>
public class ConcreteCalculation : Calculation
{
    private int _Price { get; }

    public ConcreteCalculation(int price)
    {
        _Price = price;
    }

    public override int Price()
    {
        Console.WriteLine($"結帳金額(原價):{_Price}");

        return _Price;
    }
}

裝飾者抽象類別,計算售價抽象類別

/// <summary>
/// 裝飾者抽象類別,繼承被裝飾者抽象類別
/// </summary>
public abstract class Decorator : Calculation
{
    protected Calculation Calculation;

    public void SetComponent(Calculation calculation)
    {
        this.Calculation = calculation;
    }

    public override int Price()
    {
        return Calculation?.Price()?? 0;
    }
}

實作裝飾者,全面九折 & 滿千折百無上限

/// <summary>
/// 全面九折
/// </summary>
public class AllTenPercentOff : Decorator
{
    public override int Price()
    {
        var getPrice = base.Price();

        var newPrice = (int) (getPrice * 0.9);

        Console.WriteLine($"結帳金額(全面九折):{newPrice}");

        return newPrice;
    }
}

/// <summary>
/// 滿千折百無上限
/// </summary>
public class Every1000Get100CashBack : Decorator
{
    public override int Price()
    {
        var getPrice = base.Price();
        var discountTimes = (int)Math.Floor((decimal)(getPrice / 1000));
        var newPrice = getPrice - (100 * discountTimes);

        Console.WriteLine($"結帳金額(滿千折百無上限):{newPrice}");

        return newPrice;
    }
}

計算售價實作套用折扣裝飾者

  1. 建立(被裝飾者)計算金額calculation
  2. 建立(裝飾者)全面九折allTenPercentOff & 滿千送百every1000Get100CashBack
  3. 計算金額calculation套用全面九折allTenPercentOff
  4. 再套用滿千送百every1000Get100CashBack
  5. 呼叫Price()取得金額
static void Main(string[] args)
{
    Situation.ConcreteCalculation calculation = new Situation.ConcreteCalculation(5000);
    Situation.AllTenPercentOff allTenPercentOff = new Situation.AllTenPercentOff();
    Situation.Every1000Get100CashBack every1000Get100CashBack = new Situation.Every1000Get100CashBack();

    allTenPercentOff.SetComponent(calculation);
    every1000Get100CashBack.SetComponent(allTenPercentOff);

    every1000Get100CashBack.Price();

    Console.ReadLine();
}

執行結果

結帳金額(原價):5000
結帳金額(全面九折):4500
結帳金額(滿千折百無上限):4100

完整程式碼

GitHub:Structural_04_Decorator


總結

本篇沒有將 abstract class 改為 interface,主要是因為覺得 Decorator 使用繼承的方式能使子類別更加簡潔。雖然常聽到「用組合取代繼承」的建議,但繼承的使用若只是一兩階層而已,則不需要太過計較非使用組合或是繼承不可,依據使用情況選擇適合的即可。


參考資料

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

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


#designpattern #CSharp







Related Posts

HTML學習日記02

HTML學習日記02

AI輔導室|製作翻書效果

AI輔導室|製作翻書效果

COSCUP 2021 筆記

COSCUP 2021 筆記


Comments