標籤:
第9章 OCP:開放-封閉原則
軟體實體(類、模組、函數等)應該是可以擴充的,但是不可修改。
9.1 OCP概述
遵循開放-封閉原則設計出的模組具有兩個主要特徵:
(1)對於擴充是開放的(open for extension)。這意味著模組的行為是可以擴充的。當應用的需求改變時,我們可以對模組進行擴充,使其具有滿足那些改變的新行為。
(2)對於修改是封閉的(closed for modification)。對模組進行擴充時,不必改動模組的原始碼或者二進位代碼。模組的二進位可執行版本,無論是可連結的庫、DLL或者.EXE檔案,都無需改動。
在C#或者其他任何OOPL(物件導向程式設計語言)中,可以建立出固定卻能夠描述一組任意個可能行為的抽象體。這個抽象體就是抽象基類。而這一組任意個可能的行為則表現為可能的衍生類別。
模組可能對抽象體進行操作。由於模組依賴於一個固定的抽象體,所以它對於更改可以是封閉的。同時,通過從這個抽象體派生,可以擴充此模組的行為。
9.2 Shape應用程式
9.2.1 違反OCP
查看如下代碼:
//--shape.h--------------------------------------- enum ShapeType { circle, square }; struct Shape { ShapeType itsType; };//--circle.h--------------------------------------- struct Circle { ShapeType itsType; double itsRadius; Point itsCenter; }; void DrawCircle(struct Circle*);//--square.h--------------------------------------- struct Square { ShapeType itsType; double itsSide; Point itsTopLeft; }; void DrawSquare(struct Square*);//--drawAllShapes.cc------------------------------- typedef struct Shape *private ShapePointer ; void DrawAllShapes(ShapePointer list[]private ,private 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; } } }
DrawAllShapes函數不符合OCP,因為它對於新的形狀類型的添加不是封閉的。如果希望這個函數能夠繪製包含三角形的列表,就必須變更這個函數。事實上,每增加一種新的形狀類型,都必須要更改這個函數。
9.2.2 遵循OCP
查看如下Square/Circle問題的OOD解決方案
public interface Shape { void Draw(); } public class Square : Shape { public void Draw() { //draw a square } } public class Circle : Shape { public void Draw() { //draw a circle } } public void DrawAllShapes(IList shapes) { foreach (Shape shape in shapes) shape.Draw(); }
9.2.3 預測變化和“貼切的”結構
一般而言,無論模組是多麼的“封閉”,都會存在一些無法對之封閉的變化。沒有對於所有的情況都貼切的模型。
既然不能完全封閉,那麼就必須有策略的對待這個問題。也就是說,設計人員必須對於他設計的模組應該對哪種變化封裝做出選擇。他必須先猜測出最有可能發生變化的類,然後構造抽象來隔離那些變化。
這需要設計人員具有一些從經驗中獲得的預測能力。有經驗的設計人員希望自己對使用者和應用領域很瞭解,能夠以此來判斷各種變化的可能性。然後,它可以讓設計對於最有可能發生的變化遵循OCP原則。
這一點不容易做到。並且在大多數情況下,他們都會猜測錯誤。
遵循OCP的代價也是昂貴的。建立適當的抽象是要花費開發時間和精力的。同時,那些抽象也增加了軟體設計的複雜性。
最終,我們會一直等到變化發生時才採取行動!
9.2.4 放置吊鉤
在上世紀,我們會在我們認為可能發生變化的地方“放置吊鉤”(hook)。我們覺得這樣會使軟體靈活一些。
然而,我們放置的吊鉤常常是錯誤的。更糟的是,即使不使用這些吊鉤,也必須要去支援和維護它們,從而就有了不必要的複雜性的臭味。通常,我們更願意一直等到卻是需要那些抽象時再把它放置進去。
9.2.5 使用抽象獲得顯式封閉
封閉是建立在抽象的基礎上的。因此,為了讓DrawAllShapes對於繪製順序的變化是封閉的。我們需要一種“順序抽象體”。這個抽象體定義了一個抽象介面,通過這個介面可以表示任何可能的排序策略。
一個排序策略意味著,給定兩個對象,可以推匯出先繪製哪一個。C#提供了這樣的抽象。IComparable是一個介面,它只提供一個方法:CompareTo。這個方法以一個對象作為輸入參數,當接受訊息的對象小於、等於、大於參數數對象時,該方法分別返回-1,0,1 。
如果希望Circle先於Square繪製,查看如下代碼:
public interface Shape : IComparable { void Draw(); } public class Square : Shape { public void Draw() { //draw a square } public int CompareTo(object obj) { if (obj is Circle) { return 1; } else { return 0; } } } public class Circle : Shape { public void Draw() { //draw a circle } public int CompareTo(object obj) { if (obj is Square) { return -1; } else { return 0; } } } public void DrawAllShapes(ArrayList shapes) { shapes.Sort(); foreach (Shape shape in shapes) shape.Draw(); }
對於這樣的代碼:
public int CompareTo(object obj) { if (obj is Square) { return -1; } else { return 0; } }
顯然不符合OCP。每次建立一個新的Shape類的衍生類別時,所有的CompareTo()函數都需要改動。
9.2.6 使用“資料驅動”的方法擷取封閉性
如果我們不要使Shape類的各個衍生類別之間互不知曉,可以使用表格驅動的方法。表格驅動的形狀排序機制:
/// <summary>/// This comparer will search the priorities/// hashtable for a shape‘s type. The priorities/// table defines the odering of shapes. Shapes/// that are not found precede shapes that are found./// </summary>public class ShapeComparer : IComparer{ private static Hashtable priorities = new Hashtable(); static ShapeComparer() { priorities.Add(typeof(Circle), 1); priorities.Add(typeof(Square), 2); } private int PriorityFor(Type type) { if(priorities.Contains(type)) return (int)priorities[type]; else return 0; } public int Compare(object o1, object o2) { int priority1 = PriorityFor(o1.GetType()); int priority2 = PriorityFor(o2.GetType()); return priority1.CompareTo(priority2); }}
修改DrawAllShapes方法:
public void DrawAllShapes(ArrayList shapes){ shapes.Sort(new ShapeComparer()); foreach(Shape shape in shapes) shape.Draw();}
9.3 結論
在許多方面,OCP都是物件導向設計的核心所在。遵循這個原則可以帶來物件導向技術所聲稱的巨大好處:靈活性、可重用性以及靈活性。然而,並不是說使用一種物件導向的語言就是遵循了這個原則。對於應用程式中的每個部分都肆意地進行抽象同樣不是一個好主意。正確的做法是,開發人員僅僅對程式中出現頻繁變化的那些部分作出抽象。拒絕不成熟的抽象和抽象本身一樣重要。
摘自:《敏捷式軟體開發 (Agile Software Development):原則、模式與實踐(C#版)》Robert C.Martin Micah Martin 著
轉載請註明出處:
JesseLZJ
出處:http://jesselzj.cnblogs.com
敏捷式軟體開發 (Agile Software Development):原則、模式與實踐——第9章 OCP:開放-封閉原則