Shuffy

不断的学习,不断的思考,才能不断的进步.Let's do better together!
posts - 102, comments - 43, trackbacks - 0, articles - 19
【转】http://www.cppblog.com/tiandejian/archive/2007/06/11/ec_23.html

第23条:     尽量使用非成员非友元函数,而不是成员函数

请假想一个表示网页浏览器的类。这个类可以提供诸多功能,其中包括清除下载缓存、清除访问历史、删除系统中保存的 cookie 等等:

class WebBrowser {

public:

 ...

 void clearCache();

 void clearHistory();

 void removeCookies();

 ...

};

许多用户可能需要同时执行这些操作,所以 WebBrowser 类应该提供一个函数来做这件事情:

class WebBrowser {

public:

 ...

 void clearEverything();        // 调用 clearCache clearHistory

                                // 以及 removeCookies

 ...

};

当然,这一功能也可以通过一个非成员函数调用适当的成员函数来实现:

void clearBrowser(WebBrowser& wb)

{

 wb.clearCache();

 wb.clearHistory();

 wb.removeCookies();

}

哪一个更好呢?是成员函数 clearEverything ,还是非成员函数 clearBrowser

面向对象的基本原理要求数据和对其进行操作的函数应该被包装在一起,同时建议成员函数为更优秀的选择。但不幸的是,这一建议并不是正确的。它是建立在对“面向对象的东西意味着什么”这一点的误解之上的。通过理性分析可以得知,成员函数 clearEverything 的封装性实际上比非成员函数 clearBrowser 还要。还有,非成员函数可以为 WebBrowser 相关的功能提供更便利的打包方法,从而减少编译时依赖,提高 WebBrowser 的可扩展性。很多情况下,非成员函数的方法都比成员函数的方法要好。理解这一结论的原因是十分重要的。

我们从封装问题开始。如果一个物件被封装了,那么它就是不可见的。它的封装度越高,其它人或物件能看到它的机会就越少。能看到它的东西越少,我们对其进行修改的灵活性就越高,因为只有很少的物件能看到我们的改动。也就是说,一个物件的封装度越高,系统赋予我们修改它的能力就越强。这就是我们为什么将封装置于首要位置的原因:他为我们提供了改变物件的灵活性的方法,使用这一方法只会有很少的客户端会受到影响。

请考虑与对象相关的数据。可以看到某一数据(也就是访问这一数据)的代码越少,就有更多的数据被封装起来,从而我们修改这一对象的数据特征(比如数据成员的个数、类型等等)时就更为自由,粗略计算一下某一数据可以被多少代码所访问,我们就可以计算出有多少函数可以访问这一数据:可以访问它的函数越多,这一数据的封装度就越低。

22 条中解释了为什么要使用私有的数据成员,因为如果数据成员不是私有的,那么就有无穷的函数可以访问它们。它们就毫无封装性可言。对于声明为私有的数据成员来说,可以访问它们的函数的个数就等于成员函数的个数加上友元函数的个数,因为只有成员和友元可以访问类中的私有数据。无论是成员函数(不仅仅可以访问类中的私有数据,还可以访问私有函数、 enum 类型、由 typedef 生成的类型符号,等等)还是非成员函数(上述成员函数可以访问的所有内容都不可访问)所提供的功能都是完全相同的,选择非成员非友元函数可以带来更完整的封装度,因为它不会增加可以访问类中私有部分的函数的数量。这就解释了为什么使用 clearBrowser (非成员非友元函数)比 clearEverything (成员函数)更理想: clearBrowser 可以为 WebBrowser 提供更高的封装度。

此刻我们需要关心两件事情。第一,上面的推理过程仅仅适用于非成员非友元函数。由于友元对类中的私有成员的访问权与成员函数相仿,因此是否使用二者都会对封装度带来一定的影响。以封装的观点,我们并不是在成员或非成员函数之间做出选择,选择的双方应是成员函数和非成员非友元函数。(当然,封装不是唯一的观点。第 24 条中将为你介绍,当问题转向“隐式类型转换”时,选择就在成员和非成员函数之间进行)

需要关注的第二件事仅仅是由于:封装要求函数不应为类一个成员,而并不意味着要求它也不是其它类的成员。在某些语言中(比如 Eiffel Java C# 等等 ),所有函数必须包含在类中。对于习惯于使用这些语言的程序员来说,这第二件事多多少少可以算是对他们的一剂安抚药。比如说,我们可以将 clearBrowser 定义为某个“实用工具类”中的静态成员函数。只要它不是 WebBrowser 的一部分(或它的友元),它就不会影响到 WebBrowser 中私有成员的封装性。

C++ 中可以使用一个更为自然的方法:将 clearBrowser 定义为一个非成员函数,并让其与 WebBrowser 在同一个名字空间中:

namespace WebBrowserStuff {

 

 class WebBrowser { ... };

 

 void clearBrowser(WebBrowser& wb);

 

 ...

}

然而,你得到的东西远远要比更自然的代码要多。因为与类不一样的是,名字空间可以延伸至多个源代码文件中。这一点十分重要。 ClearBrowser 这样的函数是 “便利函数”。由于它们既不是成员函数也不是友元,所以它在访问 WebBrowser 时就没有任何特权,从而它也就不能够以其它的什么办法提供 WebBrowser 的客户端代码所不具备的功能。举例说,如果 clearBrowser 不存在的话,那么客户端就只能自己动手调用 clearCache clearHistory removeCookies 这些函数了。

一个类似 WebBrowser 的类可能会有大量的便利函数,一些是关于书签的,另一些是关于打印的,还有关于 cookie 管理的,等等。作为一个一般守则,大多数客户端只会对这些便利函数中的一部分感兴趣。举例说,一个客户端程序员可能只对与书签相关的便利函数感兴趣,但是书签相关便利函数又依赖于 cookie 相关的便利函数,没有理由让这个程序员去关心那些额外信息。将着些便利函数分离开来的最直接的办法就是:将书签相关的便利函数声明在一个头文件中, cookie 关的为与另一个头文件,打印相关的在第三个:

// header "webbrowser.h" WebBrowser 自身所定义的头文件

// 同时也包含“核心的” WebBrowser 相关的功能

namespace WebBrowserStuff {

   class WebBrowser { ... };

     ...                         // “核心”相关功能

                                // 比如大多数客户端必需的非成员函数

}

 

// header "webbrowserbookmarks.h"

namespace WebBrowserStuff {

 ...                            // 书签相关的便利函数

}

 

// header "webbrowsercookies.h"

namespace WebBrowserStuff {

 ...                            // cookie 相关的便利函数

}

 

...

请注意:上面就是 C++ 标准库的组织方式。标准库使用了多个头文件(包括 <vector> <algorithm> <memory> 等等),每一个都声明了 std 名字空间中的某一些功能。而不是使用单态的 <C++StandardLibrary> 头文件,并将 std 中所有的功能都罗列于此。对于仅希望使用 vector 相关功能的客户端程序员,不应该强迫他们去 #include <memory> ;不希望使用 list 的客户端也不需要去 #include <list> 。这样就使得所有的客户端程序员仅仅需要考虑他们正在使用的那部分系统中的编译依赖问题。(参见第 31 条,其中介绍了解决减缓编译依赖问题的其它途径。)对于一个类的成员函数而言,以这种方式 分开管理功能是不可行的,因为一个类必须要保证其完整性,它不应该被分割成块。

将所有的便利函数放置于多个头文件中(但位于同一个名字空间中),同时也意味着客户端程序员可以方便地扩展便利函数集。他们所需要做的仅仅是向同一名字空间中添加更多的非成员非友元函数。比如说,如果一个 WebBrowser 的客户端程序员希望编写一个关于下载图片的便利函数,他或她仅仅需要 WebBrowserStuff 名字 空间中创建一个新的头文件来声明这些函数。新的函数与其它的便利函数一样可用,一样具有整合性。这是类无法提供的又一特性,因为类定义并不为客户端程序员提供扩展性。当然,客户端程序员可以派生新类,但是派生类仍无法访问基类中的封装(也就是私有)成员,所以我们说这样的“扩展功能”只有“二等”身份。同时,如同第 7 条中所讲,并不是所有的类都设计成了基类。

铭记在心

在特性情况下要更趋向于使用非成员非友元函数,而不是成员函数。这样做可以增强封装性,以及包装的灵活性和功能扩展性。


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理