Design Pattern (G4) - Decorator Pattern

有時候我們想要針對某個物件的實體加入一些額外的方法,一開始可能想說就直接修改類別或又寫一個繼承的類別。但這樣改到後面就失去原本物件的特性了。為了解決這個問題,你可以使用 Decorator Pattern,在保持原有物件的特性之下加入擴充。

目的

提供一個在保持原有物件特性的情況下,可動態加入擴充功能的解決方法。

動機

如一開始文章所提到的問題,舉一個遊戲的例子來說,遊戲中的人物角色可以穿著裝備而有各種數值的變化。現在有一個角色的類別和一個武器的類別,而武器類別可以有很多種而且武器的攻擊方式可能會有不同,例如近距離武器與遠距離武器。假設我需要角色拿上武器後就會有一個攻擊的方法,如果用繼承的方式,那麼有這麼多種武器組合不就要有多種繼承的組合,那彈性實在是很差。而且如果又加入了其他的防具那就不敢想像了。

使用時機


  1. 在需要保持原有物件特性的情況下,動態加入擴充功能的時候
  2. 可選擇特定類別的某些物件實體套用擴充功能



結構



在 Decorator Pattern 中,主要的角色為 Component 與 Decorator。Component 就是我們想要擴充的類別,而 Decorator 則為我們要附加的類別。原理很簡單,將 Decorator 繼承 Component,這樣對外而言 Decorator 就有 Component 的介面。為了與原有的類別進行區隔,Decorator 要包含一個 Component 的欄位,並在初始化的時候就傳進來,而 Decorator 繼承下來的那些 Component 特性就直接用傳進來的 Component 物件來回應。接著在加入要附加的功能或邏輯就好了。這樣子外面程式再呼叫時,這個 Decorator 就擁有原本物件的特性與附加的功能。

實作

限量用一個遊戲角色的範例進行實作並解說。大家玩過 RPG 遊戲都知道,遊戲角色可以透過穿著各種不同的裝備組合而使得能力值產生變化。穿著好的裝備能力值就會變高,反之變低。另外,裝備不同武器也有不同的攻擊模式,例如裝備弓箭變成遠距離攻擊,裝備劍變成近距離攻擊。像這種情況就很適合用 Decorator Pattern 來說明,因為角色穿上裝備後不會變成一個新物種,只是單單加了一些功能或能力值的變化,這就符合我們的使用時機。以下為實作的 Class Diagram:



在 Class Diagram 中,有幾個類別。ISprite為角色介面對應 Decorator Pattern 的 Component,它定義了 Name(角色名稱), HP(血量), MP(魔力量), SP(技能點數), ATK(攻擊數值), DEF(防禦數值)...等;SpriteBase 為實作 ISprite 的抽象類別;ElfSprite(妖精種族)繼承SpriteBase,對應 Decorator Pattern 的 ConcreteComponent,提供 Default 的靜態方法,主要是獲得一個預設數值的妖精種族角色實體;HumanitySprite(人類種族)也是繼承SpriteBase,也有提供 Default 的靜態方法;WearableBase 為各種裝備的 Base Class,為 Decorator,Constructor 需傳入 ISprite 的物件,它繼承了 SpriteBase,其 SpriteBase 的特性皆是藉由傳入的 ISprite 物件回傳;再來裝備又可分為 WeaponBase(武器)與 AarmorBase(防具)。武器有 Attack(攻擊)指令,而防具有 Defence(防禦)指令。其下有各式各樣的武器類型與防具類型繼承實作。以下為實作的程式碼:

ISprite.cs

public interface ISprite
{
    /// <summary>
    /// 名稱
    /// </summary>
    string Name { get; }

    /// <summary>
    /// 體力
    /// </summary>
    int HP { get; }

    /// <summary>
    /// 魔力
    /// </summary>
    int MP { get; }

    /// <summary>
    /// 技能點數
    /// </summary>
    int SP { get; }

    /// <summary>
    /// 攻擊
    /// </summary>
    int ATK { get; }

    /// <summary>
    /// 防禦
    /// </summary>
    int DEF { get; }

    /// <summary>
    /// 敏捷
    /// </summary>
    int DEX { get; }

    /// <summary>
    /// 迴避
    /// </summary>
    int AGI { get; }

    /// <summary>
    /// 智力
    /// </summary>
    int INT { get; }

    /// <summary>
    /// 幸運
    /// </summary>
    int LUK { get; }
}

SpriteBase.cs
public abstract class SpriteBase : ISprite
{
    public virtual string Name { get; set; }

    public virtual int HP { get; set; }

    public virtual int MP { get; set; }

    public virtual int SP { get; set; }

    public virtual int ATK { get; set; }

    public virtual int DEF { get; set; }

    public virtual int DEX { get; set; }

    public virtual int AGI { get; set; }

    public virtual int INT { get; set; }

    public virtual int LUK { get; set; }

    public override string ToString()
    {
        var sb = new StringBuilder();

        sb.AppendLine($"*** {Name} ***");
        sb.AppendLine($"HP: {HP}");
        sb.AppendLine($"MP: {MP}");
        sb.AppendLine($"SP: {SP}");
        sb.AppendLine($"ATK: {ATK}");
        sb.AppendLine($"DEF: {DEF}");
        sb.AppendLine($"DEX: {DEX}");
        sb.AppendLine($"AGI: {AGI}");
        sb.AppendLine($"INT: {INT}");
        sb.AppendLine($"LUK: {LUK}");

        return sb.ToString();
    }
}

ElfSprite.cs

/// <summary>
/// 妖精
/// </summary>
public class ElfSprite : SpriteBase
{
    public static ElfSprite Default(string name) =$gt;
        new ElfSprite
        {
            Name = name,
            HP = 300,
            MP = 700,
            SP = 0,
            ATK = 15,
            DEF = 15,
            DEX = 25,
            AGI = 20,
            INT = 15,
            LUK = 10
        };
}

HumanitySprite.cs

/// <summary>
/// 人族
/// </summary>
public class HumanitySprite : SpriteBase
{
    public static HumanitySprite Default(string name) =$gt;
        new HumanitySprite
        {
            Name = name,
            HP = 500,
            MP = 500,
            SP = 0,
            ATK = 15,
            DEF = 15,
            DEX = 15,
            AGI = 15,
            INT = 25,
            LUK = 15
        };
}

WearableBase.cs

/// <summary>
/// 裝備
/// </summary>
public abstract class WearableBase : SpriteBase
{
    public WearableBase(ISprite sprite)
    {
        Sprite = sprite;
    }

    protected ISprite Sprite { get; set; }

    public override string Name => Sprite.Name;

    public override int HP => Sprite.HP;

    public override int MP => Sprite.MP;

    public override int SP => Sprite.SP;

    public override int ATK => Sprite.ATK;

    public override int DEF => Sprite.DEF;

    public override int DEX => Sprite.DEX;

    public override int AGI => Sprite.AGI;

    public override int INT => Sprite.INT;

    public override int LUK => Sprite.LUK;
}


WeaponBase.cs

/// <summary>
/// 武器
/// </summary>
public abstract class WeaponBase : WearableBase
{
    public WeaponBase(ISprite sprite) : base(sprite) { }

    /// <summary>
	 /// 攻擊指令
	 /// </summary>
    public abstract void Attack();
}


SwordWeapon.cs

/// <summary>
/// 劍
/// </summary>
public class SwordWeapon : WeaponBase
{
    public SwordWeapon(ISprite sprite) : base(sprite) { }

    public override int ATK => base.ATK + 100;

    public override void Attack()
    {
        Console.WriteLine($"{Name} slash!  slash!  slash!");
    }
}


BowWeapon.cs

/// <summary>
/// 弓箭
/// </summary>
public class BowWeapon : WeaponBase
{
    public BowWeapon(ISprite sprite) : base(sprite) { }

    public override int ATK => base.ATK + 50;

    public override int DEX => base.DEX + 50;

    public override void Attack()
    {
        Console.WriteLine($"{Name} shot! shot! shot!");
    }
}


ArmorBase.cs
/// <summary>
/// 防具
/// </summary>
public abstract class ArmorBase : WearableBase
{
    public ArmorBase(ISprite sprite) : base(sprite) { }

    /// <summary>
	 /// 防禦指令
	 /// </summary>
    public abstract void Defence();
}


HelmetArmor.cs

/// <summary>
/// 頭盔
/// </summary>
public class HelmetArmor : ArmorBase
{
    public HelmetArmor(ISprite sprite) : base(sprite) { }

    public override int DEF => base.DEF + 25;

    public override void Defence()
    {
        Console.WriteLine("Reflect");
    }
}

Program.cs
class Program
{
    static void Main(string[] args)
    {
        var human = HumanitySprite.Default("Limited");
        var elf = ElfSprite.Default("Angel");

        Console.WriteLine("===== Before =====");
        Console.WriteLine(human.ToString());
        Console.WriteLine("------------------");
        Console.WriteLine(elf.ToString());

        var humanEx = human.WearArmor<HelmetArmor>().HoldWeapon<SwordWeapon>();
        var elfEx = elf.WearArmor<HelmetArmor>().HoldWeapon<BowWeapon>();

        Console.WriteLine("===== After =====");
        Console.WriteLine(humanEx.ToString());
        Console.WriteLine("------------------");
        Console.WriteLine(elfEx.ToString());

        Console.WriteLine("===== Attack =====");
        ((WeaponBase)humanEx).Attack();
        ((WeaponBase)elfEx).Attack();

        Console.ReadLine();

        /* 執行結果
        ===== Before =====
        ***Limited * **
        HP: 500
        MP: 500
        SP: 0
        ATK: 15
        DEF: 15
        DEX: 15
        AGI: 15
        INT: 25
        LUK: 15

        ------------------
        * **Angel * **
        HP: 300
        MP: 700
        SP: 0
        ATK: 15
        DEF: 15
        DEX: 25
        AGI: 20
        INT: 15
        LUK: 10

        ===== After =====
        ***Limited * **
        HP: 500
        MP: 500
        SP: 0
        ATK: 115
        DEF: 40
        DEX: 15
        AGI: 15
        INT: 25
        LUK: 15

        ------------------
        * **Angel * **
        HP: 300
        MP: 700
        SP: 0
        ATK: 65
        DEF: 40
        DEX: 75
        AGI: 20
        INT: 15
        LUK: 10

        ===== Attack =====
        Limited slash!slash!slash!
        Angel shot!shot!shot!
        */
    }
}

static class SpriteExtension
{
 public static ISprite WearArmor<T>(this ISprite sprite) where T : ArmorBase
  => Activator.CreateInstance(typeof(T), sprite) as ISprite;

 public static ISprite HoldWeapon<T>(this ISprite sprite) where T : WeaponBase
  => Activator.CreateInstance(typeof(T), sprite) as ISprite;
}

主程式執行時會產生一個人族的初心者 Limited 與實體與妖精的初心者 Angel 實體,接著利用 Decorator Pattern,Limited 裝上頭盔與劍,Angel 裝上頭盔與弓箭。印出結果可以看到 Limited 的 ATK 增加了 100,這 100 的來源來自劍。而 DEF 增加了 25 來自於頭盔。Angel 也是一樣,ATK 增加了 50,DEX 增加了 50,來自於弓箭。而 DEF 增加 25 來自於頭盔。接著呼叫攻擊指令可以發現 Limited 用砍擊,而 Angel 是用射擊的,可以看的出來各武器攻擊方式的差異。

優點

  1. 比直接繼承更有彈性。
  2. 在 Decorator 可以清楚的看到附加的邏輯與本身物件的區隔。
  3. 任何小型的擴充皆適合使用 Decorator。

缺點

因為繼承 Component 的介面,而使得無異動的方法或屬性必須實作以原物件回應,如果無異動的部分很多,就會變得很長。相較之下,一般繼承就不需要重新實作無異動部分。

相關樣式

  1. Adapter
  2. Composite
  3. Strategy

使用頻率









參考來源:

DZone - Is Inheritance Dead?
Design Patterns - Gang of Four



留言