对象的完整性

对象的完整性

对象是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),而我这里的出发点对象的完整性,殊途同归吧:)

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


什么是设计模式一套被反复使用、多数人知晓的、经过分类编目的、代码 设计经验 的总结;使用设计模式是为了 可重用 代码、让代码 更容易 被他人理解、保证代码 可靠性;设计模式使代码编制  真正工程化;设计模式使软件工程的 基石脉络, 如同大厦的结构一样;并不直接用来完成代码的编写,而是 描述 在各种不同情况下,要怎么解决问题的一种方案;能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免引
单一职责原则定义(Single Responsibility Principle,SRP)一个对象应该只包含 单一的职责,并且该职责被完整地封装在一个类中。Every  Object should have  a single responsibility, and that responsibility should be entirely encapsulated by t
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强烈推荐。原文截图*************************************************************************************************************************原文文本************
适配器模式将一个类的接口转换成客户期望的另一个接口,使得原本接口不兼容的类可以相互合作。
策略模式定义了一系列算法族,并封装在类中,它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,它是针对软件开发中经常遇到的一些设计问题,总结出来的一套通用的解决方案。
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
迭代器模式提供了一种方法,用于遍历集合对象中的元素,而又不暴露其内部的细节。
外观模式又叫门面模式,它提供了一个统一的(高层)接口,用来访问子系统中的一群接口,使得子系统更容易使用。
单例模式(Singleton Design Pattern)保证一个类只能有一个实例,并提供一个全局访问点。
组合模式可以将对象组合成树形结构来表示“整体-部分”的层次结构,使得客户可以用一致的方式处理个别对象和对象组合。
装饰者模式能够更灵活的,动态的给对象添加其它功能,而不需要修改任何现有的底层代码。
观察者模式(Observer Design Pattern)定义了对象之间的一对多依赖,当对象状态改变的时候,所有依赖者都会自动收到通知。
代理模式为对象提供一个代理,来控制对该对象的访问。代理模式在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。
工厂模式(Factory Design Pattern)可细分为三种,分别是简单工厂,工厂方法和抽象工厂,它们都是为了更好的创建对象。
状态模式允许对象在内部状态改变时,改变它的行为,对象看起来好像改变了它的类。
命令模式将请求封装为对象,能够支持请求的排队执行、记录日志、撤销等功能。
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。 基本介绍 **意图:**在不破坏封装性的前提下,捕获一个对象的内部状态,并在该
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为
享元模式(Flyweight Pattern)(轻量级)(共享元素)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结