想象有个 class 用来表示网页浏览器。这样的 class 可能提供的众多函数中,有一些用来清除下载元素高速缓存区(cache of download elements)、清除访问过的 URLs 的历史记录(history of visited URLs)、以及移除系统中的所有cookies,如下所示:
class WebBrowser{
public:
...
void clearCache();
void clearHistory();
void removeCookies();
...
};
许多用户可能会想整个执行这些动作,所以 WebBrowser 也提供这样一个函数:
class WebBrowser{
public:
...
void clearEverything(); // 调用 clearCache, clearHistory 和 removeCookies
...
};
当然,这一机能也可以由一个 non-member 函数调用适当的 member 函数而提供出来:
void clearBrowser(WebBrowser& wb){
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
那么这两种实现方式,哪一种更胜一筹呢?
面向对象守则要求,数据以及操作数据的那些函数应该被捆绑在一块,这意味着它建议 member 函数是较好的选择。不幸的是这个建议是不正确的。这是基于面向对象真实意义的一个误解。
分析:
-
让我们从封装开始讨论。如果某些东西被封装,它就不再可见。愈多东西被封装,愈少人可以看到它。而愈少人可以看到它,我们就有愈大的弹性去改变它,因为我们的改变仅仅直接影响看到改变的那些人事物。因此,愈多东西被封装,我们改变那些东西的能力也就愈大。这就是我们首先推崇封装的原因:它使我们能够改变事物而只影响有限客户。
-
现在考虑对象内的数据。愈少代码可以看到数据(也就是访问它),愈多的数据可被封装,而我们也就愈能自由地改变对象数据,例如改变成员变量的数量、类型等等。相反,愈多的函数可以访问它,数据的封装性也就愈低。
由于第二种方式并不增加 “ 能够访问 class 内的 private 成分 ” 的函数数量。所以它更好:它导致 WebBrowser class 有较大的封装性。
在这一点上有两件事情值得注意:
第一,这个论述只适用于 non-member non-friend 函数。friends 函数对 class private 成员的访问权利和 member 函数相同,因此两者对封装的冲击力道也相同。从封装的角度看,这里的选择关键并不在 member 和 non-member 函数之间,而是在 member 和 non-member non-friend 函数之间。
第二件值得注意的事情是,只因为封装性而让函数 “ 成为 class 的 non-member ”,并不意味它 “ 不可以是另一个 class 的 member ”。
在 C++ 中,比较自然的做法是让 clearBrowser 成为一个 non-member 函数并且位于 WebBrowser 所在的同一个 namespace(命名空间)内:
namespace WebBrowserStuff{
class WebBrowser{ ... };
void clearBrowser(WebBrowser& wb);
...
};
namespace 可以跨越多个源码文件而 class 不能。这很重要。它保证了 class WebBrowser{ … }; 和 void clearBrowser(WebBrowser& wb); 在一个命名空间内,以便 void clearBrowser(WebBrowser& wb); 实现功能。
总结:
- 宁可拿 non-member、non-friend 函数替换 member 函数。这样做可以增加封装性、包裹弹性(packaging flexibility)和机能扩充性。