什么是封装?这是一个问题。当你问别人这个问题时,小到面向对象的初学者,大到一个高级程序员,他们的回答都可能是:“把数据和关于它的所有操作都放在一个类中,这就是封装。”
他们错了。
封装 封装是一个方法,而不是尽头。封装本身并没有什么内涵,只是它可以在我们的代码中产生其他我们想要的东西,特别是它产生的灵活性和稳健性。请看这个结构体,我想大家都赞成它的实现是没经过封装的:
struct Point { int x; int y; };
这个结构体的缺点是缺乏灵活性。一旦对它做出了一些修改,太多用户代码会被破坏。如果我们后来决定我们要计算出x和y而不是储存它们,我们会很不幸。同样的,如果我们要用一个较高级的设计??可以从数据库里查找出x和y,我们也会受到障碍。这是缺乏封装的真正问题:它阻止了以后可能发生的实现的改变。结果是,没封装的代码是缺乏灵活性的,而且不是很稳健。当它改变时,用户代码不能随之温文尔雅地改变。
现在考虑一个类,它提供给用户类似于上面的结构体的能力的接口,但是带有封装的实现:
class Point { public: int XValue() const; int YValue() const; void XValue(int newXValue); void YValue(int newYValue);
private: ... // 其他东西... };
这个接口支持那个结构体(储存int的x和y),但也提供了选择性的实现,比如基于计算或数据库查找的。这是更灵活的设计,而且这个灵活性使软件更稳健。如果类的实现发现不足,它可以在不改变用户代码的条件下修改。如果公有成员函数的声明不改变的话,用户代码就不会受到影响。(如果采用恰当的实现,用户甚至不用重新编译。)
封装的代码会比没封装的更灵活,灵活性会使代码更高级。
封装度 上面的类没有完全封装它的实现。如果实现改变了,代码仍然会被破坏。特别是类的成员函数可能被破坏。十有八九,它们都依赖于类的数据成员的细节。但很明显的是这个类也封装得比那个结构体好得多,我们将会把它规定的更正式些。
这很容易。这个类封装得比那个结构体好的原因是,(公有的)结构体数据成员改变比(私有的)类数据成员改变可能会造成更多的代码破坏。这会引出一个合理的途径来评估两种实现的相对封装性:如果改变一个比相应地改变另一个可能导致更多的代码破坏,那么前者的封装度就比后者少。这个定义符合我们的直觉??如果一个改变会破坏很多代码,我们可能更喜欢做另一个影响较小的改变。这是封装(如果做了一些改变多少代码可能破坏)和实际灵活性(我们做出改变的可能性)之间的直接关系。
一个来衡量有多少代码可能被破坏简单的方法是计算会受影响的函数个数。那就是如果改变一个实现会比改变另一个导致更多的潜在函数破环,则第一个实现的封装就比第二个少。如果我们应用这个原理到上面的结构体,我们可以看到改变它的数据成员可能破坏无数的函数??每个使用这个结构体的函数。总的来说,我们没办法数出这是多少函数,因为没有什么方法可以查找所有使用这个结构体的代码,对于库代码更是如此。但是,如果类的数据成员改变可能破坏的函数数目却很容易得出来:那就是所有访问类的私有部分的函数。对于这个例子我们可以知道那只有四个函数(假设类的私有部分没有声明其他的什么),因为它们都方便地列在类的声明中。因为只有这些函数可以访问类的私有部分,所以如果这部分改变的话只有他们会受到影响。
封装和非成员函数 我们现在找到了一个合理的方式来测量类的封装量,那就是计算如果改变了类的实现会被破坏的函数的数目。在这种情况下,一个有n个成员函数的类显然比一个有n+1个成员函数的类封装得要好。而且可以看出应该尽量用非成员非友元函数来代替成员函数:如果一个函数f可以用成员函数也可以用非友元非成员函数实现,让它成为成员会降低封装度,而且让它成为非成员则不会。
我们在成员函数和非友元非成员函数之间选择是很重要的。和成员函数一样,当类的实现改变时,友元函数也可能被破坏。此外,我们现在常见的抱怨“友元函数破坏了封装”不完全是对的。友元不会破坏封装,它们只是降低了它??和成员函数一样。
这个分析可以应用到任何类型的成员函数,包括静态的。当函数的功能可以以非友元非成员实现时,如果给类增加静态成员函数就会减少封装性,和增加非静态成员函数的量一样。这暗示了只为了表现出和类有关而把一个自由函数移到类的里面,成为静态成员的做法一般来说是个坏主意。比如,如果我有一个抽象基类Widget,然后用一个工厂函数使用户可以建立Widget,下面是常见的,但是不好的组织方法:
// 较差封装性的设计 class Widget { ... // 所有的Widget资料;可能是 // public,private或protected
public: // 也可以是非友元非成员 static Widget* make(/* params */); };
一个比较好的设计是把make移出Widget,这样增加了系统的整体封装性。要表示Widget和make有关系,正确的工具是名字空间:
// 封装得较好的设计 namespace WidgetStuff { class Widget { ... }; Widget* make( /* params */ ); };
结论 “非成员函数能提高封装度”,这个颠覆传统的说法一定让你吃了不小的惊吧,但这的确是事实。从另一个角度想问题:看看STL。在STL中,数据是放在容器中的,而对数据的操作则放在非成员的算法中,然后通过迭代器把它们粘合起来。这样可以用一个算法操作多个不同的容器,封装度也很高。如果是传统概念的封装,则不同的容器都要有一套算法,反而会造成巨量的代码重复,并降低了封装度。
所以,要牢记一点:如果一个函数既可以用成员也可以用非成员来实现时,你应该尽量用非成员函数来实现。这个决定提高了类的封装性。当你想到封装时,你应该首先想到非成员函数。
|
|