--- tags: 設計模式 --- # 外觀模式(Facade Pattern) ## 前言 - 當原本的介面太難用了, 我們可以重新定義一個介面, 供外部使用 - Facade 滿足最小知識原則. - Facade 的重點在 "提供一個簡單易使用的公開接口介面" #### 問題需求 假設現實生活中, 你需要實作一個按鈕讓使用者(們)可以透過微波爐微波食物. 此時你可能需要使用到三個模組的部分功能, 並且依照**正確的順序**使用它們, 否則微波爐會爆炸XD 1. 啟動電源(電源供應模組) 1. 加熱食物(加熱模組) 1. 冷卻機器(冷卻模組) ```mermaid graph TB; 美女 宅男 小屁孩 其他沒用到微波爐的使用者 美女--使用-->電源供應模組 美女--使用-->加熱模組 美女--使用-->冷卻模組 宅男--使用-->電源供應模組 宅男--使用-->加熱模組 宅男--使用-->冷卻模組 小屁孩--使用-->電源供應模組 小屁孩--使用-->加熱模組 小屁孩--使用-->冷卻模組 subgraph 電源供應模組 加熱模組 冷卻模組 其他跟微波爐無關的模組 end ``` ##### 可能實作程式碼 ```C# public class 美女{ public void 美女操作微波爐(){ var 電源 = new 電源供應模組(); 電源.供電(); new 加熱模組().加熱(); new 冷卻模組().冷卻(); 電源.斷電(); } public void 美女化妝() => throw new NotImplementedException(); } public class 宅男{ public void 宅男操作微波爐(){ var 電源 = new 電源供應模組(); 電源.供電(); new 加熱模組().加熱(); new 冷卻模組().冷卻(); 電源.斷電(); } public void 宅男攤在沙發上看電視() => throw new NotImplementedException(); } public class 小屁孩{ public void 小屁孩操作微波爐(){ var 電源 = new 電源供應模組(); 電源.供電(); new 加熱模組().加熱(); new 冷卻模組().冷卻(); 電源.斷電(); } public void 小屁孩玩耍() => throw new NotImplementedException(); } ``` ##### 問題 - 使用者(們)必須認識電源供應模組、加熱模組、冷卻模組, 並且知道這三個模組的正確使用順序, 才能夠正確使用微波爐的功能. - 使用者(們)直接依賴三個模組, 增加日後修改的困難. 比如說 : 若日後需要抽換加熱模組, 就必須修改每一個加熱模組被使用的地方, (e.g. 將加熱模組換成美國製造的加熱模組) ```C# // 將 new 加熱模組().加熱(); // 換成 new 加熱模組_美國製造().加熱(); ``` ## Facade 介紹 #### 定義 > Facade is a structural design pattern that provides a simplified interface to a library, a framework, or any other complex set of classes. > Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use. > * Applicability > * Need to provide a simple interface to a complex system > * Need to decouple a subsystem from its clients > * Need to provide an interface to a software layer > * Consequences > * Shields clients from subsystem components > * Promotes weak coupling between the subsystem and its clients > * Facade doesn't prevent clients from using subsystem classes if they need to - 為複雜的子系統定義一組統一的介面/接口/Facade , 使用者可透過此介面/接口/Facade 更容易操作子系統 - 子系統可以表示為一個複雜的類別、一個模組(包含多個類別)等等. #### 子系統定義 - 真正功能被實作處 - 不會和 Facade 有循環參考的問題 - 子系統用到 Facade, Facade 又用到子系統 - 通常已經存在且有一定程度的穩定性 #### Facade 定義 - 介於 Client 和子系統之間, 以作為 Client 與子系統之間的溝通橋樑 - 知道子系統的公開介面 - 通常是類別, 有時候也可能是介面或抽象類別(當對外的 Facade 需要抽象的時候, 使用). #### Structure Illustrate ![Facadestructure.png](https://github.com/s0920832252/C_Sharp/blob/master/Files/DesignPattern/Facadestructure.png?raw=true) ```mermaid graph LR; Client--使用-->統一介面/接口/Facade 統一介面/接口/Facade -- 使用 --> 類別1 統一介面/接口/Facade -- 使用 --> 類別2 統一介面/接口/Facade -- 使用 --> 類別3 subgraph 複雜子系統 類別1 類別2 類別3 沒被使用到的類別... end ``` ## Code Example ### 複雜模組, 透過 Facade 簡化操作 ```C# public class 電源供應模組 { public void 供電() => throw new NotImplementedException(); public void 斷電() => throw new NotImplementedException(); } public class 加熱模組 { public void 加熱() => throw new NotImplementedException(); } public class 冷卻模組 { public void 冷卻() => throw new NotImplementedException(); } // Facade public class 微波爐 { private readonly 電源供應模組 _電源供應模組 = new 電源供應模組(); private readonly 加熱模組 _加熱模組 = new 加熱模組(); private readonly 冷卻模組 _冷卻模組 = new 冷卻模組(); public void 微波() { _電源供應模組.供電(); _加熱模組.加熱(); _冷卻模組.冷卻(); _電源供應模組.斷電(); } } public class 美女 { private readonly 微波爐 _微波爐 = new 微波爐(); public void 操作微波爐() => _微波爐.微波(); public void 美女化妝() => throw new NotImplementedException(); } public class 宅男{ private readonly 微波爐 _微波爐 = new 微波爐(); public void 操作微波爐() => _微波爐.微波(); public void 宅男攤在沙發上看電視() => throw new NotImplementedException(); } public class 小屁孩{ private readonly 微波爐 _微波爐 = new 微波爐(); public void 操作微波爐() => _微波爐.微波(); public void 小屁孩玩耍() => throw new NotImplementedException(); } ``` - Client 依賴於微波爐, 而非依賴於電源供應模組、加熱模組、冷卻模組, Client 類別並不需要認知到這三個模組, 更不需要知道如何使用正確這些模組的邏輯 - 未來需要抽換電源供應模組、加熱模組、冷卻模組, 僅需要修改微波爐類別, 而不需要修改 Client 類別. ### 複雜類別, 透過 Facade 簡化操作 ```C# // God Class : 職責過多的類別. public class GodClass { public void DoSomeThing1() => throw new NotImplementedException(); public void DoSomeThing2() => throw new NotImplementedException(); public void DoSomeThing3() => throw new NotImplementedException(); public void DoSomeThing4() => throw new NotImplementedException(); public void DoSomeThing5() => throw new NotImplementedException(); public void DoSomeThing6() => throw new NotImplementedException(); public void DoSomeThing7() => throw new NotImplementedException(); public void DoSomeThing8() => throw new NotImplementedException(); public void DoSomeThing9() => throw new NotImplementedException(); public void DoSomeThing10() => throw new NotImplementedException(); // 以下略 } // 實務上, 只需要使用到 God Class 的其中兩個方法. public class Facade1 { private readonly GodClass _godClass = new GodClass(); public void DoSomeThing1() => _godClass.DoSomeThing1(); public void DoSomeThing2() => _godClass.DoSomeThing2(); } // 實務上, 只需要使用到 God Class 的其中三個方法. public class Facade2 { private readonly GodClass _godClass = new GodClass(); public void DoSomeThing3() => _godClass.DoSomeThing3(); public void DoSomeThing4() => _godClass.DoSomeThing4(); public void DoSomeThing5() => _godClass.DoSomeThing5(); } ``` - 有時候我們沒辦法修改 God Class 的程式碼, 但 God Class 的使用又太過於複雜. 這時候可以為其建立一些 Facade, 讓 Client 端使用. 日後若 God Class 被重構, 也僅需要修改 Facade, 而不需要修改 Client 端. - 改變客戶端所依賴的類別, 讓其不再依賴於 God Class, 方便日後重構. ### Additional Facade ```C# public class SubClass { public void DoSomeThing1() => throw new NotImplementedException(); // 以下略 } public class SubClass2 { public void DoSomeThing2() => throw new NotImplementedException(); // 以下略 } public class SubClass3 { public void DoSomeThing3() => throw new NotImplementedException(); // 以下略 } public class SubClass4 { public void DoSomeThing4() => throw new NotImplementedException(); // 以下略 } public class SubClass5 { public void DoSomeThing5() => throw new NotImplementedException(); // 以下略 } public class SubClass6 { public void DoSomeThing6() => throw new NotImplementedException(); // 以下略 } public class SubClass6 { public void DoSomeThing6() => throw new NotImplementedException(); // 以下略 } public class SubClass7 { public void DoSomeThing7() => throw new NotImplementedException(); // 以下略 } public class SubClass8 { public void DoSomeThing8() => throw new NotImplementedException(); // 以下略 } public class SubClass9 { public void DoSomeThing9() => throw new NotImplementedException(); // 以下略 } ``` ##### GodFacade ```C# public class GodFacade { private readonly SubClass _subClass; private readonly SubClass2 _subClass2; private readonly SubClass3 _subClass3; private readonly SubClass4 _subClass4; private readonly SubClass5 _subClass5; private readonly SubClass6 _subClass6; private readonly SubClass7 _subClass7; private readonly SubClass8 _subClass8; private readonly SubClass9 _subClass9; public GodFacade(SubClass subClass, SubClass2 subClass2, SubClass3 subClass3 , SubClass4 subClass4, SubClass5 subClass5, SubClass6 subClass6 , SubClass7 subClass7, SubClass8 subClass8, SubClass9 subClass9) { _subClass = subClass; _subClass2 = subClass2; _subClass3 = subClass3; _subClass4 = subClass4; _subClass5 = subClass5; _subClass6 = subClass6; _subClass7 = subClass7; _subClass8 = subClass8; _subClass9 = subClass9; } public void DoSomeThing1() => _subClass.DoSomeThing1(); public void DoSomeThing2() => _subClass2.DoSomeThing2(); public void DoSomeThing3() => _subClass3.DoSomeThing3(); public void DoSomeThing4() => _subClass4.DoSomeThing4(); public void DoSomeThing5() => _subClass5.DoSomeThing5(); public void DoSomeThing6() => _subClass6.DoSomeThing6(); public void DoSomeThing7() => _subClass7.DoSomeThing7(); public void DoSomeThing8() => _subClass8.DoSomeThing8(); public void DoSomeThing9() => _subClass9.DoSomeThing9(); } ``` ##### AdditionalFacade ```C# public class AdditionalFacade { private readonly SubClass _subClass; private readonly SubClass2 _subClass2; private readonly SubClass3 _subClass3; private readonly SubClass4 _subClass4; public AdditionalFacade(SubClass subClass, SubClass2 subClass2, SubClass3 subClass3 , SubClass4 subClass4) { _subClass = subClass; _subClass2 = subClass2; _subClass3 = subClass3; _subClass4 = subClass4; } public void DoSomeThing1() => _subClass.DoSomeThing1(); public void DoSomeThing2() => _subClass2.DoSomeThing2(); public void DoSomeThing3() => _subClass3.DoSomeThing3(); public void DoSomeThing4() => _subClass4.DoSomeThing4(); } public class AdditionalFacade2 { private readonly SubClass5 _subClass5; private readonly SubClass6 _subClass6; private readonly SubClass7 _subClass7; private readonly SubClass8 _subClass8; private readonly SubClass9 _subClass9; public AdditionalFacade2(SubClass5 subClass5, SubClass6 subClass6, SubClass7 subClass7, SubClass8 subClass8, SubClass9 subClass9) { _subClass5 = subClass5; _subClass6 = subClass6; _subClass7 = subClass7; _subClass8 = subClass8; _subClass9 = subClass9; } public void DoSomeThing5() => _subClass5.DoSomeThing5(); public void DoSomeThing6() => _subClass6.DoSomeThing6(); public void DoSomeThing7() => _subClass7.DoSomeThing7(); public void DoSomeThing8() => _subClass8.DoSomeThing8(); public void DoSomeThing9() => _subClass9.DoSomeThing9(); } // 此 Facade 可考慮刪除不使用. public class Facade { private readonly AdditionalFacade _additionalFacade; private readonly AdditionalFacade2 _additionalFacade2; public Facade(AdditionalFacade additionalFacade, AdditionalFacade2 additionalFacade2) { _additionalFacade = additionalFacade; _additionalFacade2 = additionalFacade2; } public void DoSomeThing1() => _additionalFacade.DoSomeThing1(); public void DoSomeThing2() => _additionalFacade.DoSomeThing2(); public void DoSomeThing3() => _additionalFacade.DoSomeThing3(); public void DoSomeThing4() => _additionalFacade.DoSomeThing4(); public void DoSomeThing5() => _additionalFacade2.DoSomeThing5(); public void DoSomeThing6() => _additionalFacade2.DoSomeThing6(); public void DoSomeThing7() => _additionalFacade2.DoSomeThing7(); public void DoSomeThing8() => _additionalFacade2.DoSomeThing8(); public void DoSomeThing9() => _additionalFacade2.DoSomeThing9(); } ``` - Facade 可能會變成 God Object, 耦合太多類別 - 將一些職責移到 AdditionalFacade, 讓 AdditionalFacade 可以減輕 GodFcade 問題的產生, - AdditionalFacade 除了可以讓其他 Facade 使用外, 也可以讓 Client 端單獨使用. ### 類別操作不好用, 建立更好使用的 Facade 給 Client 使用 ```C# // 原本 Connecter 的使用方式是必須自己初始化相對應的屬性後, 再呼叫 Connect() 方法. // 但使用者再使用時, 很容易忘記或是不知道要初始化屬性, 導致 Connect() 執行結果不符合預期. public class Connecter { public int ConnectTime { get; set; } public int Delay { get; set; } public IPAddress SourceIpAddress { get; set; } public IPAddress TargetIpAddress { get; set; } public void Connect() => Console.WriteLine($"Delay {Delay} 秒後, 建立一條從 {SourceIpAddress} 到 {TargetIpAddress} 的連線, 持續 {ConnectTime}"); } // 認為 Connecter 的實作不好用, 但又沒辦法修改或是沒時間等等理由. // 建立 SimplyConnecter(Facade), 給使用者. // SimplyConnecter 的使用方式不同於 Connecter, 僅需使用 Connect(Args) 方法, 並傳入對應的參數. // 封裝正確使用 Connecter 的邏輯 public class SimplyConnecter { private Connecter _connecter; public void Connect(int connectTime, int delay, string sourceIp, string targetIp) { _connecter = new Connecter { ConnectTime = connectTime, Delay = delay, SourceIpAddress = IPAddress.Parse(sourceIp), TargetIpAddress = IPAddress.Parse(targetIp), }; _connecter.Connect(); } } ``` - 有時候我們會將一些操作比較不好使用的類別(通常不容易或是有一些歷史因素, 導致沒辦法修改), 重新封裝. 令其在操作上比較容易. ### 多墊一層, 日後抽換更方便 ```C# public class City { public string Name { get; set; } public int Age { get; set; } = 3; public override string ToString() => $"{Name} is {Age}"; } // 將 Clone 的概念集中在 Clones. // Facade public class Clones { public static T CloneObj<T>(T obj) { var serialize = JsonSerializer.Serialize(obj); return JsonSerializer.Deserialize<T>(serialize); } } ``` ##### 測試程式 ```C# internal static class Program { private static void Main(string[] args) { var city = new City(){Name = "test1",Age = 100}; var city2 = Clones.CloneObj(city); city2.Name = "test2"; Console.WriteLine(city); Console.WriteLine(city2); Console.ReadKey(); } } ``` ##### 輸出結果 ``` test1 is 100 test2 is 100 ``` - Client 端若需要使用 Clone, 僅需要使用 Clones.CloneObj<T>(args), 至於如何實作的, Client 並不需要關心. - 日後需要修改 CloneObj 的實作, 僅需要修改 Clones.CloneObj<T>(args) 即可, 不需要修改每一個需要使用 CloneObj 的 Client 端. ## 總結 - Facade 把複數子系統包起來在一個類別 => 讓外界對其統一呼叫就好 - Facade 能將 Client 端程式碼以及複雜的子系統隔開. 降低呼叫端對複雜子系統的依賴 - Facade 讓使用者更容易使用一個複雜的子系統, 也避免使用者需要去進行複雜的呼叫操作 - Facade 可能會變成 God Object(耦合太多類別) - Facade 實作上跟 Adapter 很像,但重視的地方不一樣. Facade 重視的是 Client 看到的介面接口. - Facade 為了已存在的類別定義新的介面/接口/Facade, 反之 Adapter 試著讓已存在的介面/接口能再被利用. - Adapter 經常只包一個類別在其內部使用, 但 Facade 常常是包整個子系統(複數類別). - Client 可透過這個接口介面(Facade)去知道有哪些功能可以使用. - 使用者無須了解哪些子系統被使用到(亙本不需要認識到子系統),, 也無須了解如何正確地使用子系統的相關邏輯, 更不用理解子系統的實作邏輯. - 子系統不需要認識到 Facade - Facade 將 Client 與子系統隔開, 因此降低了 Client 需要操作的物件數量以及使子系統更容易使用(將操作物件的邏輯放在Facade). - Facade 能降低子系統物件之間循環依賴的可能性 ( 子系統不能再使用 Facade ! ) - Facade 無法防止 Client 直接使用子系統 ## 參考 [Facade](https://refactoring.guru/design-patterns/facade) [[Design Pattern] 外觀模式 Facade Pattern](https://dotblogs.com.tw/jesperlai/2018/04/15/153646) [外观模式](https://www.runoob.com/design-pattern/facade-pattern.html) [Facade Pattern](https://softwaredevelopmenttricky.blogspot.com/2017/12/facade-pattern.html) [Facade Design Pattern](https://sourcemaking.com/design_patterns/facade) [[Design Pattern] Facade 門面模式](https://ithelp.ithome.com.tw/articles/10227186) [Programming Patterns Overview](https://kremer.cpsc.ucalgary.ca/patterns/) --- ###### Thank you! You can find me on - [GitHub](https://github.com/s0920832252) - [Facebook](https://www.facebook.com/fourtune.chen) 若有謬誤 , 煩請告知 , 新手發帖請多包涵 # :100: :muscle: :tada: :sheep: <iframe src="https://skilltree.my/c67b0d8a-9b69-47ce-a50d-c3fc60090493/promotion?w=250" width="250" style="border:none"></iframe>