軟件設(shè)計(jì)原則之開閉原則
如果模塊仍可擴(kuò)展,則稱其為打開狀態(tài)。例如,應(yīng)該可以向其包含的數(shù)據(jù)結(jié)構(gòu)添加字段,或者向其執(zhí)行的功能集添加新元素。
SOLID設(shè)計(jì):開閉原則(OCP)
開閉原理(OCP)是眾所周知的SOLID縮寫詞中的O。
伯特蘭·邁耶(Bertrand Meyer)曾因創(chuàng)造了開放/封閉原則一詞而廣受贊譽(yù),該原則出現(xiàn)在1988年的《面向?qū)ο蟮能浖?gòu)造》一書中。它的原始定義是
如果模塊仍可擴(kuò)展,則稱其為打開狀態(tài)。例如,應(yīng)該可以向其包含的數(shù)據(jù)結(jié)構(gòu)添加字段,或者向其執(zhí)行的功能集添加新元素。
如果某個(gè)模塊可供其他模塊使用,則將其稱為已關(guān)閉。假設(shè)已為模塊提供了良好定義的穩(wěn)定描述(信息隱藏意義上的接口)
根據(jù)這些定義,通常以這種方式表達(dá)和總結(jié)原理:
模塊應(yīng)該打開以進(jìn)行擴(kuò)展,而關(guān)閉則可以進(jìn)行修改。
關(guān)于這個(gè)簡(jiǎn)單定義的含義及其對(duì)在現(xiàn)實(shí)世界中使用面向?qū)ο缶幊蹋∣OP)的含義的爭(zhēng)論已經(jīng)(并且仍然有很多)。
在這篇文章中,我將盡量具體而簡(jiǎn)潔。
解釋OCP的經(jīng)典代碼示例
可以使用以下方式在C#中翻譯解釋OCP的經(jīng)典代碼示例:
public class Circle { }
public class Square { }
public static class Drawer {
public static void DrawShapes(IEnumerable<object> shapes) {
foreach (object shape in shapes) {
if (shape is Circle) {
DrawCircle(shape as Circle);
} else if (shape is Square) {
DrawSquare(shape as Square);
}
}
}
private static void DrawCircle(Circle circle) { /*Draw circle*/ }
private static void DrawSquare(Square square) { /*Draw Square*/ }
}
public class Circle { }
public class Square { }
public static class Drawer {
public static void DrawShapes(IEnumerable<object> shapes) {
foreach (object shape in shapes) {
if (shape is Circle) {
DrawCircle(shape as Circle);
} else if (shape is Square) {
DrawSquare(shape as Square);
}
}
}
private static void DrawCircle(Circle circle) { /*Draw circle*/ }
private static void DrawSquare(Square square) { /*Draw Square*/ }
}
我覺得這個(gè)例子有點(diǎn)毛骨悚然。我相信人們一定不知道編寫這樣的代碼是什么OOP。它可以明顯地進(jìn)行重構(gòu),以這樣的:
public interface IShape { void Draw(); }
public class Circle : IShape { public void Draw() { /*Draw circle*/ }}
public class Square : IShape { public void Draw() { /*Draw Square*/ } }
public static class Drawer {
public static void DrawShapes(IEnumerable<IShape> shapes) {
foreach (IShape shape in shapes) {
shape.Draw();
}
}
}
public interface IShape { void Draw(); }
public class Circle : IShape { public void Draw() { /*Draw circle*/ }}
public class Square : IShape { public void Draw() { /*Draw Square*/ } }
public static class Drawer {
public static void DrawShapes(IEnumerable<IShape> shapes) {
foreach (IShape shape in shapes) {
shape.Draw();
}
}
}
讓我們看一下這種重構(gòu)的含義:
我們引入了一個(gè)新的抽象IShape 來表示我們已經(jīng)想到的一個(gè)概念。實(shí)際上,在第一個(gè)代碼示例中,方法DrawShapes()已經(jīng)接受了一系列形狀。隨著新IShape的抽象我們的設(shè)計(jì)現(xiàn)在是開放,接受更多的形狀,例如三角形或Pentagone。
DrawShapes()方法將繪制任何新形狀,而無需修改。換句話說,DrawShapes()方法的實(shí)現(xiàn)已關(guān)閉。
這是通常顯示OCP的方式。這一切都是關(guān)于通過以下方式預(yù)測(cè)系統(tǒng)的未來變化:
發(fā)生更改時(shí),現(xiàn)有代碼保持不變。此處的現(xiàn)有代碼是DrawShapes()具體方法主體,IShape接口以及Circle和Square類。
發(fā)生更改時(shí),用于實(shí)現(xiàn)更改的新代碼將寫入實(shí)現(xiàn)現(xiàn)有抽象的新類中。這里的新類可能是Triangle和Pentagone。
變化點(diǎn)原理
查看OCP的另一種方法是變化點(diǎn)(PV)原則,其中指出:
確定預(yù)測(cè)變化的點(diǎn),并在它們周圍創(chuàng)建穩(wěn)定的界面。
我發(fā)現(xiàn)PV比OCP更可理解,因?yàn)樗强尚械?。首先確定潛在的變化,然后圍繞這些變化構(gòu)建適當(dāng)?shù)某橄蟆?/span>
真正的挑戰(zhàn):期待
但是真正的挑戰(zhàn)是期待,期待很難。如果預(yù)期容易,那么我們將成為比特幣的億萬富翁。在現(xiàn)實(shí)世界中,當(dāng)您預(yù)計(jì)到的風(fēng)險(xiǎn)很高時(shí):
我們確實(shí)預(yù)期會(huì)有變化。這就是YAGNI原則所說的(您將不需要它):始終在真正需要它們時(shí)執(zhí)行這些事情,永遠(yuǎn)不要在僅僅預(yù)見到需要它們時(shí)才執(zhí)行。開發(fā)和維護(hù)抽象是有代價(jià)的,如果我們不需要它,則這是負(fù)數(shù)。
我們無法預(yù)期實(shí)際需要的變化的風(fēng)險(xiǎn)也很高。但是一旦變體需求成為現(xiàn)實(shí),這就是開發(fā)人員的責(zé)任,即重構(gòu)并創(chuàng)建正確的抽象以及將對(duì)這些抽象起作用的正確的穩(wěn)定代碼。這是一次愚弄我,不要愚弄我兩次的想法:我不應(yīng)該預(yù)見到我需要的東西,但是我應(yīng)該確定然后在需要時(shí)編寫正確的抽象。
現(xiàn)實(shí)世界中的OCP
這是一種實(shí)用的OCP方法:
無論如何,KISS原則都適用(保持簡(jiǎn)單愚蠢):不要低估預(yù)期的難度,不要浪費(fèi)資源來創(chuàng)建不需要的抽象。
編寫自動(dòng)測(cè)試:編寫測(cè)試的最大好處之一是,有一陣子,您必須從客戶端的角度來看代碼。如果您的代碼包含某些難以通過測(cè)試覆蓋的區(qū)域,則肯定意味著您的代碼應(yīng)進(jìn)行重構(gòu)以輕松進(jìn)行100%可測(cè)試。經(jīng)驗(yàn)表明,從可測(cè)試性差的代碼重構(gòu)為可完全測(cè)試的代碼時(shí),自然會(huì)出現(xiàn)對(duì)正確抽象的需求。
一些靜態(tài)分析器可以幫助您查明典型的OCP違規(guī)情況:
當(dāng)向下轉(zhuǎn)換引用(即從基類或接口到子類或葉類的轉(zhuǎn)換)時(shí),
使用is或as運(yùn)算符時(shí)(如上面的第一個(gè)示例)。
NDepend的規(guī)則基類不應(yīng)使用派生類:此規(guī)則的匹配明顯違反了OCP。
請(qǐng)記住一次欺騙我,不要兩次欺騙我。一旦確定需要抽象一些概念,就必須重構(gòu)代碼。當(dāng)然有時(shí)候,如果大量的客戶端代碼依賴于您的API是不可能的:在這種情況下,您不能輕松地進(jìn)行重構(gòu),并且常常必須忍受錯(cuò)誤的設(shè)計(jì)。這就是為什么公共API設(shè)計(jì)如此敏感的主題的原因:您別無選擇,只能盡最大的努力去預(yù)期并接受過去的設(shè)計(jì)錯(cuò)誤。
可以使用“訪客”模式對(duì)多個(gè)變體開放
最后,我們強(qiáng)調(diào)一下,在現(xiàn)實(shí)世界中,數(shù)據(jù)對(duì)象(如此處的形狀)不會(huì)實(shí)現(xiàn)自身的算法(例如繪圖)。經(jīng)驗(yàn)告訴我們,這明顯違反了OCP,因?yàn)楫?dāng)對(duì)數(shù)據(jù)對(duì)象需要一種新算法時(shí),例如在圖形中添加持久性后,必須再次修改所有形狀類。這也違反了單一職責(zé)原則(SRP,即SOLID中的S),因?yàn)樾螤铑惉F(xiàn)在具有兩個(gè)職責(zé):1)保存形狀數(shù)據(jù)2)繪制形狀。
因此,我們現(xiàn)在有兩種變體:我們需要一種抽象形狀和應(yīng)用于形狀的算法的方法,以便編寫諸如algorithm.ApplyOn(shape)之類的東西。這兩個(gè)抽象類型的呼叫被命名為雙調(diào)度呼叫:真正調(diào)用的實(shí)現(xiàn)既取決于IShape的對(duì)象的類型和IAlgorithm對(duì)象的類型。如果您有N個(gè)形狀和M個(gè)算法,則需要[N x M]個(gè)實(shí)現(xiàn)。
幸運(yùn)的是,訪客模式有助于實(shí)現(xiàn)雙重調(diào)度。具有新的持久性算法的代碼將如下所示:
//
// Shapes elements
//
public interface IShape { void Accept(IVisitor visitor); }
public class Circle : IShape {
public void Accept(IVisitor visitor) { visitor.Visit(this); }
}
public class Square : IShape {
public void Accept(IVisitor visitor) { visitor.Visit(this); }
}
//
// Visitors algorithms on shapes elements
// don't use the IAlgorithm terminology to keep up with the classical visitor pattern terminology
//
public interface IVisitor {
void Visit(Circle circle);
void Visit(Square square);
}
public class DrawAlgorithm : IVisitor {
public void Visit(Circle circle) { /*Draw circle*/}
public void Visit(Square square) { /*Draw square*/}
}
public class PersistAlgorithm : IVisitor {
public void Visit(Circle circle) { /*Persist circle*/}
public void Visit(Square square) { /*Persist square*/}
}
public static class Program {
public static void ApplyVisitorAlgorithmOnShapesElements(IEnumerable<IShape> shapes, IVisitor visitor) {
foreach (IShape shape in shapes) {
// Double dispatching:
// shape can be both: Circle or Square
// visitor can be both Draw or Persist
shape.Accept(visitor);
}
}
}
//
// Shapes elements
//
public interface IShape { void Accept(IVisitor visitor); }
public class Circle : IShape {
public void Accept(IVisitor visitor) { visitor.Visit(this); }
}
public class Square : IShape {
public void Accept(IVisitor visitor) { visitor.Visit(this); }
}
//
// Visitors algorithms on shapes elements
// don't use the IAlgorithm terminology to keep up with the classical visitor pattern terminology
//
public interface IVisitor {
void Visit(Circle circle);
void Visit(Square square);
}
public class DrawAlgorithm : IVisitor {
public void Visit(Circle circle) { /*Draw circle*/}
public void Visit(Square square) { /*Draw square*/}
}
public class PersistAlgorithm : IVisitor {
public void Visit(Circle circle) { /*Persist circle*/}
public void Visit(Square square) { /*Persist square*/}
}
public static class Program {
public static void ApplyVisitorAlgorithmOnShapesElements(IEnumerable<IShape> shapes, IVisitor visitor) {
foreach (IShape shape in shapes) {
// Double dispatching:
// shape can be both: Circle or Square
// visitor can be both Draw or Persist
shape.Accept(visitor);
}
}
}
