軟件設計原則----開-閉原則(OCP)

設計一個模塊時,應當使該模塊在不被修改的前提下被擴展,即可在不必修改源代碼的情況下改變該模塊的行爲。

 陳述:
 軟件實體(類、模塊、函數等)應該是可以擴展的,同時還可以是不必修改的,更確切的說,函數實體應該:
(1)對擴展是開放的
當應用的需求變化時,我們可以對模塊進行擴展,使其具有滿足改變的新的行爲。即:我們可以改變模塊的功能
(2)對更改是封閉的
對模塊進行擴展時,不必改動模塊已有的源代碼或二進制代碼。


分析:

  • 世界是變化的(而且變化很快),軟件是對現實的抽象。---->軟件必須能夠擴展

  • 如果任何修改都需要改變已經存在的代碼,那麼可能導致牽一髮動全身現象,進而導致雪崩效應,使軟件質量顯著下降。

實現OCP的關鍵是抽象:

例1:既不開放也不封閉的Client:



問題:
client和server都是具體類,接口與實現沒有實現分離。如果我們想要讓client調用一個新的server類,那麼我們不得不修改client的源代碼。從而帶來編譯、鏈接、部署等一系列的問題。

class client{
server& s;
public:
client(server& SER):s(SER) {}
void useServer(){
s.ServerFunc();
}
};
class server{
int serverData;
public:
void ServerFunc();
};
修改後的設計:

  • 設計中ClientInterfece類是一個擁有抽象成員函數的抽象類。Client類使用一個抽象類,然而Client的對象卻是用Server類的派生類的對象。
  • 如果希望Client對象使用一個不同的服務器類,那麼只需從ClientInterfece類派生一個新的類,無需對Client類做任何改動。

class client{
ClientInterface& ci;
public:
client(ClientInterface &
CI):ci(CI){}
void useServer(){
ci.ServerFunc();
}
};
class ClientInterface{
virtual void ServerFunc()=0;
};
class server:public ClientInterface{
int serverData;
public:
void ServerFunc();
};
問題:
爲什麼上述的ClientInterface這個類要取這麼個名字,而不叫AbastractServer?
其實這裏面蘊含了一個思想:

——client類中更多的描述了高層的策略,而Server類中是對這些策略的一種具體實現。

  • 而接口是策略的一個組成部分,它與client端的關係更加密切。
  • ClientInterface中定義了client期望Server做什麼,而server具體類是對client這種要求的一種具體實現。
  • OCP原則要求我們清晰地區分策略和策略的具體實現形式。允許擴展具體的實現形式(開放),同時將這種擴展與策略隔離開來,使其對上層的策略透明(封閉)。

例2:

//---------shape.h-----------------
emum ShapeType{circle,square};
struct Shape{
ShapeType itsType;
};
//---------circle.h-----------------
struct Circle{
ShapeType itsType;
double itsRadius;
CPoint itscenter;
};
//---------square.h-----------------
struct Square{
ShapeType itsType;
double itsSide;
CPoint itsTopLeft;
};
//---------drawAllShapes.cpp----------
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:
s->Square();
break;
case circle:
s->DrawCircle();
break;
}
}}

例2的問題:

這個程序不符合OCP,如果需要處理的幾何圖形中再加入“三角形”將引發大量的修改。

  •  僵化的
增加Triangle會導致Shape、Square、Circle以及DrawAllShapes的重新編譯和部署
  •  脆弱的
因爲存在大量的既難以查找又難以理解的Switch和If語句,修改稍有不慎,程序就會莫明其妙的出錯
  •  牢固的
想在一個程序中複用DrawAllShapes,都必須帶上Circle、Square,即使那個程序不需要他們


例2 修改後的設計:

class Shape{
public:
virtual void Draw() const=0;
};
class Square:public Shape{
public:
virtual void Draw() const;
};
class Circle:public Shape{
public:
virtual void Draw() const;
};
void DrawAllShapes(Vector<Shape*>&
list){
vector<Shape*>::iterator i;
for(i=list.begin();i!=list.end();i++)
(*i)->Draw();
}

完全封閉了嗎?

  •  上述代碼並不完全封閉——“如果我們希望正方形在所有圓之前繪製”會怎麼樣?——對繪圖的順序無法實現封閉
  • 更糟糕的是,剛纔的設計反而成爲了實現“正方形在所有圓之前繪製”功能的障礙。

小結:

  •  一般而言,無論模塊多麼“封閉”,都會存在一些無法對之封閉的變化
             沒有對所有變化的情況都封閉的模型

  • 我們怎麼辦?
             既然不可能完全封閉,我們必須有策略的對待此問題——對模型應該封閉那類變化作出選擇,封閉最可能出現的變化
             ----這需要對領域的瞭解,豐富的經驗和常識。
            --------錯誤的判斷反而不美,因爲OCP需要額外的開銷(增加複雜度)
            ----敏捷的思想——我們預測他們,但是直到我們發現他們才行動

OCP----封裝思想的體現

對可變性的封裝原則:

  •  找到一個系統的可變因素,將之封裝起來。
  •  考慮你的設計中什麼會發生變化-------對應思路:什麼會導致設計改變

具體的:

  •  一種可變性不應該散落在代碼的很多角落裏,而應被封裝在一個對象裏。(繼承可看作封裝變化的方法。)
  •  一種可變性不應與另一種可變性混在一起。(繼承層次不應太多。)

相應設計模式:

  •  Strategy
  • Simple Factory
  • Factory Method
  • Abstract Factory
  • Builder
  • Bridge
  • Façade
  • Mediator

參考資源:

《設計模式:可複用面向對象軟件的基礎》,ERICH GAMMA RICHARD HELM RALPH JOHNSON JOHN VLISSIDES著作,李英軍 馬曉星 蔡敏 劉建中譯,機械工業出版社,2005.6

《敏捷軟件開發:原則、模式與實踐》,Robert C. Martin著,鄧輝譯,清華大學出版社,2003.9

《設計模式解析》,Alan Shalloway等著(徐言聲譯),人民郵電出版社,2006.10

發佈了90 篇原創文章 · 獲贊 110 · 訪問量 69萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章