對象的完整性

對象的完整性

 

 

對象是OOP的基本單元,由於維護一個對象需要很大的代價,所以設計一個對象也需要謹慎。

按照中國教科書的習慣,一般要把這個問題分解爲對象的合理性、正確性和完整性。在這裏我不想把人搞糊塗也不想把我搞糊塗,我只是提對象的完整性。當然也借鑑了牛人布魯克斯的術語,他在《人月神話》裏對系統概念的完整性推崇倍至。

對象的完整性,從正面的角度來說,就是指對象的函數接口是完備的;從反面的角度來說,就是不能殘缺;從用戶的角度來說,就是不能缺少某些函數接口而不能進行合理的操作。總而言之,一個完整的對象應該是合理的、正確的、完備的,能夠讓你完成你希望從這個對象得到的任何合理的操作。

以上都是廢話,核心的問題是,如何讓你設計的對象滿足完整性。

我想從兩個方面說這個問題,一個說從思想層面上,一個是從工具層面上。

1.     對象的完整性的意義

對象是對現實世界物質的抽象,應該反映物質的屬性,反映物質的本質屬性應該成爲對象的成員函數(這裏所說的成員函數都是非靜態的成員函數,以下同)。例如對於一個長方形(rectangle)對象,有長、寬、面積、對角線的長度,這些都應該成爲rectangle對象的成員函數。

物質本身的屬性成爲對象的成員函數,一般人們沒有異議。但是對於物質之間的關係是否應該成爲對象的成員函數,存在一些不同的看法。例如對於Rectangle類,兩個Rectangle類對象之間的包含關係,Rectangle對象和點(Point)對象之間的包含關係,可以設計爲成員函數:

class Rectangle

{

///////……………

bool contain(const Rectangle& other) const

{

// code here implement here

}

bool contain(const Point& pt) const

{

// code here implement here

}

/////……………..

};

當然也可以設計爲全局函數:

bool contain(const Rectangle& a, const Rectangle& b)

{

// code here implement here

}

bool contain(const Rectangle& rect, const Point& pt)

{

// code here implement here

}

大部分的人認爲設計爲成員函數是更好的選擇,因爲作爲成員函數使用似乎更加符合面向對象的精神,而使用全局函數則似乎返回到了遙遠的面向過程設計的年代。但是我至少有兩個理由認爲全局函數是更好的選擇:

1.         對於異質對象之間的關係來說,成員函數的歸屬沒有必然的邏輯根據。例如對於RectanglePoint對象來說,contain關係的實現放在哪個類定義裏面呢?你可以選擇其中的一個,也可以選擇全部,但是這樣作都是設計者的主觀選擇,沒有必然的邏輯根據。全局函數沒有這個問題。

2.         如果物質之間的關係很多,會導致類對象定義的成員函數過多(有的一個Date類竟然定義了60多個的成員函數),成員函數過多會導致用戶理解困難,設計者維護困難。這是因爲成員函數一般會訪問私有數據,而一旦私有數據的形式變動,那麼大量的成員函數需要全部更改,維護起來十分的困難。全局函數一般通過成員函數訪問類的私有數據,維護起來相對容易;而且全局函數會顯著的減少成員函數的數量(一般不超過20個),用戶的理解也比較容易。

所以你看,全局函數也有自己的優點。

因爲兩種方式都有各自的優劣,所以選擇起來就有一定的猶豫。我得一般原則是:同質關係放在成員函數,異質關係設計爲全局函數;儘量保持成員函數的數目不超過20。當然這是一般的原則,也有特殊的情況,這要設計者自己把握了。

當我們使用物質本身的屬性和物質之間的關係設計類的時候,如果能夠把物質的屬性和關係抽象完備,那麼類設計也就完備了。有的一些類並不對應與現實世界的物質,而是一些抽象的概念,例如容器類,這就需要更加謹慎的抽象類的屬性和關係了。

從思想的層面上說,還可以從另外的一個角度說明問題。在artima網站2003年採訪Bjarne Stroustrup的時候,有過這樣的一句話:The functions that are taking any responsibility for maintaining the invariant should be in the class,意思是有責任維護類的不變性的函數應該成爲類的接口。不變性歸根結底也是物質的屬性(本身屬性或者關係屬性),是此物質區別於彼物質的標示,是維持物質內部的合理狀態。

維持類的合理狀態就是類的不變性,這個解釋可能更容易理解。比如一個Rectangle類對象,它的不變性就是長、寬大於0,面積是長寬的乘積等等,如果違反了這些不變性,就破壞了類內部的合理狀態,類就不能稱其爲類了。所以Rectangle通過成員函數讓你修改它的長寬,並且在成員函數中檢查參數的範圍,維護類的不變性。

從另外一個角度來說,如果一個類的成員變量的值可以爲任意的,那麼就沒有必要把這個物質抽象爲類,你可以把它抽象爲struct。所以Bjarne Stroustrup說:I particularly dislike classes with a lot of get and set functions.。這樣的類基本上就意味着它是一個struct

類本身的屬性,類之間的關係;或者說類的不變性,是保證一個類成員函數完備的基礎。思想深刻的牛人或許不需要驗證就可以說他的類設計是完整的;但是對於吾輩之芸芸衆生,則需要一定的手段來保證和驗證類的完整性,着就需要我們從工具層面上說起。

2.     對象完整性的工具驗證

我們保證對象完整性的工具就是:測試。

先從例子出發,還是Rectangle

class Rectangle

{

     double width_,height_;

public:

     Rectangle(double w,double h)

         :width_(0),height_(0)

     {

         setWidth(w);

         setHeight(h);

     }

     double getWidth() const

     {

         return width_;

     }

     void setWidth(double w)

     {

         if(w > 0){

              width_ = w;

         }

     }

     double getHeight() const

     {

         return height_;

     }

     void setHeight(double h)

     {

         if(h > 0){

              height_ = h;

         }

     }

};

測試的時候,很容易需要測試Rectangle的面積:

void test()

{

     Rectangle rect(3,4);

     assertEqual(rect.getWidth() == 3);

     assertEqual(rect.getHeight() == 4);

     /////...

     assertEqual(rect.getArea() == 12);

}

很顯然需要爲Rectangle補充一個求面積的函數:

class Rectangle

{   

     /////...

     double getArea() const

     {

          return width_ * height_;

     }

};

隨着測試的繼續進行,Rectangle之間的關係測試也會出現,從而也需要把相關的函數添加進去,對象的完整性就會逐漸的得到滿足。當你感覺沒有更多測試的時候,對象的完整性基本就得到保證了。

“這怎麼看起來象TDD?”不錯,是和TDD很像。不過TDD的設計者有他們的出發點:編寫整潔可用的代碼(clean code that works),而我這裏的出發點對象的完整性,殊途同歸吧:)

 

 

 

 

 

 

 

 

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