在面向對象的設計中有不少流行的思想,好比說 "全部的成員變量都應該設置爲私有(Private)","要避免使用全局變量(Global Variables)","使用運行時類型識別(RTTI:Run Time Type Identification,例如 dynamic_cast)是危險的" 等等。那麼,這些思想的源泉是什麼?爲何它們要這樣定義?這些思想老是正確的嗎?本篇文章將介紹這些思想的基礎:開放封閉原則(Open Closed Principle)。html
Ivar Jacobson 曾說過 "全部系統在其生命週期中都會進行變化,只要系統要開發一個版本以上這一點就需時刻記住。"。java
All systems change during their life cycles. This must be borne in mind when developing systems expected to last longer than the first version.編程
那麼咱們到底如何才能構建一個穩定的設計來面對這些變化,以使軟件生命週期持續的更長呢?數組
早在1988年Bertrand Meyer 就給出了指導建議,他創造了當下很是著名的開放封閉原則。套用他的原話:"軟件實體(類、模塊、函數等)應對擴展開放,但對修改封閉。"。數據結構
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.編程語言
當一個需求變化致使程序中多個依賴模塊都發生了級聯的改動,那麼這個程序就展示出了咱們所說的 "壞設計(bad design)" 的特質。應用程序也相應地變得脆弱、僵化、沒法預期和沒法重用。開放封閉原則(Open Closed Principle)即爲解決這些問題而產生,它強調的是你設計的模塊應該從不改變。當需求變化時,你能夠經過添加新的代碼來擴展這個模塊的行爲,而不去更改那些已經存在的能夠工做的代碼。ide
開放封閉原則(Open Closed Principle)描述函數
符合開放封閉原則的模塊都有兩個主要特性:post
1. 它們 "面向擴展開放(Open For Extension)"。this
也就是說模塊的行爲是可以被擴展的。當應用程序的需求變化時,咱們可使模塊表現出全新的或與以往不一樣的行爲,以知足新的需求。
2. 它們 "面向修改封閉(Closed For Modification)"。
模塊的源代碼是不能被侵犯的,任何人都不容許修改已有源代碼。
看起來上述兩個特性是互相沖突的,由於一般擴展模塊行爲的常規方式就是修改該模塊。一個不能被修改的模塊一般被認爲其擁有着固定的行爲。那麼如何使這兩個相反的特性共存呢?
抽象是關鍵。
Abstraction is the Key.
在使用面向對象設計技術時,能夠建立固定的抽象和一組無限界的可能行爲來表述。這裏的抽象指的是抽象基類,而無限界的可能行爲則由諸多可能衍生出的子類來表示。爲了一個模塊而篡改一個抽象類是有可能的,而這樣的模塊則能夠對修改封閉,由於它依賴於一個固定的抽象。而後這個模塊的行爲能夠經過建立抽象的衍生類來擴展。
示例:Client/Server 引用
圖1 展現了一個簡單的且不符合開放封閉原則的設計。
(圖 1: 封閉的 Client)
Client 和 Server 類都是具體類(Concrete Class),因此沒法保證 Server 的成員函數是虛函數。 這裏 Client 類使用了 Server 類。若是咱們想讓 Client 對象使用一個不一樣的 Server 對象,那麼必須修改 Client 類以使用新的 Server 類和對象。
圖 2 中展現了符合開放封閉原則的相應設計。
(圖 2: 開放的 Client)
在這個示例中,AbstractServer 類是一個抽象類,幷包含一個純虛成員函數。Client 類依賴了這個抽象,但 Client 類將使用衍生的 Server 類的對象實例。若是咱們須要 Client 對象使用一個不一樣的 Server 類,則能夠從 AbstractServer 類衍生出一個新的子類,而 Client 類則依然保持不變。
示例:Shape 抽象
考慮下面這個例子。咱們有一個應用程序須要在標準 GUI 窗口上繪製圓形(Circle)和方形(Square)。圓形和方形必須以特定的順序進行繪製。圓形和方形會被建立在同一個列表中,並保持適當的順序,而程序必須可以順序遍歷列表並繪製全部的圓形和方形。
在 C 語言中,使用過程化技術是沒法知足開放封閉原則的。咱們可能會經過下面代碼顯示的方式來解決該問題。
enum ShapeType {circle, square}; struct Shape { ShapeType itsType; }; struct Square { ShapeType itsType; double itsSide; Point itsTopLeft; }; struct Circle { ShapeType itsType; double itsRadius; Point itsCenter; }; void DrawSquare(struct Square*); void DrawCircle(struct Circle*); typedef struct Shape *ShapePointer; void DrawAllShapes(ShapePointer list[], int n) { int i; for (i=0; i<n; i++) { struct Shape* s = list[i]; switch (s->itsType) { case square: DrawSquare((struct Square*)s); break; case circle: DrawCircle((struct Circle*)s); break; } } }
在這裏咱們看到了一組數據結構定義,這些結構中除了第一元素相同外,其餘都不一樣。經過第一個元素的類型碼來識別該結構是在表示一個圓形(Circle)仍是一個方形(Square)。函數 DrawAllShapes 遍歷了數組中的結構指針,檢查類型碼而後調用相匹配的函數(DrawCircle 或 DrawSquare)。
這裏函數 DrawAllShapes 不符合開放封閉原則,由於它沒法保證對新的 Shape 種類保持封閉。若是咱們想要擴展這個函數,使其可以支持一個圖形列表而且包含三角形(Triangle)定義,則咱們將不得不修改這個函數。事實上,每當咱們須要繪製新的圖形種類時,咱們都不得不修改這個函數。
固然這個程序僅僅是一個例子。在實踐中 DrawAllShapes 函數中的 switch 語句將不斷地在應用程序內的各類函數間不斷的調用,而每一個函數只是少量有些不一樣。在這樣的應用中增長一個新的 Shape 意味着須要搜尋全部相似的 switch 語句(或者是 if/else 鏈)存在的地方,而後增長新的 Shape 功能。此外,要讓全部的 switch 語句(或者是 if/else 鏈)都有相似 DrawAllShapes 函數這樣較好的結構也是不太可能的。而更有可能的則是 if 語句將和一些邏輯運算符綁定到了一塊兒,或者 switch 語句中的 case 子句的堆疊。所以要在全部的位置找到和理解這些問題,而後添加新的圖形定義可不是件簡單的事情。
下面這段代碼展現了符合開放封閉原則的 Cicle/Square 問題的一個解決方案。
public abstract class Shape { public abstract void Draw(); } public class Circle : Shape { public override void Draw() { // draw circle on GUI } } public class Square : Shape { public override void Draw() { // draw square on GUI } } public class Client { public void DrawAllShapes(List<Shape> shapes) { foreach (var shape in shapes) { shape.Draw(); } } }
在這個例子中,咱們建立了一個 Shape 抽象類,這個抽象類包含一個純虛函數 Draw。而 Circle 和 Square 都衍生自 Shape 類。
注意在這裏若是咱們想擴展 DrawAllShapes 函數的行爲來繪製一個新的圖形種類,咱們所須要作的就是增長一個從 Shape 類衍生的子類。而DrawAllShapes 函數則無需進行修改。所以DrawAllShapes 符合了開放封閉原則,它的行爲能夠不經過對其修改而擴展。
在比較現實的狀況中,Shape 類可能包含不少個方法。可是在應用程序中增長一個新的圖形仍然是很是簡單的,由於所須要作的僅是建立一個衍生類來實現這些函數。同時,咱們也再也不須要在應用程序內查找全部須要修改的位置了。
由於更改符合開放封閉原則的程序是經過增長新的代碼,而不是修改已存在的代碼,以前描述的那種級聯式的更改也就不存在了。
策略性的閉合(Strategic Closure)
要明白程序是不可能 100% 徹底封閉的。例如,試想上面的 Shape 示例,若是咱們如今決定全部的 Circle 都應該在 Square 以前先進行繪製,則 DrawAllShapes 函數將會發生什麼呢?DrawAllShapes 函數是不可能對這樣的變化保持封閉的。一般來講,不管模塊的設計有多封閉,老是有各類各樣的變化會打破這種封閉。
所以,徹底閉合是不現實的,因此必須講究策略。也就是說,程序設計師必須甄別其設計對哪些變化封閉。這須要一些基於經驗的預測。有經驗的設計師會很好的瞭解用戶和所在的行業,以判斷各類變化的可能性。而後能夠肯定對最有可能的變化保持開放封閉原則。
使用抽象來獲取顯示地閉合
那咱們該如何使 DrawAllShapes 函數對繪製邏輯中的排序的變化保持閉合呢?要記住閉合是基於抽象的。所以,爲了使 DrawAllShapes 對排序閉合,則咱們須要對排序進行某種程度的抽象。上述例子中關於排序的一個特例就是某種類別的圖形須要在其餘類別的圖像以前進行繪製。
一個排序策略就是,給定任意兩個對象,能夠發現哪個應當被先繪製。所以,咱們能夠在 Shape 中定義一個名爲 Precedes 的方法,它能夠接受另外一個 Shape 做爲參數並返回一個 bool 類型的結果。若是結果爲 true 則表示接收調用的 Shape 對象應排在被做爲參數的 Shape 對象的前面。
咱們可使用重載操做符技術來實現這樣的比較功能。這樣經過比較咱們就能夠獲得兩個 Shape 對象的相對順序,而後排序後就能夠按照順序進行繪製。
下面顯示了簡單實現的代碼。
public abstract class Shape { public abstract void Draw(); public bool Precedes(Shape another) { if (another is Circle) return true; else return false; } } public class Circle : Shape { public override void Draw() { // draw circle on GUI } } public class Square : Shape { public override void Draw() { // draw square on GUI } } public class ShapeComparer : IComparer<Shape> { public int Compare(Shape x, Shape y) { return x.Precedes(y) ? 1 : 0; } } public class Client { public void DrawAllShapes(List<Shape> shapes) { SortedSet<Shape> orderedList = new SortedSet<Shape>(shapes, new ShapeComparer()); foreach (var shape in orderedList) { shape.Draw(); } } }
這達成了排序 Shape 對象的目的,並可按照適當的順序進行排序。但咱們仍然尚未一個合適的排序抽象。以如今這種狀況,單獨的 Shape 對象將不得不覆寫 Precedes 方法來指定順序。這將如何工做呢?咱們須要在 Precedes 中寫什麼樣的代碼才能確保 Circle 可以在 Square 以前繪製呢?
public bool Precedes(Shape another) { if (another is Circle) return true; else return false; }
能夠看出,這個函數不符合開放封閉原則。沒法使其對新衍生出的 Shape 子類保持封閉。每次當一個新的 Shape 衍生類被建立時,這個方法將老是被修改。
使用 "數據驅動(Data Driven)" 的方法來達成閉合
使用表驅動(Table Driven)方法可以達成對 Shape 衍生類的閉合,而不會強制修改每一個衍生類。
下面展現了一種可能的設計。
private Dictionary<Type, int> _typeOrderTable = new Dictionary<Type, int>(); private void Initialize() { _typeOrderTable.Add(typeof(Circle), 2); _typeOrderTable.Add(typeof(Square), 1); } public bool Precedes(Shape another) { return _typeOrderTable[this.GetType()] > _typeOrderTable[another.GetType()]; }
經過使用這種方法咱們已經成功地使 DrawAllShapes 函數在通常狀況下對排序問題保持封閉,而且每一個 Shape 的衍生類都對新的 Shape 子類或者排序策略的修改(例如修改排序規則以使先繪製 Square)等保持封閉。
這裏仍然沒法對多種 Shape 的順序保持封閉的就是表(Table)自己。但咱們能夠將這個表定義放置在單獨的模塊中,使表與其餘模塊隔離,這樣對錶的更改則再也不會對任何其餘模塊產生影響。
進一步的擴展閉合
這並非故事的尾聲。
咱們能夠掌控 Shape 的層級結構和 DrawAllShapes 函數對依據不一樣 Shape 類型的排序規則的閉合。儘管如此,Shape 的衍生類對不判斷圖形類型的排序規則是非閉合的。看起來可能咱們但願能夠根據更高級別的結構來對 Shape 進行排序。對這個問題的一個完整的研究已經超出了這篇文章的範圍,可是感興趣的讀者能夠考慮如何實現。例如讓一個 OrderedShape 類來持有一個抽象的 OrderedObject 類,而其自身同時繼承自 Shape 和 OrderedObject 類的實現。
總結
關於開放封閉原則(Open Closed Principle)還有不少能夠講的。在不少方面這個原則都是面向對象設計的核心。始終遵循該原則才能從面向對象技術中持續地得到最大的益處,例如:可重用性和可維護性。同時,對該原則的遵循也不是經過使用一種面向對象的編程語言就可以達成的。更確切的說,它須要程序設計師更專一於將抽象技術應用到程序中那些趨於變化的部分上。
參考資料