開放—封閉原則

正如Ivar所說,“所有的系統在它的生命週期內都會改變,開發系統時期待它比第一個版本能夠持續更長的時間往往另人頭疼。”怎麼設計才能使其面對改變比較穩定並且比第一個版本持續更長時間?Bertrand Meyer在1988年就給出了指導方針即後來創造的著名的開放-封閉原則。“軟件實體(類、模塊、函數等)應該對於擴展開放,但對於修改封閉。

        當對程序進行一個單一個改動會導致它所依賴的模型的一系列的改變,這種程序就是我們不想要的“壞”設計。程序變得脆弱、死板、不可預知和不能重用。開放-封閉原則通過最直接的方法改變這種狀況。它這樣描述:你應該設計永遠不再必修的模塊,當需求改變時,你通過添加新的代碼擴展這些模塊的行爲,而不是改變已經在使用的舊代碼。

   描述:

   遵守開放-封閉原則的模塊有兩個主要的屬性。

  1. 它們對擴展開放。這意味着模塊的行爲可以擴展。當需求改變時我們可以通過多種方法創建模塊的行爲。
  2. 它們對修改封閉。模塊的源代碼是不可以訪問的。不允許任何人去修改代碼。

這兩個屬性看起來並不一致。通常情況下擴展一個模塊的行爲需要修改這個模塊。一個模塊通常有固定的行爲通常情況下不能修改。怎麼解決這兩個屬性不一致的問題呢?

  抽象是關鍵

在C++中,利用面向對象的設計,它可以設計一個固定的抽象代表一組未確定的可能的操作。這裏的抽象是抽象的基類,未確定的操作組由子類來實現。模塊可以操作一個抽象。這樣的模塊可以對修改封閉因爲它依賴於一個固定的抽象,通過創建新的子類可以擴展它的行爲。

如圖1所示爲一個簡單的沒有遵守開放-封閉原則的設計。Client 和 Server類都是實體類。Client類使用Server類。如果我們希望一個Client對象使用不同的服務器對象,這個Client類需要修改去創建新的服務器類。

圖2顯示了對應的遵守開放-封閉原則的設計。此時AbstractServer類是個擁有純虛函數的虛類。Client類使用這個虛類。然後Client對象將使用Server類的子類。如果我們想要Client對象使用一個不同的server類,可以新建一個AbstractServer類的子類,Client類可以不用修改。

形狀抽象

  考慮下面的例子,我們有一個應用需要在標準GUI上畫圓形和方形。這個圓形和方形在特定的指令下被畫出。通過合適的指令需要一組圓和方形,程序遍歷執行指令畫出每個圓形和方形。用面向過程的技術沒有遵守開放-封閉原則,我們將解決這個問題如下面的程序所示,我們看到一組擁有相同的第一個元素的數據結構,但其它元素不同。第一個元素是個類型,用來判斷它是圓形還是方形。函數DrawAllShapes遍歷指向這些數據結構的指針的數組,檢查它的類型並調用相應的函數。

Enum ShapeType {circle, square};

Struct shape

{

       ShapType itsType;

};

Struct Circle

{

       ShapeType itsType;

       Double itsRadius;

       Point itsCenter;

};

Struct Square

{

       ShapeType itsType;

       Double itsSide;

       Point itsTopLeft;

};

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:

           DrawSqure((struct Square*)s);

       Break;

       Case circle:

              DrawCircle((struct Circle*)s);

Break;

}

}

}

       函數DrawAllShapes沒有遵守開放-封閉原則,因爲它沒有對添加新的形狀封閉。如果我想擴展函數使它畫出一組形狀,其中包括三角形,那麼我需要修改函數。實際上每次新增加一個形狀都需要修改這個函數。

       上面的程序中是一個簡單的例子。在實際應用中DrawAllShapes中的switch分支會被各種各樣的函數重複使用。每一個所做的事僅有一點不相同。如果添加一個新的形狀意味着查找每一個switch 語句並且添加新的形狀。而且其它的switch語句分支可能並不會像在DrawAllShapes裏一樣比較清楚的排列。於是當添加一個一個形狀時查找和理解每一個switch會帶來無窮的繁瑣。

       下面的代碼是遵守開放-封閉原則的一個方案。這種方法有個抽象的Shape類被創建,這個抽象類只有一個純虛函數Draw。圓形和方形兩個類都繼承於Shape類。

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(Set<shape*>& list)

{

       for(Iterator<Shape*>i(list); i; i++)

       (*i)->Draw();

}

在這段程序中如果我們想擴展DrawAllShapes函數來畫一種新的圖形,這個函數不需要修改,因此它遵守了開放-封閉原則。它的行爲可以被擴展而不需要對它進行修改。在現實世界中Shape類可能有多個方法,然而嚮應用中添加一個新的形狀仍然是件很簡單的事。因爲需要做的就是創建一個新的繼承類並且實現所有的函數,不再需要在應用中到處尋找看哪些地方需要修改。

       選擇性封閉

一般來說無論程序多麼優秀也難做到100%的封閉。例如我們的程序二中的DrawAllShapes函數,如果要求所有的圓形應該在方形之前被畫出。此時這個函數對這類改變不再封閉。通常情況下,無論一個模塊多麼封閉,總是無法對一些改變封閉。既然無法做到完全的封閉,可以進行選擇性封閉。即設計者需要選擇程序可以對哪些需求改變封閉。這需要通過經驗得來的預見性。有經驗的設計者對用戶和行業足夠了解,能夠判斷每種需求改變的可能性。所以他可以確保程序對大多數需求改變封閉。

通過抽象獲得明確的封閉。

我們怎麼可以使DrawAllShapes函數對要求改變畫圖的順序封閉?記住封閉是基於抽象的。因此爲了讓它對順序封閉,我們需要“順序抽象”。這個特定的順序與在其它形狀前畫特定類型的形狀有關。順序規則意味着,給定任何兩個對象,它能夠發現先畫哪個。因此我們可以給Shape定義一個方法Precedes,它的參數爲另外一種形狀並且返回一個bool類型。在C++中這個函數可以通過重載操作符“<”來表示。下面的程序顯示了添加了順序方法的Shape類。

class Shape

{

       public:

         virtual void Draw() const = 0;

         virtual bool Precedes(const Shape&) const = 0;

         bool operator<(const shape& s){return Precedes(s);}

};

現在我們有了方法來檢查兩個對象的順序,我們可以對它們排序然後再畫它們。下面的程序是C++的實現。

void DrawAllShapes(Set<Shape*>& list)

{

       OrderedSet<Shape*> orderdList = list;

       orderedList.Sort();

       for(Iterator<Shape*> i(orderedList); i; i++)

       (*i)->Draw();

}

它給了我們一種給對象排序的方法,並且以合適的順序畫出它們。但我們仍然沒有一個像樣的排序抽象。一個圖形通過重載Precedes方法實現對特定的圖形排序,它是怎麼實現的呢?下面的代碼實現了Circle::Precedes保證圓形在方形之前畫。

bool Circle::Precedes(const Shape& s) const

{

    if (dynamic_cast<square*>(s))

       return true;

      else

              return false;

}

很明顯這個函數並不遵守開放-封閉原則。它沒辦法對新添加一種圖形封閉。每次添加一個新的圖形,這個函數就需要修改。

通過“數據驅動”的方法實現封閉

Shape的繼承類可以通過一個表驅動的方法使不用修改每一個繼承類來實現封閉性。下面的代碼顯示了一種可能的實現方法。通過這種方法我們可以成功地實現DrawAllShapes函數對順序問題和每個Shape的繼承類對新添加一個圖形的封閉性。

class Shape

{

       public:

              virtual void Draw() const = 0;

              virtual bool Precedes(const Shape&) const;

              bool operator<(const Shape& s) const{return Precedes(s);}

       private:

              static char* typeOrderTable[];

}

static char* typeOrderTable[]=

{

           “Circle”,

              “Square”,

0

};

bool Shape::Precedes(const Shape& s) const

{

     // cal the result by look up the table

}

上面的實現唯一對順序沒有實現封閉的是表本身。然而這個表可以放在它自己的模塊內,與其它模塊相分離,這樣它的修改就不會影響到其它的模塊了。

探索與約定

正如前面所說,開放-封閉原則是面向對象思想的一些探索和約定的最初的動機,下面是一些比較重要的。

使所有成員變量爲私有

這是面向對象編程最基本的一條。成員變量應該只能被成員方法所見,它不應被其它類所見,即使繼承類。因此它們應該被聲明爲private,而不是public或protected。

根據開放-封閉原則,這條約定的原因很清楚。當類的成員變量改變了,每一個依賴於這些成員的函數都需要修改。在面向對象設計中,我們認爲成員方法對成員變量的改變不應該封閉。但是其它類包括子類都應該它具有封閉性。這種要求我們叫做“封裝”。

現在如果我們有一個成員變量,我們永遠不會去改變它,那麼它還需要聲明爲private嗎?是不是可以把它聲明爲private,以便讓客戶程序更容易訪問?如果這個變量確實不會被改變,而且客戶程序也遵守約定不會去改變它,那麼把它聲明爲public完全沒有問題,但是我們無法保證不會有客戶程序不小心去改變它,如果這樣的話其它所有依賴它的客戶程序就會出錯。

不要使用全局變量

它與不使用public成員變量類似。任何模塊使用了全局變量都無法對其它可以對全局模塊進行寫操作的模塊封閉。任何一個模塊沒有按其它模塊期待的方式使用全局變量,都會影響到其它模塊。很多模塊都會受到某個突發奇想的操作的影響是比較危險的。

另一方面,如果一個全局變量沒有太多的依賴,而且不會用不一致的方式使用,它不會有什麼危害。設計者在使用全局變量前要評估它對封閉性帶來的影響和它對程序帶來的方便性。選擇性的使用全局變量通常很廉價。

運行時變量是危險的

通常情況下認爲使用dynamic_cast或任何依賴於運行時的行爲都是比較危險的。下面兩段程序第一段違反了開放-封閉原則,而第二段使用了dynamic_cast,但並沒有違反開放-封閉原則。做爲通常原則,如果依賴運行時沒有違反開放-封閉原則,它是安全的。

class Shape{};

class Square:public Shape

{

     private:

            friend DrawSquare(Square*);

};

class Circle:public Shape

{

     private:

     friend DrawCircle(Circle*);

};

void DrawAllShapes(Set<Shape*>& ss)

{

     for(Iterator<shape*>i(ss); i; i++)

     {

            Circle* c = dynamic_cast<Circle*>(*i);

            Square* s = dynamic_cast<Square*>(*i);

            if(c)

                   DrawCircle(c);

            else if (s)

                   DrawSquare(s);

}

}

class Shape

{

     public:

            virtual void Draw() const = 0;

};

class Square : public Shape

{

}

void DrawSquaresOnly(Set<Shape*>& ss)

{

     for(Iterator<shape*>i(ss); i; i++)

{

            Square* s = dynamic_cast<Square*>(*i);

            if(s)

                   s->Draw();

}

}

   結論

    關於開放-封閉原則有很多可以講的。很多情況下這條原則是面向對象的核心。遵守這條原則可以最大程序獲得面向對象技術帶來的好處。遵守這條原則並不僅僅表示使用面向對象的語言,而是它要求設計師認爲容易被改變的部分使用抽象技術。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章