有時候我們想要針對某個物件的實體加入一些額外的方法,一開始可能想說就直接修改類別或又寫一個繼承的類別。但這樣改到後面就失去原本物件的特性了。為了解決這個問題,你可以使用 Decorator Pattern,在保持原有物件的特性之下加入擴充。
在 Decorator Pattern 中,主要的角色為 Component 與 Decorator。Component 就是我們想要擴充的類別,而 Decorator 則為我們要附加的類別。原理很簡單,將 Decorator 繼承 Component,這樣對外而言 Decorator 就有 Component 的介面。為了與原有的類別進行區隔,Decorator 要包含一個 Component 的欄位,並在初始化的時候就傳進來,而 Decorator 繼承下來的那些 Component 特性就直接用傳進來的 Component 物件來回應。接著在加入要附加的功能或邏輯就好了。這樣子外面程式再呼叫時,這個 Decorator 就擁有原本物件的特性與附加的功能。
SpriteBase.cs
ElfSprite.cs
HumanitySprite.cs
WearableBase.cs
WeaponBase.cs
SwordWeapon.cs
BowWeapon.cs
ArmorBase.cs
HelmetArmor.cs
Program.cs
主程式執行時會產生一個人族的初心者 Limited 與實體與妖精的初心者 Angel 實體,接著利用 Decorator Pattern,Limited 裝上頭盔與劍,Angel 裝上頭盔與弓箭。印出結果可以看到 Limited 的 ATK 增加了 100,這 100 的來源來自劍。而 DEF 增加了 25 來自於頭盔。Angel 也是一樣,ATK 增加了 50,DEX 增加了 50,來自於弓箭。而 DEF 增加 25 來自於頭盔。接著呼叫攻擊指令可以發現 Limited 用砍擊,而 Angel 是用射擊的,可以看的出來各武器攻擊方式的差異。
參考來源:
DZone - Is Inheritance Dead?
Design Patterns - Gang of Four
目的
提供一個在保持原有物件特性的情況下,可動態加入擴充功能的解決方法。動機
如一開始文章所提到的問題,舉一個遊戲的例子來說,遊戲中的人物角色可以穿著裝備而有各種數值的變化。現在有一個角色的類別和一個武器的類別,而武器類別可以有很多種而且武器的攻擊方式可能會有不同,例如近距離武器與遠距離武器。假設我需要角色拿上武器後就會有一個攻擊的方法,如果用繼承的方式,那麼有這麼多種武器組合不就要有多種繼承的組合,那彈性實在是很差。而且如果又加入了其他的防具那就不敢想像了。使用時機
- 在需要保持原有物件特性的情況下,動態加入擴充功能的時候
- 可選擇特定類別的某些物件實體套用擴充功能
結構
在 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
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 是用射擊的,可以看的出來各武器攻擊方式的差異。
優點
- 比直接繼承更有彈性。
- 在 Decorator 可以清楚的看到附加的邏輯與本身物件的區隔。
- 任何小型的擴充皆適合使用 Decorator。
缺點
因為繼承 Component 的介面,而使得無異動的方法或屬性必須實作以原物件回應,如果無異動的部分很多,就會變得很長。相較之下,一般繼承就不需要重新實作無異動部分。
相關樣式
- Adapter
- Composite
- Strategy
使用頻率
參考來源:
DZone - Is Inheritance Dead?
Design Patterns - Gang of Four
留言
張貼留言