開篇還是引用呂振宇老師的那篇經典的文章《設計模式隨筆-蠟筆與毛筆的故事》。這個真是太經典了,沒有比這個例子能更好的闡明橋接模式了,這裡我就直接盜來用了。 現在市面上賣的蠟筆很多,各種型號,各種顏色種類繁多, 假如一盒蠟筆有24種顏色,那麼它能塗抹出24種不同的顏色來,蠟筆型號是固定的,如果想畫出各種 ...
開篇還是引用呂振宇老師的那篇經典的文章《設計模式隨筆-蠟筆與毛筆的故事》。這個真是太經典了,沒有比這個例子能更好的闡明橋接模式了,這裡我就直接盜來用了。
現在市面上賣的蠟筆很多,各種型號,各種顏色種類繁多, 假如一盒蠟筆有24種顏色,那麼它能塗抹出24種不同的顏色來,蠟筆型號是固定的,如果想畫出各種線條那麼就要購買不同型號的蠟筆,假如我們要塗抹出粗,中,細三種線條,那麼我們就要買3盒粗,中,細型號的蠟筆才能滿足需求,那麼就是3盒*24色=72只蠟筆。假如使用毛筆來作畫,我們需要準備3只粗,中,細的毛筆和24種顏料就好了, 那麼就是3只毛筆+24種顏料。使用毛筆和使用蠟筆的差別是:用毛筆只需要3+24=27,用蠟筆需要準備3*24=72. 為什麼會出現這麼大的差別呢?仔細分析就會發現,畫畫的時候不僅對筆的型號有要求,而且還對顏色有要求,也就是說有兩個引起變化的點或者說有兩個變化的維度。蠟筆的型號和顏色直接綁定在一起了,他們二者完全融合(耦合)在一起了,他們的屬性從生產出來就已經固化了(是靜態的),不能被改變了。 而毛筆的型號和顏料的顏色是毫無關係(解耦),毛筆廠家生產不同型號的毛筆,顏料廠家生產不同顏色的顏料,二者互不相干,只有在使用的時候用戶決定用什麼型號的毛筆蘸什麼顏色的顏料(動態設置)來作畫,這個時候毛筆和顏料才動態發生關係,如果用戶想使用一個型號的毛筆畫不同顏色的畫,毛筆可以洗掉再蘸不同的顏色就可以。
在看看蠟筆和毛筆在應對變化的優劣比較, 如果用戶需要畫一個加粗線條,蠟筆需要買一盒(24),毛筆只需要買一支就可以了。如果要加一種顏色,蠟筆需要增加4支(加粗,粗,中,細),而毛筆僅僅只需要增加一種顏色就夠了。 從數學的角度來講蠟筆不管是顏色或者型號的變化都會形成:型號數*顏色數,毛筆卻是:型號數+顏色數。這樣看來毛筆更有優勢,更容易應對變化的需求。
那麼在軟體開發的過程中也會碰到類似的問題,怎麼來解決這類問題呢?這就是我們將要探討的橋接模式(Brigde)。
一、橋接模式的定義
橋接模式(Bridge Pattern):將抽象部分與它的實現部分分離,使它們都可以獨立地變化。它是一種對象結構型模式,又稱為柄體(Handle and Body)模式或介面(Interface)模式。
二、橋接模式結構圖
1、Abstraction(抽象類):
用於定義抽象類的介面,它一般是抽象類而不是介面,其中定義了一個Implementor(實現類介面)類型的對象並可以維護該對象,它與Implementor之間具有關聯關係,它既可以包含抽象業務方法,也可以包含具體業務方法。
2、RefinedAbstraction(擴充抽象類):
擴充由Abstraction定義的介面,通常情況下它不再是抽象類而是具體類,它實現了在Abstraction中聲明的抽象業務方法,在RefinedAbstraction中可以調用在Implementor中定義的業務方法。
3、Implementor(實現類介面):
定義實現類的介面,這個介面不一定要與Abstraction的介面完全一致,事實上這兩個介面可以完全不同,一般而言,Implementor介面僅提供基本操作,而Abstraction定義的介面可能會做更多更複雜的操作。Implementor介面對這些基本操作進行了聲明,而具體實現交給其子類。通過關聯關係,在Abstraction中不僅擁有自己的方法,還可以調用到Implementor中定義的方法,使用關聯關係來替代繼承關係。
4、ConcreteImplementor(具體實現類):
具體實現Implementor介面,在不同的ConcreteImplementor中提供基本操作的不同實現,在程式運行時,ConcreteImplementor對象將替換其父類對象,提供給抽象類具體的業務操作方法。
三、橋接模式經典實現
public abstract class Implementor { public abstract void Operation(); } public abstract class Abstraction { protected Implementor implementor; public Implementor Implementor { set { this.implementor = value; } } public virtual void Operation() { implementor.Operation(); } } public class ConcreteImplementorA : Implementor { public override void Operation() { Console.WriteLine(this.GetType().Name + " Operation"); } } public class ConcreteImplementorB : Implementor { public override void Operation() { Console.WriteLine(this.GetType().Name + " Operation"); } } public class RefinedAbstraction : Abstraction { public override void Operation() { base.Operation(); } }
客戶端調用:
static void Main(string[] args) { Abstraction abstraction = new RefinedAbstraction(); Implementor implementor = new ConcreteImplementorA(); abstraction.Implementor = implementor; abstraction.Operation(); implementor = new ConcreteImplementorB(); abstraction.Implementor = implementor; abstraction.Operation(); Console.ReadKey(); }
輸出結果:
四、橋接模式的實例
我們分別模擬實現開頭提到的蠟筆和毛筆。我們只選擇三種型號(Large,Middle,Small)和三種顏色(Red,Green, Blue).
1、蠟筆的實現
蠟筆類代碼:
public abstract class Crayon { protected string size; protected string color; protected abstract void SetSize(); protected abstract void SetColor(); public void Display() { SetSize(); SetColor(); Console.WriteLine(this.GetType().Name + ": [Size]=" + size + "[Color]=" + color); } } public abstract class LargeCrayon : Crayon { protected override void SetSize() { size = "Large"; } } public abstract class MiddleCrayon : Crayon { protected override void SetSize() { size = "Middle"; } } public abstract class SmallCrayon : Crayon { protected override void SetSize() { size = "Small"; } } public class RedLargeCrayon : LargeCrayon { protected override void SetColor() { color = "Red"; } } public class GreenLargeCrayon : LargeCrayon { protected override void SetColor() { color = "Green"; } } public class BlueLargeCrayon : LargeCrayon { protected override void SetColor() { color = "Blue"; } } public class RedMiddleCrayon : MiddleCrayon { protected override void SetColor() { color = "Red"; } } public class GreenMiddleCrayon : MiddleCrayon { protected override void SetColor() { color = "Green"; } } public class BlueMiddleCrayon : MiddleCrayon { protected override void SetColor() { color = "Blue"; } } public class RedSmallCrayon : SmallCrayon { protected override void SetColor() { color = "Red"; } } public class GreenSmallCrayon : SmallCrayon { protected override void SetColor() { color = "Green"; } } public class BlueSmallCrayon : SmallCrayon { protected override void SetColor() { color = "Blue"; } }
客戶端調用:
static void Main(string[] args) { Crayon.Crayon redLargeCrayon, greenLargeCrayon, blueLargeCrayon, redMiddleCrayon, greenMiddleCrayon, blueMiddleCrayon, redSmallCrayon, greenSmallCrayon, blueSmallCrayon; redLargeCrayon = new RedLargeCrayon(); greenLargeCrayon = new GreenLargeCrayon(); blueLargeCrayon = new BlueLargeCrayon(); redMiddleCrayon = new RedMiddleCrayon(); greenMiddleCrayon = new GreenMiddleCrayon(); blueMiddleCrayon = new BlueMiddleCrayon(); redSmallCrayon = new RedSmallCrayon(); greenSmallCrayon = new GreenSmallCrayon(); blueSmallCrayon = new BlueSmallCrayon(); redLargeCrayon.Display(); greenLargeCrayon.Display(); blueLargeCrayon.Display(); redMiddleCrayon.Display(); greenMiddleCrayon.Display(); blueMiddleCrayon.Display(); redSmallCrayon.Display(); greenSmallCrayon.Display(); blueSmallCrayon.Display(); Console.ReadKey(); }
輸出:
蠟筆是一種典型的多層繼承結構, 型號和顏色實在繼承體系中得以實現,在程式編譯的時候型號和顏色就已經綁定好了,在運行時無法再動態改變,並且類非常多,如果增加型號或者顏色將非常難以維護。
2、墨筆的實現
毛筆類結構圖
毛筆類代碼:
public abstract class Brush { private Color color; protected string size; public void SetColor(Color color) { this.color = color; } protected abstract void SetSize(); public void Draw() { SetSize(); Console.WriteLine(this.GetType().Name + ": [Size]=" + this.size + "->[Color]=" + this.color.CurrentColor); } } public abstract class Color { protected string color; public string CurrentColor { get { return color; } } } public class RedColor:Color { public RedColor() { this.color = "Red"; } } public class GreenColor:Color { public GreenColor() { this.color = "Green"; } } public class BlueColor : Color { public BlueColor() { this.color = "Blue"; } } public class LargeBrush : Brush { protected override void SetSize() { this.size = "Large"; } } public class MiddleBrush : Brush { protected override void SetSize() { this.size = "Middle"; } } public class SmallBrush : Brush { protected override void SetSize() { this.size = "Small"; } }
客戶端調用:
static void Main(string[] args) { Brush largeBrush, middleBrush, smallBrush; Color red, green, blue; red = new RedColor(); green = new GreenColor(); blue = new BlueColor(); largeBrush = new LargeBrush(); middleBrush = new MiddleBrush(); smallBrush = new SmallBrush(); largeBrush.SetColor(red); largeBrush.Draw(); largeBrush.SetColor(green); largeBrush.Draw(); largeBrush.SetColor(blue); largeBrush.Draw(); middleBrush.SetColor(red); middleBrush.Draw(); middleBrush.SetColor(green); middleBrush.Draw(); middleBrush.SetColor(blue); middleBrush.Draw(); smallBrush.SetColor(red); smallBrush.Draw(); smallBrush.SetColor(green); smallBrush.Draw(); smallBrush.SetColor(blue); smallBrush.Draw(); Console.ReadKey(); }
輸出結果:
LargeBrush: [Size]=Large->[Color]=Red LargeBrush: [Size]=Large->[Color]=Green LargeBrush: [Size]=Large->[Color]=Blue MiddleBrush: [Size]=Middle->[Color]=Red MiddleBrush: [Size]=Middle->[Color]=Green MiddleBrush: [Size]=Middle->[Color]=Blue SmallBrush: [Size]=Small->[Color]=Red SmallBrush: [Size]=Small->[Color]=Green SmallBrush: [Size]=Small->[Color]=Blue
毛筆類之間的結構發生了一些變化,將蠟筆的深度繼承關係變成了一個平行的關聯關係,這樣帶來的好處是毛筆的型號和顏色可以在兩個體系中獨立的變化而互不影響。這樣就降低了耦合度,提高了擴展性,和可維護性,使得類的數量也急劇減少,降低了複雜度。
五、橋接模式的優點
- 分離抽象介面及其實現部分。橋接模式使用“對象間的關聯關係”解耦了抽象和實現之間固有的綁定關係,使得抽象和實現可以沿著各自的維度來變化。所謂抽象和實現沿著各自維度的變化,也就是說抽象和實現不再在同一個繼承層次結構中,而是“子類化”它們,使它們各自都具有自己的子類,以便各自子類的組合,從而獲得多維度組合對象。
- 在很多情況下,橋接模式可以取代多層繼承方案,多層繼承方案違背了“單一職責原則(SRP)”,復用性較差,且類的個數非常多,橋接模式是比多層繼承方案更好的解決方法,它極大減少了類的個數。
- 橋接模式提高了系統的可擴展性,在兩個變化維度中任意擴展一個維度,都不需要修改原有系統,符合“開閉原則(OCP)”。
六、橋接模式的缺點
- 橋接模式的使用會增加系統的理解與設計難度,由於關聯關係建立在抽象層,要求開發者一開始就就要針對抽象層進行設計與編程。
- 橋接模式要求正確識別出系統中兩個獨立變化的維度,因此其使用範圍具有一定的局限性,如何正確識別兩個獨立維度也需要一定的經驗積累。
七、橋接模式的使用場景
- 如果一個系統需要在抽象化和具體化之間增加更多的靈活性,避免在兩個層次之間建立靜態的繼承關係,通過橋接模式可以使它們在抽象層建立一個關聯關係。
- “抽象部分”和“實現部分”可以以繼承的方式獨立擴展而互不影響,在程式運行時可以動態將一個抽象化子類的對象和一個實現化子類的對象進行組合,即系統需要對抽象化角色和實現化角色進行動態耦合。
- 一個類存在兩個(或多個)獨立變化的維度,且這兩個(或多個)維度都需要獨立進行擴展。
- 對於那些不希望使用繼承或因為多層繼承導致系統類的個數急劇增加的系統,使用橋接模式。
八、練習
某軟體公司欲開發一個數據轉換工具,可以將資料庫中的數據轉換成多種文件格式,例如txt、xml、pdf等格式,同時該工具需要支持多種不同的資料庫。試使用橋接模式對其進行設計。
可以使用橋接模式做一個簡單的實現如下:
public abstract class Database { public abstract string GetData(); } public abstract class Exportor { private Database database; public void SetDatabase(Database database) { this.database = database; } public void Export() { var data = this.database.GetData(); var fileType = this.GetFileType(); Console.WriteLine(this.GetType().Name + "[Database] is [" + data + "] [FileType] is [" + fileType + "]"); } protected abstract string GetFileType(); } public class SQLDatabase : Database { public override string GetData() { return "SQLDatabase"; } } public class OracalDatabase : Database { public override string GetData() { return "OracalDatabase"; } } public class SQLiteDatabase : Database { public override string GetData() { return "SQLiteDatabase"; } } public class DBaseDatabase : Database { public override string GetData() { return "DBaseDatabase"; } } public class ExcelExportor : Exportor { protected override string GetFileType() { return "Excel"; } } public class TxtExportor : Exportor { protected override string GetFileType() { return "TxT"; } } public class XmlExportor : Exportor { protected override string GetFileType() { return "XML"; } } public class PDFExportor : Exportor { protected override string GetFileType() { return "PDF"; } }
客戶端調用:
static void ExecuteExport() { Exportor exportExcel, exportPdf, exportXml, exportTxt; Database sql, sqlite, dbase, oracal; sql = new SQLDatabase(); sqlite = new SQLiteDatabase(); dbase = new DBaseDatabase(); oracal = new OracalDatabase(); exportExcel = new ExcelExportor(); exportPdf = new PDFExportor(); exportTxt = new PDFExportor(); exportXml = new XmlExportor(); exportXml.SetDatabase(sql); exportXml.Export(); exportXml.SetDatabase(oracal); exportXml.Export(); }
輸出:
XmlExportor[Database] is [SQLDatabase] [FileType] is [XML] XmlExportor[Database] is [OracalDatabase] [FileType] is [XML]
抽出一個泛型執行器,使客戶端調用代碼更優雅一點,泛型執行器的代碼如下:
public interface IExportorExcutor<in T, in V> where T : Exportor where V : Database { void Execute(); } public class ExportorExcutor<T, V> : IExportorExcutor<T, V> where T : Exportor, new() where V : Database, new() { public static IExportorExcutor<T, V> Of() { return new ExportorExcutor<T, V>(); } public void Execute() { var export = new T(); var database = new V(); export.SetDatabase(database); export.Export(); } }
客戶端調用代碼:
static void Main(string[] args) { ExportorExcutor<ExcelExportor, SQLiteDatabase>.Of().Execute(); ExportorExcutor<ExcelExportor, OracalDatabase>.Of().Execute(); ExportorExcutor<ExcelExportor, SQLDatabase>.Of().Execute(); ExportorExcutor<ExcelExportor, DBaseDatabase>.Of().Execute(); ExportorExcutor<PDFExportor, SQLiteDatabase>.Of().Execute(); ExportorExcutor<PDFExportor, OracalDatabase>.Of().Execute(); ExportorExcutor<PDFExportor, SQLDatabase>.Of().Execute(); ExportorExcutor<PDFExportor, DBaseDatabase>.Of().Execute(); ExportorExcutor<TxtExportor, SQLiteDatabase>.Of().Execute(); ExportorExcutor<TxtExportor, OracalDatabase>.Of().Execute(); ExportorExcutor<TxtExportor, SQLDatabase>.Of().Execute(); ExportorExcutor<TxtExportor, DBaseDatabase>.Of().Execute(); ExportorExcutor<XmlExportor, SQLiteDatabase>.Of().Execute(); ExportorExcutor<XmlExportor, OracalDatabase>.Of().Execute(); ExportorExcutor<XmlExportor, SQLDatabase>.Of().Execute(); ExportorExcutor<XmlExportor, DBaseDatabase>.Of().Execute(); Console.ReadKey(); }
輸出:
ExcelExportor[Database] is [SQLiteDatabase] [FileType] is [Excel] ExcelExportor[Database] is [OracalDatabase] [FileType] is [Excel] ExcelExportor[Database] is [SQLDatabase] [FileType] is [Excel] ExcelExportor[Database] is [DBaseDatabase] [FileType] is [Excel] PDFExportor[Database] is [SQLiteDatabase] [FileType] is [PDF] PDFExportor[Database] is [OracalDatabase] [FileType] is [PDF] PDFExportor[Database] is [SQLDatabase] [FileType] is [PDF] PDFExportor[Database] is [DBaseDatabase] [FileType] is [PDF] TxtExportor[Database] is [SQLiteDatabase] [FileType] is [TxT] TxtExportor[Database] is [OracalDatabase] [FileType] is [TxT] TxtExportor[Database] is [SQLDatabase] [FileType] is [TxT] TxtExportor[Database] is [DBaseDatabase] [FileType] is [TxT] XmlExportor[Database] is [SQLiteDatabase] [FileType] is [XML] XmlExportor[Database] is [OracalDatabase] [FileType] is [XML] XmlExportor[Database] is [SQLDatabase] [FileType] is [XML] XmlExportor[Database] is [DBaseDatabase] [FileType] is [XML]
橋接模式就探討到這裡。