编程爱好者联盟 2017-02-28
想象一个表示web浏览器的类。这样一个类提供了清除下载缓存,清除URL访问历史,从系统中移除所有cookies等接口:
class WebBrowser { public: ... void clearCache(); void clearHistory(); void removeCookies(); ... };
许多用户想将这些动作一块执行,所以web浏览器为此可以提供一个函数:
class WebBrowser { public: ... void clearEverything(); // calls clearCache, clearHistory, // and removeCookies ... };
当然,这个功能也可以通过非成员函数来提供,让它调用合适的成员函数就可以了:
void clearBrowser(WebBrowser& wb) { wb.clearCache(); wb.clearHistory(); wb.removeCookies(); }
哪种方法才是更好的呢?是成员函数clearEverying还是非成员函数clearBrower?
面向对象准则指出数据以及操作数据的函数应该被捆绑到一起,这就表明它建议成员函数是更好的选择。不幸的是,这个建议是不正确的。它曲解了面向对象的含义。面向对象准则指出数据应该尽可能的被封装。违反直觉的是,成员函数clearEverything实际上并没有比非成员函数clearBrower有更好的封装性。并且提供非成员函数能够为web浏览器的相关功能提供更大的包装(packaging)灵活性,相应的,就可以产生更少的编译依赖和更好的可扩展性。因此非成员函数比成员函数在许多方面都要好。明白为什么很重要。
以封装开始。如果一些东西被封装了,就意味着被隐藏起来了。封装的东西越多,就有更少的客户能看到它们。更少的客户能看到它们就意味着我们有更大的灵活性来进行对它们进行修改,因为我们的修改直接影响的是能看到这些修改的客户。因此封装性越好,就赋予我们更大的能力来对其进行修改。这也是我们将封装摆在第一位的原因:它以一种只影响有限数量的客户的方式为我们修改东西提供了灵活性。
考虑同一个对象相关联的数据。看到这些数据的代码越少(也就是可访问它),数据就被封装的越好,我们就有更大的自由来修改这个对象的数据的一些特征,像数据成员的数量,类型等等。通过确认有多少代码能够看到数据来判断数据的封装性是粗粒度的方法,我们可以计算出能够访问数据的函数的数量,能访问数据的函数越多,封装性越差。
Item 22解释了数据成员应该是private的,因为如果不是,未限定数量的成员函数就能够访问它们。这样就根本没有封装性可言。对于private的数据成员,能够访问它们的函数的数量为所在类的成员函数的数量加上友元函数的数量,因为只有成员函数和友元函数能够访问private成员。考虑在一个成员函数(不仅能访问类的private数据,也能访问private函数,enums,typedef等等)和一个非成员非友元函数(私有的数据和函数都不能访问)之间做一个选择,它们提供了相同的功能,能够产生更大封装性的选择是非成员非友元函数,因为他们没有增加能够访问类私有部分的函数的数量。这就解释了为什么clearBrower(非成员非友元函数)要优于clearEverything:在WebBrowser类中,它产生了更大的封装。
在这点上有两件事情需要注意。第一,这个论述只适用于非成员非友元函数。友元同成员函数对类的私有成员有相同的访问权,因此对封装有相同的影响。从封装的观点来看,不是在成员和非成员函数之间进行选择,而是在成员和非成员非友元函数之间进行选择。(封装当然不是仅有的选择视角,Item 24中解释了在隐式类型转换中,需要在成员和非成员函数之间做出选择。)
第二件需要注意的事情恰恰是因为封装性指明类的函数为非成员函数这个观点,这并不意味着这个函数不能是别的类的成员函数。我们可以将clearBrower声明成一个utility类的静态成员函数。只要它不是WebBrowser的一部分(或者一个友元),它就不会影响WebBrower的private成员的封装性。
在c++中,一个更加自然的方法是使clearBrower成为同WebBrowser有相同命名空间的非成员函数:
namespace WebBrowserStuff { class WebBrowser { ... }; void clearBrowser(WebBrowser& wb); ... }
不仅仅是更加自然,因为命名空间不像类,它是可以跨文件的。这是很重要的,因为像clearBrower这样的函数是很便利的函数。既不是成员也不是友元,对WebBrower类没有特殊访问权,因此它不能提供WebBrowser客户没有获取到的其他任何功能。举个例子,如果clearBrower这个函数不存在,客户只好自己调用clearCache,clearHistory,和removeCookies。
像webBrower这样的类可以有大量的便利函数,一些和标签相关,另一些和打印相关还有一些和cookie管理相关等等。通常大多数客户只对其中的一部分有兴趣。没有理由让只对标签便利函数感兴趣的客户编译依赖于cookie相关的便利函数。将它们分开的直接的方法是将它们声明在不同的头文件中。
// header ”webbrowser.h — header for class WebBrowser itself // as well as ”core WebBrowser-related functionality namespace WebBrowserStuff { class WebBrowser { ... }; ... // ”core related functionality, e.g. // non-member functions almost // all clients need } // header ”webbrowserbookmarks.h namespace WebBrowserStuff { ... // bookmark-related convenience } // functions // header ”webbrowsercookies.h namespace WebBrowserStuff { ... // cookie-related convenience } // functions
注意标准C++库就是这么组织的。它并没有在std命名空间中将所有东西包含在一个单一的<C++ Stand Library>头文件中,而是有许多头文件(<vector>,<algorithm>,<memory>等等),每个头文件声明了std命名空间中的一部分功能。只使用vector相关功能客户不需要#include <memory>;不需要使用list的客户不必#include <list>。这就允许客户只编译依赖于它们实际用到的部分。(Item 31中讨论了减少编译依赖的其他方法)。当一个功能来源于一个类的成员函数,那么将其分割就是不可能的,因为一个类必须被定义在一个整体中。它不能再分了。
将所有的便利函数放在不同的头文件中——但放在一个命名空间中——同样意味着客户可以很容易的对便利函数进行扩展。他们需要做的是向命名空间中添加更多的非成员非友元函数。举个例子,如果一个WebBrower客户决定实现图片下载相关的便利函数,他只需要创建一个头文件,在命名空间WebBrowserStuff中将这些函数进行声明。新函数能像旧的函数一样同它们整合在一起。这也是类不能提供的另外一个性质,因为客户是不能对类定义进行扩展的。当然,客户可以派生出新类,但派生类没有权限访问基类的封装成员(像private成员),这样的”扩展功能就是二等身份。此外,正如Item 7中解释的,并不是所有类都被设计成基类。