--- tags: 設計模式 --- # 橋接模式(Bridge Pattern) ## 前言 - 比較古老的 pattern , 所以之後的 patten 幾乎都會使用到橋接模式的技巧. - 如果你想要**重構某個複雜的類別**, 想將其功能實作抽離到其他類別去, 此時可參考橋接模式. - 你希望**未來類別擴充的因素是彼此不影響的**(不會有相乘的狀況發生), 意即擴充某個因素的項目時, 不會影響到其他因素的類別層次. 此時可參考橋接模式. #### 問題需求 假設我們今天想要設計一個手機硬體程式, 且這個手機硬體程式支援後續各個廠牌的擴充. 那麼依據物件導向的概念 , 我們可能會將手機共通的功能抽象化到到介面或是抽象類別. 然後替各自品牌的手機實作屬於他們自己的功能, 如下圖. ```mermaid graph TD; 手機--繼承-->三星手機 手機--繼承-->IPhone ``` 但一些軟體的實作 (e.g. 通訊錄, 照相功能) 在不同手機上是不同的 , 因此我們必須另外實作. 若是我們使用繼承來擴充功能, 那麼我們的類別圖就變成下面這樣. ##### 先依照手機品牌分類, 再依據手機功能分類 ```mermaid graph TD; 手機--繼承-->三星手機 手機--繼承-->IPhone 三星手機--繼承-->有通訊錄功能的三星手機 三星手機--繼承-->有照相功能的三星手機 IPhone--繼承-->有通訊錄功能的IPhone IPhone--繼承-->有照相功能的IPhone ``` ##### 先依照手機功能分類, 再依據手機品牌分類 ```mermaid graph TD; 手機功能 手機功能--繼承-->通訊錄功能 手機功能--繼承-->照相功能 通訊錄功能--繼承-->有通訊錄功能的三星手機 通訊錄功能--繼承-->有通訊錄功能的IPhone 照相功能--繼承-->有照相功能的三星手機 照相功能--繼承-->有照相功能的IPhone ``` 不論你先使用哪一種因素來分類, 你都會發現我們有 2 個因素再決定我們的底層類別數 - **手機品牌**以及**手機功能** , 所以底層類別數是 2 * 2 = 4 個. 如果當我們在某一個因素(維度)上新增一個項目時, 會連帶影響另外一個因素(維度). 舉例來說, 假設我們希望新增一個手機功能---上網, 那麼我們的類別圖就會變成 ```mermaid graph TD; 手機--繼承-->三星手機 手機--繼承-->IPhone 三星手機--繼承-->有通訊錄功能的三星手機 三星手機--繼承-->有照相功能的三星手機 三星手機--繼承-->有上網功能的三星手機 IPhone--繼承-->有通訊錄功能的IPhone IPhone--繼承-->有照相功能的IPhone IPhone--繼承-->有上網功能的IPhone ``` 底層類別數是 2 * 3 = 6 個. 假設我們又希望增加一個手機品牌---ASUS, 那麼我們的類別圖就會變成 ```mermaid graph TD; 手機--繼承-->三星手機 手機--繼承-->IPhone 手機--繼承-->ASUS 三星手機--繼承-->有通訊錄功能的三星手機 三星手機--繼承-->有照相功能的三星手機 三星手機--繼承-->有上網功能的三星手機 IPhone--繼承-->有通訊錄功能的IPhone IPhone--繼承-->有照相功能的IPhone IPhone--繼承-->有上網功能的IPhone ASUS--繼承-->有通訊錄功能的ASUS ASUS--繼承-->有照相功能的ASUS ASUS--繼承-->有上網功能的ASUS ``` 底層類別數是 3 * 3 = 9 個. 假設我們又希望新增一個新的因素(維度)---顏色(金色和黑色) , 那麼我們的類別圖就會變成 ```mermaid graph TD; 手機--繼承-->三星手機 手機--繼承-->IPhone 手機--繼承-->ASUS 三星手機--繼承-->有通訊錄功能的三星手機 有通訊錄功能的三星手機--繼承-->金色的有通訊錄功能的三星手機 有通訊錄功能的三星手機--繼承-->黑色的有通訊錄功能的三星手機 三星手機--繼承-->有照相功能的三星手機 有照相功能的三星手機--繼承-->金色的有照相功能的三星手機 有照相功能的三星手機--繼承-->黑色的有照相功能的三星手機 三星手機--繼承-->有上網功能的三星手機 有上網功能的三星手機--繼承-->金色的有上網功能的三星手機 有上網功能的三星手機--繼承-->黑色的有上網功能的三星手機 IPhone--繼承-->有通訊錄功能的IPhone 有通訊錄功能的IPhone--繼承-->金色的有通訊錄功能的IPhone 有通訊錄功能的IPhone--繼承-->黑色的有通訊錄功能的IPhone IPhone--繼承-->有照相功能的IPhone 有照相功能的IPhone--繼承-->金色的有照相功能的IPhone 有照相功能的IPhone--繼承-->黑色的有照相功能的IPhone IPhone--繼承-->有上網功能的IPhone 有上網功能的IPhone--繼承-->金色的有上網功能的IPhone 有上網功能的IPhone--繼承-->黑色的有上網功能的IPhone ASUS--繼承-->有通訊錄功能的ASUS 有通訊錄功能的ASUS--繼承-->金色的有通訊錄功能的ASUS 有通訊錄功能的ASUS--繼承-->黑色的有通訊錄功能的ASUS ASUS--繼承-->有照相功能的ASUS 有照相功能的ASUS--繼承-->金色的有照相功能的ASUS 有照相功能的ASUS--繼承-->黑色的有照相功能的ASUS ASUS--繼承-->有上網功能的ASUS 有上網功能的ASUS--繼承-->金色的有上網功能的ASUS 有上網功能的ASUS--繼承-->黑色的有上網功能的ASUS ``` 底層類別數是 3 * 3 * 2 = 18 個類別... **PS : 真實世界 , 你要是真這麼設計你的類別, 光是你的底層類別就很有可能可以破百XD.** ## Bridge 介紹 ### Bridge 定義 > Decouple an abstraction from its implementation so that the two can vary independently. > * Applicability > * Need to avoid a permanent binding between an abstraction (type!) and its implementation > * Both abstractions and implementation should be extensible through subclassing > * Need to isolate changes in implementations from clients > * Need to completely hide implementation from clients > * Need to split objects because of prolifereation of classes ("nested generalizations") > * Consequences > * Decouples interface and implementation > * Improves extensibility > * Hides implementation details from clients - 橋接模式讓**抽象以及其相對應的實作能夠解偶** - 橋接模式將類別劃分成概念(抽象)與行為(實作), 並**放在不同的類別樹中**. - 因為兩個類別並不處在同個類別樹中, 故概念類別的任何修改都不會影響到行為類別, 反之亦然. - 橋接模式要求我們**善用組合**而非使用繼承來處理類別的設計. - 使用繼承處理問題 ```mermaid graph LR; 概念類別--繼承-->A概念類別 概念類別--繼承-->B概念類別 概念類別--繼承-->C概念類別 A概念類別--繼承-->有C行為的A概念類別 A概念類別--繼承-->有D行為的A概念類別 B概念類別--繼承-->有C行為的B概念類別 B概念類別--繼承-->有D行為的B概念類別 C概念類別--繼承-->有C行為的C概念類別 C概念類別--繼承-->有D行為的C概念類別 ``` - 使用組合處理問題 --- UML圖超像是用一座橋(虛線)將兩個類別樹給連接起來. ```mermaid graph LR; 概念類別 -..-|概念類別使用行為類別去做事|行為類別 概念類別--繼承-->概念類別A類別 概念類別--繼承-->概念類別B類別 概念類別--繼承-->概念類別C類別 行為類別--繼承-->行為類別C類別 行為類別--繼承-->行為類別D類別 ``` ### Bridge UML ![bridge-diagram.png](https://github.com/s0920832252/C_Sharp/blob/master/Files/DesignPattern/bridge-diagram.png?raw=true) - Abstraction - 通常是抽象類別或是介面, 不過當 RefinedAbstraction 不需要存在的時候, 其也可以是普通的實作類別. - 內部會擁有一個或以上的 Implementor 物件的欄位/屬性 - 可以透過依賴注入的方式取得 Implementor - Refined Abstraction - 實作 Abstraction 介面的實作類別, 通常會進行功能的擴充. - Implementor - 定義實作部分的共同介面, 通常和 Abstraction 有很大的差異. - Implementor 只包含基本操作, 讓 Abstraction 可以利用 Implementor 的具體實作類別去組合出更高階的操作 - 通常是抽象類別或是介面 - Concreate - Implementor 的實作類別 ### Pseudo Code ```C# public interface IImplementor { void Method(); } public class Implementor1 : IImplementor { public void Method() => Console.WriteLine($"{nameof(Implementor1)}"); } // ... ~ ImplementorN public abstract class Abstraction { protected readonly IImplementor Implementor; protected Abstraction(IImplementor implementor) => Implementor = implementor; public abstract void Feature(); // 定義上不一定要使用抽象. } public class RefinedAbstraction : Abstraction { public RefinedAbstraction(IImplementor implementor) : base(implementor) {} public override void Feature() => Implementor?.Method(); } ``` ## Code Example ### Sample - 不使用 Bridge 來實作 ##### 需求 - 實作手機類別 , 假設不同廠牌在打電話的功能的實作是相同的, 但通訊錄以及照相功能的實作是不同的. ```C# public abstract class 手機 // 也可以用介面 { public abstract string 手機品牌 { get; } public void 打電話() => Console.WriteLine($"執行{手機品牌}的{nameof(打電話)}功能 [假設每一家的實作相同]"); } public class 三星手機 : 手機 { public override string 手機品牌 => nameof(三星手機); } public class Iphone : 手機 { public override string 手機品牌 => "Iphone"; } public class 有通訊錄功能的三星手機 : 三星手機 { public void 通訊錄() => Console.WriteLine($"執行{手機品牌}的三星{nameof(通訊錄)}功能"); } public class 有照相功能的三星手機 : 三星手機 { public void 照相() => Console.WriteLine($"執行{手機品牌}的三星{nameof(照相)}功能"); } public class 有通訊錄功能的Iphone手機 : Iphone { public void 通訊錄() => Console.WriteLine($"執行{手機品牌}的Iphone{nameof(通訊錄)}功能"); } public class 有照相功能的Iphone手機 : Iphone { public void 照相() => Console.WriteLine($"執行{手機品牌}的Iphone{nameof(照相)}功能"); } ``` ##### 測試程式 ```C# internal static class Program { private static void Main(string[] args) { new 有通訊錄功能的三星手機().打電話(); new 有照相功能的三星手機().打電話(); new 有通訊錄功能的三星手機().通訊錄(); new 有照相功能的三星手機().照相(); new 有通訊錄功能的Iphone手機().打電話(); new 有照相功能的Iphone手機().打電話(); new 有通訊錄功能的Iphone手機().通訊錄(); new 有照相功能的Iphone手機().照相(); Console.ReadKey(); } } ``` ##### 輸出結果 ``` 執行三星手機的打電話功能 [假設每一家的實作相同] 執行三星手機的打電話功能 [假設每一家的實作相同] 執行三星手機的三星通訊錄功能 執行三星手機的三星照相功能 執行Iphone的打電話功能 [假設每一家的實作相同] 執行Iphone的打電話功能 [假設每一家的實作相同] 執行Iphone的Iphone通訊錄功能 執行Iphone的Iphone照相功能 ``` ### Sample - 使用 Bridge 來實作 ```C# public abstract class 手機 // 也可以考慮用介面 { public abstract string 手機品牌 { get; } public void 打電話() => Console.WriteLine($"執行{手機品牌}的{nameof(打電話)}功能 [假設每一家的實作相同]"); public abstract void 執行手機軟體(); } public interface I手機軟體 { string 執行軟體(); } public class 三星照相機 : I手機軟體 { public string 執行軟體() => $"{nameof(三星照相機)}功能"; } public class Iphone照相機 : I手機軟體 { public string 執行軟體() => $"{nameof(Iphone照相機)}功能"; } public class 三星通訊錄 : I手機軟體 { public string 執行軟體() => $"{nameof(三星通訊錄)}功能"; } public class Iphone通訊錄 : I手機軟體 { public string 執行軟體() => $"{nameof(Iphone通訊錄)}功能"; } public class 三星手機 : 手機 { public override string 手機品牌 => nameof(三星手機); public I手機軟體 手機軟體 { get; set; } public override void 執行手機軟體() => Console.WriteLine($"執行{手機品牌}的{手機軟體.執行軟體()}"); } public class Iphone : 手機 { public override string 手機品牌 => nameof(Iphone); public I手機軟體 手機軟體 { get; set; } public override void 執行手機軟體() => Console.WriteLine($"執行{手機品牌}的{手機軟體.執行軟體()}"); } ``` ##### 測試程式 ```C# internal static class Program { private static void Main(string[] args) { 手機 有通訊錄功能的三星手機 = new 三星手機() { 手機軟體 = new 三星通訊錄() }; 手機 有通訊功能的Iphone = new Iphone() { 手機軟體 = new Iphone通訊錄() }; 手機 有照相機功能的三星手機 = new 三星手機() { 手機軟體 = new 三星照相機() }; 手機 有照相功能的Iphone = new Iphone() { 手機軟體 = new Iphone照相機() }; 有通訊錄功能的三星手機.打電話(); 有照相機功能的三星手機.打電話(); 有通訊錄功能的三星手機.執行手機軟體(); 有照相機功能的三星手機.執行手機軟體(); 有通訊功能的Iphone.打電話(); 有照相功能的Iphone.打電話(); 有通訊功能的Iphone.執行手機軟體(); 有照相功能的Iphone.執行手機軟體(); Console.ReadKey(); } } ``` ##### 輸出結果 ``` 執行三星手機的打電話功能 [假設每一家的實作相同] 執行三星手機的打電話功能 [假設每一家的實作相同] 執行三星手機的三星通訊錄功能 執行三星手機的三星照相機功能 執行Iphone的打電話功能 [假設每一家的實作相同] 執行Iphone的打電話功能 [假設每一家的實作相同] 執行Iphone的Iphone通訊錄功能 執行Iphone的Iphone照相機功能 ``` - 當使用 Bridge 後 , 手機軟體的實作就已經跟手機切開了. 若之後需要增加新的手機軟體, 也只要再增加新的手機軟體類別並且讓此類別實作手機軟體介面就好, 手機類別以及其子類別不需要做任何的改變. 反之亦然. ### Sample - 有兩個 Implementor ```C# public abstract class 手機 // 也可以考慮用介面 { public abstract string 手機品牌 { get; } public void 打電話() => Console.WriteLine($"執行{手機品牌}的{nameof(打電話)}功能 [假設每一家的實作相同]"); public abstract void 照相(); public abstract void 通訊錄(); } public interface I通訊錄 { string 通訊錄(); } public class 三星通訊錄 : I通訊錄 { public string 通訊錄() => $"{nameof(三星通訊錄)}功能"; } public class Iphone通訊錄 : I通訊錄 { public string 通訊錄() => $"{nameof(Iphone通訊錄)}功能"; } public interface I照相機 { string 照相(); } public class 三星照相機 : I照相機 { public string 照相() => $"{nameof(三星照相機)}功能"; } public class Iphone照相機 : I照相機 { public string 照相() => $"{nameof(Iphone照相機)}功能"; } public class 三星手機 : 手機 { private readonly I通訊錄 _通訊錄; private readonly I照相機 _照相機; public override string 手機品牌 => "三星手機"; public 三星手機(I照相機 照相機, I通訊錄 通訊錄) { _照相機 = 照相機; _通訊錄 = 通訊錄; } public override void 照相() => Console.WriteLine($"執行{手機品牌}的{_照相機.照相()}"); public override void 通訊錄() => Console.WriteLine($"執行{手機品牌}的{_通訊錄.通訊錄()}"); } public class Iphone : 手機 { private readonly I通訊錄 _通訊錄; private readonly I照相機 _照相機; public override string 手機品牌 => "Iphone"; public Iphone(I照相機 照相機, I通訊錄 通訊錄) { _照相機 = 照相機; _通訊錄 = 通訊錄; } public override void 照相() => Console.WriteLine($"執行{手機品牌}的{_照相機.照相()}"); public override void 通訊錄() => Console.WriteLine($"執行{手機品牌}的{_通訊錄.通訊錄()}"); } ``` ##### 測試程式 ```C# internal static class Program { private static void Main(string[] args) { 手機 三星手機 = new 三星手機(new 三星照相機(), new 三星通訊錄()); 手機 iphone = new Iphone(new Iphone照相機(), new Iphone通訊錄()); 三星手機.打電話(); 三星手機.通訊錄(); 三星手機.照相(); iphone.打電話(); iphone.通訊錄(); iphone.照相(); Console.ReadKey(); } } ``` ##### 輸出結果 ``` 執行三星手機的打電話功能 [假設每一家的實作相同] 執行三星手機的三星通訊錄功能 執行三星手機的三星照相機功能 執行Iphone的打電話功能 [假設每一家的實作相同] 執行Iphone的Iphone通訊錄功能 執行Iphone的Iphone照相機功能 ``` - 手機類別以及其子類別的功能可以隨意替換成 Iphone or 三星 or 其他廠商製造. - 使用橋接模式後, 若需要新增功能, 僅需要新增建構子參數(注入), 以及新增對應的方法(此方法會使用到新被注入的物件), 反之若需移除, 也僅需要移除對應的方法, 以及移除方法使用的建構子參數. - 在設計手機的子類別時會依據其需要的實作決定建構子參數 ## 總結 - 橋接模式的執行流程圖 ```sequence Client->Abstraction : 使用 Abstraction 提供的方法 Note right of Abstraction: 組合 Implementor(s) 提供的方法, 以完成 Abstraction 的功能. Abstraction->Implementor : 使用 Implementor(s) 提供的方法 Implementor-->Abstraction: 若有回傳值, 則回傳結果. Abstraction-->Client: 若有回傳值, 則回傳結果 ``` - 橋接模式將類別劃分成概念(抽象)與行為(實作), 並放在不同的類別樹中, 因此抽象跟實作可以**各自擴充, 不會影響到對方.** - **遵守開放封閉原則**, 可以簡單地藉由新增新的概念類別(抽象)或行為類別(實作)以滿足新的需求 - **遵守單一職責原則**, 概念類別(抽象)可以專注於處理高層次的邏輯, 行為類別(實作)可以專注於處理實作的細節 - 橋接模式讓概念(抽象)與行為(實作)能夠解耦合 - 對於行為類別的實作改變, 不會影響到概念類別的呼叫方式(因為行為類別的簽章沒變) - 對於行為類別的實作改變, 不會影響到呼叫端的呼叫方式(因為客戶端僅會與概念類別互動, 並不需要知道實作細節, 而概念類別無改變) - 對於概念類別自己的實作改變(無用到行為類別), 不會影響到行為類別的使用(行為類別本來就不會認知到概念類別) - 橋接模式和轉接器模式(Adapter)的 UML 很像, 轉接器模式通常在開發前期設計類別的時候使用, 因為其不希望程式設計師設計出抽象和實作混用的類別(不方便未來擴充), 而轉接器模式則通常在已有的程式中使用, 其目標是讓相互不兼容的類別能夠很好的合作. - 橋接模式和策略模式(Strategy)的 UML 很像, 但兩者關注的重點與要解決的問題不同. **橋接模式重視的是解耦合, 策略模式重視的是策略的多變**. ## 參考 [Design Pattern: Structural Patterns — Bridge Pattern (橋接模式)](https://medium.com/bucketing/structural-patterns-bridge-pattern-e06e4de5045c) [橋接模式 (Bridge Pattern)](http://corrupt003-design-pattern.blogspot.com/2017/01/bridge-pattern.html) [Bridge Pattern -- 分成功能階層和實作階層](http://twmht.github.io/blog/posts/design-pattern/bridge.html) [Bridge](https://reactiveprogramming.io/blog/en/design-patterns/bridge) [桥接模式](https://refactoringguru.cn/design-patterns/bridge) [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>