对象的完整性
对象是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 @H_227_403@@H_639_404@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:
@H_828_502@class Rectangle @H_828_502@{ @H_828_502@ double width_,height_; @H_828_502@public: @H_828_502@ Rectangle(double w,double h) @H_828_502@ :width_(0),height_(0) @H_828_502@ { @H_828_502@ setWidth(w); @H_828_502@ setHeight(h); @H_828_502@ } @H_828_502@ double getWidth() const @H_828_502@ { @H_828_502@ return width_; @H_828_502@ } @H_828_502@ void setWidth(double w) @H_828_502@ { @H_828_502@ if(w > 0){ @H_828_502@ width_ = w; @H_828_502@ } @H_828_502@ } @H_828_502@ double getHeight() const @H_828_502@ { @H_828_502@ return height_; @H_828_502@ } @H_828_502@ void setHeight(double h) @H_828_502@ { @H_828_502@ if(h > 0){ @H_828_502@ height_ = h; @H_828_502@ } @H_828_502@ } @H_828_502@};测试的时候,很容易需要测试Rectangle的面积:
@H_828_502@void test() @H_828_502@{ @H_828_502@ Rectangle rect(3,4); @H_828_502@ assertEqual(rect.getWidth() == 3); @H_828_502@ assertEqual(rect.getHeight() == 4); @H_828_502@ /////... @H_828_502@ assertEqual(rect.getArea() == 12); @H_828_502@}很显然需要为Rectangle补充一个求面积的函数:
@H_828_502@class Rectangle @H_828_502@{ @H_828_502@ /////... @H_828_502@ double getArea() const @H_828_502@ { @H_828_502@ return width_ * height_; @H_828_502@ } @H_828_502@};随着测试的继续进行,Rectangle之间的关系测试也会出现,从而也需要把相关的函数添加进去,对象的完整性就会逐渐的得到满足。当你感觉没有更多测试的时候,对象的完整性基本就得到保证了。
“这怎么看起来象TDD?”不错,是和TDD很像。不过TDD的设计者有他们的出发点:编写整洁可用的代码(clean code that works),而我这里的出发点对象的完整性,殊途同归吧:)
原文链接:https://www.f2er.com/javaschema/288355.html