對象的完整性
對象是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. 對於異質對象之間的關係來說,成員函數的歸屬沒有必然的邏輯根據。例如對於Rectangle和Point對象來說,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),而我這裏的出發點對象的完整性,殊途同歸吧:)