本文
我們在先前 SOLID - OCP(開放封閉原則) 有提到過,軟體設計原則,應該對擴展開放,對修改封閉。
而這次我們要介紹的裝飾者模式能夠更優雅地做到這件事。
首先我們先回憶一下,特斯拉(不要懷疑我又回來啦)在不違反 OCP 原則下安裝軟體的時候我們可以怎麼做。
internal class Program
{
static void Main(string[] args)
{
var tesla = new Tesla("Model3");
tesla.Print();
var autoPilot= new AutoPilot();
var settingRing = new SettingRing();
autoPilot.AddSoftWare();
settingRing.AddSoftWare();
}
}
public class Tesla
{
private string _model;
public Tesla(string model)
{
this._model = model;
}
public void Print()
{
Console.WriteLine(this._model);
}
}
public abstract class SoftWare
{
public abstract void AddSoftWare();
}
public class AutoPilot : SoftWare
{
public override void AddSoftWare()
{
Console.WriteLine("現在有自動導航的功能了!");
}
}
public class SettingRing : SoftWare
{
public override void AddSoftWare()
{
Console.WriteLine("現在有自訂喇叭鈴聲的功能了!");
}
}
結果:
現在程式的架構圖:
當有第三個軟體功能要更新的時候就只需要繼承 Software 增加一個子類別實作就可以了。
這種情況下出現的問題是當我們要安裝很多個軟體時,就會開始增加 AddSoftWare。
理想的情況下安裝軟體這件事應該在內部軟體自行升級後再顯示出結果即可,而要如何將我們所需的功能按照正確的順序串聯起來,這時候就可以使用裝飾者模式。
我們現在的順序是,先安裝自動導航功能再安裝自訂鈴聲功能。
internal class Program
{
static void Main(string[] args)
{
var tesla = new Model3();
//先安裝自動導航功能
Decorator teslaWithAutopilot = new AutoPilot(tesla);
//再安裝自訂鈴聲功能
Decorator teslaWithSettingRing = new SettingRing(teslaWithAutopilot);
teslaWithSettingRing.Print();
}
}
/// <summary>
/// Tesla抽象類別
/// </summary>
public abstract class Tesla
{
public abstract void Print();
}
/// <summary>
/// Model3
/// </summary>
public class Model3 : Tesla
{
/// <summary>
/// 重寫基類方法
/// </summary>
public override void Print()
{
Console.WriteLine("Model3");
}
}
/// <summary>
/// 裝飾抽象類
/// </summary>
public abstract class Decorator : Tesla
{
private Tesla _tesla;
public Decorator(Tesla tesla)
{
this._tesla = tesla;
}
public override void Print()
{
if (this._tesla != null)
{
this._tesla.Print();
}
}
}
/// <summary>
/// 新增自動駕駛功能
/// </summary>
public class AutoPilot : Decorator
{
public AutoPilot(Tesla tesla)
: base(tesla)
{
}
public override void Print()
{
base.Print();
// 新增新功能
AddAutoPilot();
}
/// <summary>
/// 新的行為方法
/// </summary>
public void AddAutoPilot()
{
Console.WriteLine("現在有自動駕駛功能了!");
}
}
/// <summary>
/// 新增自動駕駛功能
/// </summary>
public class SettingRing : Decorator
{
public SettingRing(Tesla tesla)
: base(tesla)
{
}
public override void Print()
{
base.Print();
// 新增新功能
AddRing();
}
/// <summary>
/// 新的行為方法
/// </summary>
public void AddRing()
{
Console.WriteLine("現在有自訂喇叭鈴聲的功能了!");
}
}
結果:
程式的架構圖變成:
說明:
第1步、第一個 Tesla 是我們定義出來做為基底的抽象類別。
第2步、我們的 Model3 類別繼承 Tesla 後自行定義了一個具體的物件。
第3步、我們的 Decorator 裝飾者抽象類別也繼承了 Tesla,最主要的功用是能讓其他類別透過 Decorator 來擴展原本 Tesla 的功能。
第4步、接下來就是 AutoPilot 以及 SettingRing 透過 Decorator 分別對 Tesla 增加了不同的功能。
小提醒:
上述也提到了 Decorator 是有裝飾的順序的,所以我們的順序是 Tesla 透過 AutoPilot 裝飾後,再將 teslaWithAutopilot 放入 SettingRing 進行裝飾。
這樣最大的優點在於,每個裝飾物件只需專注於關心自己添加的功能,不需要關心自己是如何被添加到整個物件關聯內的,當裝飾物件不需要時也能輕易的移除該功能。
接下來可以觀察到我們的 Model3 這個類別其實是可以直接整合進 Tesla 這個基底內的。
internal class Program
{
static void Main(string[] args)
{
var tesla = new Tesla("Model3");
var autoPilot = new AutoPilot();
var settingRing = new SettingRing();
//先安裝自動導航功能
autoPilot.Decorate(tesla);
//再安裝自訂鈴聲功能
settingRing.Decorate(autoPilot);
settingRing.Print();
}
}
/// <summary>
/// Tesla抽象類別
/// </summary>
public class Tesla
{
public Tesla()
{
}
private string _model;
public Tesla(string model)
{
this._model = model;
}
public virtual void Print()
{
Console.WriteLine(this._model);
}
}
/// <summary>
/// 裝飾抽象類
/// </summary>
public class Decorator : Tesla
{
protected Tesla _tesla;
public void Decorate(Tesla tesla)
{
this._tesla = tesla;
}
public override void Print()
{
if (this._tesla != null)
{
this._tesla.Print();
}
}
}
/// <summary>
/// 新增自動駕駛功能
/// </summary>
public class AutoPilot : Decorator
{
public override void Print()
{
base.Print();
Console.WriteLine("現在有自動駕駛功能了!");
}
}
/// <summary>
/// 新增自動駕駛功能
/// </summary>
public class SettingRing : Decorator
{
public override void Print()
{
base.Print();
Console.WriteLine("現在有自訂喇叭鈴聲的功能了!");
}
}
結果:
程式架構圖變成:
總結
優點:
- 有效地把核心職責和裝飾功能區分開來。
- 裝飾者能夠更靈活的擴充物件功能。
- 通過裝飾的順序不同我們可以產生不同的行為模式。
缺點:
- 當使用過度程式碼會變得很複雜,不易維護。
適用場景:
- 快取有多層的時候可以用到,Ex:第一層記憶體快取、第二層 Redis 快取、第三層直接到 DB 拿資料。
當你這次新加的需求功能不知道甚麼時候又會被反悔的時候可以用到。
後記
會開始想要了解裝飾者模式主要是看到了公司大神、前輩再進行快取擴充的時候用到這個設計模式,想要了解他們是怎麼做的就只好先從裝飾者模式開始學習。
後來理解怎麼使用後,同事也拿來做為某些功能替代方案所使用Ex:某個服務因為離線導致我們取不到資料,那如果我們知道源頭 DB 再哪就可以透過裝飾者模式做一層保護層出來,當服務取不到資料時就直接進DB拿取資料。
這邊學習後也有種體悟,了解或是知道怎麼使用設計模式是一回事、而當自己在寫程式的時候會不會想到可以使用設計模式又是另外一回事,我想這應該只能靠經驗的累積以及觀看前人程式碼後抽絲剝繭去思考為甚麼他們會這樣寫才能增加自己的經驗值了;再次讚嘆前輩、大神留下的程式碼。