1. 程式人生 > >c++ 面向物件類設計五項基本原則

c++ 面向物件類設計五項基本原則

類設計五項基本原則
原則:

單一職責原則
開放封閉原則
Liskov替換原則
依賴倒置原則
介面隔離原則

第8章 單一職責原則 ( SRP )

就一個類而言.應該僅有一個引起它變化的原因.

  一個class就其整體應該只提供單一的服務 如果一個class提供多樣的服務,那麼就應該把它拆分,反之,如果一個在概念上單一的功能卻由幾個class負責,這幾個class應該合併

第9章 開放-封閉原則 ( OCP )

軟體實體(類. 模組. 函式等等)應該是可以擴充套件的. 但是不可修改的.
例如.把一個類的功能抽象出來.形成一個抽象介面.然後對該介面程式設計.這樣當需要擴充套件時只要從該介面派生一個

新類就可以完成擴充套件的功能.

看一個例子: 一個儲存形狀的連結串列. 列印其中的每個形狀. 形狀可能是圓.可能是矩形.要求先列印所有的圓形.

第一種方案:
struct 形狀{
  bool  是否圓形?
  //其他資料
};
形狀 [N] ; //陣列;
for(i=0; i<N; ++i){
   if ( 是圓形 ) 繪製圓形.
   else  繪製矩形;
}
這就是一個糟糕的設計. 如果增加第三種圖形如三角形. 改動是很麻煩的.

第2種方案:
struct 圖形 { 
   virtual void draw() = 0;
};

struct 圓形 : public 圖形{
   void draw() { 繪製圓 }
   //...
};

struct 矩形 : public 圖形{
   void draw() { 繪製矩形 }
   //...
};

vector<圖形*> v;
foreach(v.begin(), v.end(), mem_fun(&圖形::draw) );
這種設計 . 如果要增加三角形的種類. 只要從圖形類派生就可以了. 繪製部分不用改動.

第三種設計:
前兩種設計都暫時沒有考慮"先輸出圓形"這個要求.
我們為vector<圖形*> 排序設計了一個比較圖形*的函式:
struct 圖形{
   bool bijiao(const 圖形& s) const {
       if (dynamic_cast<矩形*>(&s) ) return true;
       else return false;
   }
   //..
};
這個bijiao的函式經過包裝就可以用在sort()中對vector<圖形*>排序.
但...這個函式不具有封閉性.如果增加一個三角形類. 這個函式還要改動..

另一種設計:
將bijiao函式使用"表格驅動"的方法.獲得排序功能的封閉性:

class Shape {
public:
   virtual void draw() const = 0;
   bool bijiao(const Shape&) const;
private:
   static const char* typeOrderTable[];
};
const char* Shape::typeOrderTable[] = {
    typeid(Circle).name(), 
    typeid(Square).name(),
    0
};
然後在bijiao函式中. 根據 typeid(*this).name() 和 typeid(s).name()在表格中的位置.來比較.
這樣如果增加了三角形. 想調整輸出的順序為 先三角形. 再圓. 再矩形.  只要新增三角形類. 
並修改型別名錶格就可以了.

 一個設計並實現好的class,應該對擴充的動作開放,而對修改的動作封閉 也就是說,這個class應該是允許擴充的,但不允許修改 如果需要功能上的擴充,一般來說應該通過新增新類實現,而不是修改原類的程式碼 新增新類不單可以通過直接繼承,也可以通過組合


第10章 Liskov 替換原則  ( LSP )

Barbara Liskov說: 所有針對基類編的程式. 在用派生類替換後. 程式的行為不變.
違反這一規則的例子是 : 讓正方形從矩形派生. 
雖然正方形看起來 IsA 矩形. 但它們的行為不是. 例如: 
class Rectangle { //矩形
    void setwidth( double );    //這兩個函式對正方形來說. 行為和矩形不同.
    void setheight( double ); 
    //...
};
這就是一個違法LSP規則的設計. 因為把正方形作為矩形的派生類. 但它沒有"可替換性".


第11章 依賴倒置原則 ( DIP )

高層模組不應該依賴於低層模組. 二者都應該依賴於抽象.
抽象不應該依賴於細節. 細節應該依賴於抽象.

為什麼叫"倒置"呢. 因為在結構化分析和設計的傳統開發方法裡.經常是高層依賴低層模組.而這在
面向物件設計時是糟糕的. 因為那意味著對低層模組的改動會直接影響到高層模組.從而迫使高層整個
作出改動.

高層模組不能依賴於低層模組. 這是"框架設計"時的核心原則.

"高層模組應該依賴於抽象介面.而不應該依賴於具體類." 根據這一規則:
任何變數都不應該持有一個指向具體類的指標或引用.
任何類都不應該從具體類派生.
任何方法都不應該覆寫它的任何基類中已經實現了的方法.

事實上. 對於象Java中的String這樣低層的類. 高層的模組可以依賴它. 因為它是穩定的.

例如: 有個按鈕類 (Button) 控制 燈類 (Lamp) 的開啟(turnOn)和關閉(trunOff).
糟糕的設計(違反了DIP) :
public class Button {
    private Lamp itsLame;    //依賴具體的類 Lamp
    public void poll() {
        if (...) itsLamp.turnOn();
    }

上邊的Button類依賴於低層的 Lamp 類. 要解除對Lamp的依賴. 我們抽象出一個抽象介面:
interface ButtonServer; 然後Button只操作ButtonServer介面. 而讓Lamp類從該
介面派生.


第12章 介面隔離原則 (ISP)

有些物件.它們的介面不是內聚的. ISP建議將它們分為多個具有內聚介面的抽象基類.

例如: 有個Door物件. 它可以被鎖和被解鎖. 如:
class Door {
public: 
    virtual void Lock() = 0;
    virtual void Unlock() = 0;
    virtual bool IsDoorOpen() = 0;
};
現在需要一個 TimedDoor 類. 它會在門開啟一定時間後發出警報聲. 自動提醒關門.
現在有個 Timer 類:
class Timer {
public:
    //向Timer 物件 註冊一個 TimerClient物件. 當指定的時間timeout到達時. 它自動
    //向TimerClient物件傳送 TimeOut() 訊息;
    void Register( int timeout, TimerClient* client);
};
class TimerClient{
public:
    virtual void TimeOut() = 0;
};

現在設計一個TimedDoor 類. 下邊是個糟糕的設計:
讓 Door 介面 繼承自 TimerClient . 這樣Door就有了 TimerOut()純虛擬函式.
然後讓 TimedDoor 類 實現 Door 介面. 就可以將 TimedDoor物件向Timer物件註冊.

這個設計有個問題. 讓 Door 介面 繼承自 TimerClient . 但並不是所有的Door都要有定時功能.
所以在這個設計中. Door的介面變"胖" . 被 TimeOut()函式 汙染了 .

解決的辦法是分離介面. 下邊有兩種辦法:

1. 使用委託分離介面:
建立一個派生自TimerClient的物件. 並把對該物件的請求委託給TimedDoor. 如:
class TimedDoor : public Door {
public:
    virtual void DoorTimeOut ( );
};

class DoorTimeAdapter : public TimerClient {
public:
    DoorTimerAdapter( TimedDoor& theDoor) : itsTimedDoor(theDoor) {}
    virtual void TimeOut() {
        itsTimedDoor.DoorTimeOut();
    }
private:
    TimedDoor& itsTimedDoor;
}; 

這樣. 如果有:
TimedDoor  td; 
並有個Timer 的物件 tm . 就可以用委託類 DoorTimeAdapter 來註冊 :
tm.Register( new DoorTimeAdapter(td) );

2. 使用多繼承分離介面
使TimedDoor繼承自 Door 和 TimerClient :
class TimeDoor : public Door, public TimerClient {
public:
    virtual void DoorTimeOut();
};