心寄笔端 附庸风雅

甘草的技术博客

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
让我们来谈一些关于面向对象理论的有趣的话题。

正方形和矩形的故事

正方形是一个特殊的矩形。
                                ----《几
何》

在面向对象理论的讨论中,有一个很令人不解的问题,就是为什么正方形不能从矩形派生?因为从几何数学的定义出发,正方形就是一个矩形,一个特殊的矩形。
首先,我们要看看矩形的封装。我们这里有很多种矩形的封装。
class Rect
{
    Point p1; 
//top-left point
    Point p2; //bottom-right point
};

//OR
class Rect
{
    
int x1;
    
int y1;
    
int x2;
    
int y2;
};

//OR
class Rect
{
    Point p; 
//top-left point
    int width;
    
int height;
};

//More...

我们可以给出很多种矩形的封装形式,它们是不是一个对象呢?是,只要我们写出同样的操作接口,表达同样的行为就可以了,那么对象的使用者是不关心,也不可能关心对象的内部结构的(除非是C/C++用sizeof这样的操作符去打量这个类型,然后它也不过是知道个大小)。那么就是说,决定了一个对象的根本在于对象的行为。
以上这段话,也许是我一厢情愿的想法,但是我也有我的代码作为证据。
C/C++代码可以如此构造:
我们把上述3种Rect分别定义在rect1.h, rect2.h, rect3.h,然后我们在对应的.cpp文件中实现它们。它们的成员变量尽管不同,但是接口都是一致的。
然后,我们在某个函数中使用这些类型,例如:
void setRectWidth(Rect* pRect, int width)
{
    pRect->setWidth(width);
}
OK, 当我们想使用第一种声明的class Rect的时候,我们只要include "rect1.h"即可。
至少,这部分代码是可以编译通过的。而且,setWidth方法会依照不通的实现,以不同的角度(方式)改变成员。

那么,既然如此,我们就再看看正方形的行为。
class Square
{
    
void setWidth(...) {}
}
这里的setWidth和Rect类中的setWidth(),setHeight()在行为上是不同的。因为Square的setWidth会改变整个正方形的横向和纵向两个方向的边长,而Rect的setWidth则(应该)不是。所以会产生这种尴尬的原因在于,在数学上,我们承认正方形是矩形的一个特例,但是在面向对象上,正方形在行为上根本区别于矩形。所以,要明确的是,我们以往说A Cat is-a Animal。是在承认猫在行为上具有动物的(共同、普遍)行为等。
更多的内容,需要大家参考Liskov的LSP原则【1】,使用该原则,我们可以反证在面向对象上,正方形不是一个矩形。

对象的Clone语义
我们可以Clone一个对象嘛?当然了,但是不是什么时候都可以Clone一个对象的。因为那关系到一个逻辑的问题。

很多人一直都在争论Michael Bay的电影《逃出克隆岛》的中关于记忆可以被克隆的大胆设想。

这是一个逻辑的问题:它不是语法的问题,所以编译器帮助不了你。我们想这样的一个问题,如果我们有一个对象是Rect,我们想复制一个Rect,它们的大小相同,OK,我们可以Clone它。而现在,我们有一个Socket对象,你想复制它嘛?一个构造完毕的Socket对象应该和Local的一个端口binding在一起,而端口就是资源。那么当我们有了两个对象,其中一个是另外一个的复制品,那么端口这个资源怎么办呢?也要复制嘛(怎么复制?)?
在Java中,有Cloneable接口和CloneNotSupportedException异常,一个对象的可克隆性,就被这两个设施淡淡地描绘出来了,而实际上,许多人并没有意识到这个事情的存在。

这并不难于理解,像“文件”,“管道”,“流”对象,都应该是不可复制的对象。
需要说明的有两点:【2】
1. 对象是否可以被Clone,这是一个设计问题,毕竟作为对象的描述-内存数据,当然是可以被Copy的。
2. 我们也许会给Socket classes的对象找到一个需要复制出它们的副本的设计理由,我不是很确定。


Socket classes
我们已经讲了这么多的内容了,希望你可以从中体会到那些我想传达的信息,最后,我们再举一个关于Socket classes的例子,来结束我们关于面向对象理论的阐述。
而具体这个例子,是关于对象模型?粒度划分?还是关于类型安全,或者是行为确定,或者是全部都包括呢?就需要你来体会了。【3】

在Win32,或者是UNIX编程中,我们通常能看到这样的(宏)代码:
#define SOCKET unsigned int

也就是说,我们用到了Socket API,socket,select,listen等函数,所需要的参数的数据类型,也不过是一个整数。既然是一个整数,那么在C/C++中,就难以保证其类型安全了,一个整数值,是一个处于侦听的Socket,还是一个TCP Socket,一个UDP的Socket,甚至,它是不是Socket,都是一个未知数。那么在这个环节上,类型安全的问题就显得尤为突出了。

以MFC为例,CAsyncSocket,CSocket封装了SOCKET,至少目前,你再使用Socket的时候,你知道这个对象是一个Socket了,而不是一个其他的什么对象。
但是,这远远不够的,看CAsyncSocket的实现代码,我们就知道了,它把侦听的Socket,和用于通信的Socket,揉二者之行为于一个类中。那么,我们获得的一个CAsyncSocket对象的引用,我们如何能确定它就是一个Server Socket?
当然了,设置一个标志成员,就可以解决这个问题了。然后,这并非是一个好的办法。
到了Java中,我们就可以看到,这两种事物,有了不同的类型,Socket和ServerSocket,再也不会犯下错误了。在ACE中,在Ruby的库中,都可以看到如此这般的设计。从根本上讲,这二者,尽管同属于TCP Socket的实现类,但是其行为有着根本的不同,一个Listen,一个Send和Receive。

于是在SOCKET->MFC CSocket->Java ServerSocket Socket的演化过程中,我们看到了对象模型的建立,粒度的划分,类型安全,以及行为的确定,还有许多许多,于是需要我们再去挖掘。


【1】:在后面的章节中,我们会更多地讨论LSP的。
【2】:就此点,我也需要更多的思考,也希望得到更多的想法。
【3】:这是我在CSDN BLOG原稿的最后一篇,所以有“最后”等字样。
posted on 2008-07-07 12:37  甘草  阅读(431)  评论(0编辑  收藏  举报
Baidu
Google
心寄笔端
TEST
以后我会加上Power By的,先别介意