封装
- 封装的定义:将类的的具体实现细节隐藏在接口之后,可以说封装的工作就是设计接口。
- 接口的定义:类的可访问元素(public成员、友元等),还包括全局函数。
- 服务客户:作为类的设计者,我们的代码大多数时候并不是给自己使用的,而是要给客户(借助我们的代码进行开发的另一批程序员)使用,他们对我们类的实现方式没有概念,他们的工作依赖我们高质量的接口。
- 更新维护:大型程序的更新维护必不可少,而良好的封装可以降低更新维护的代价。封装使得我们可以改变实现而只影响有限客户。
我们往往需要改变我们类的具体实现方式(下面有例子)。如果不进行封装,假设我们有一个public成员变量然后取消了它。那么客户代码中所有涉及该成员变量的代码都被破坏了,客户需要重新编写代码、重新测试、重新编写文档等等。但如果进行封装,虽然实现方式的改变但接口不变,那么客户实际上什么也不用做。
对同一种问题有多个实现方式。比如一个计算汽车平均速度的软件,我的实现方法可能有两种:
a. “随时维持一个当下平均速度”
b. “每次需要时才计算平均速度”
a方案的空间开销大,但可以立即返回平均速度。b方案的空间开销小,但平均速度获取的实时性较差。两种方案各有千秋,要命的是,我们可能需要多次在两种方案中做切换。
将成员变量声明成private
-
有利于接口的一致性。成员函数的访问加(),成员变量不加。如果是我们类的使用者是我们自己,固然可以,但我们的客户往往不知道类中哪些是成员变量哪些是成员函数。使用private接口,要求客户统一使用()的方式进行访问。
-
减少客户出错的几率。有些成员我们需要向客户隐藏它们的存在。有些成员函数客户需要进行读写,我们为其提供get、set方法。如果将成员变量设为public访问,客户对成员变量的随意使用很容易出现错误。
-
有利于更新维护。任何public的改变都是一场灾难。如果本来有一个public变量然后我们决定将删除,那么客户所有涉及该变量的代码都需要重写、重测。这就是我们推崇封装的首要原因:它能使我们改变事物而只影响有限客户。
-
protected不是封装。protected成员会被派生类使用,如果我们改变了protected对象,那么不可预知的大量的派生类的代码也要重写。从封装的角度看,只有两种访问权限:private与非private。
接口的设计
接口的设计可概括为一句话:让接口容易被使用,不容易被误用。
-
与内置类型保持一致,各接口的使用方法保持一致:
这个法则重点体现在运算符重载与STL上。比如STL每个容器都有size、begin、end这些固定操作,重载 = 返回值为引用,重载 + - * / 返回值为常量对象,重载流操作运算符返回值为 ostream& 或 istream& 。 -
宁以非成员函数代替成员函数和友元:
面向对象守则要求数据应该尽可能地被封装。
//想象有个class用来表示网络浏览器。
class webBrowser{
public:
void clearCache();
void clearURLs();
void clearCookies();
}
//许多用户想要一次性执行这三个动作。
//这里有两种选择:
class WebBrowser{
public:
...
void clearEverything();
...
}
void clearEverything(webBrowser& wb)
{
wb.clearCache();
wb.clearURLs();
wb.clearCookies();
}
成员函数与友元可以访问类的私有成员,就意味着类多了一个接口,所以实际上非成员函数的封装性更好(尤其是clearEverything这样的便利函数)。
namespace WebBrowserStuff{
class WebBrowser{...};
void clearBrowser(WebBrowser& wb);
}
使用非成员函数的一个问题是我们似乎割裂了函数与类之间的关系。这一问题的解决依赖namespace。
我们让类与函数在同一个namespace中,而又分散在不同的头文件中。这正是std的组织方式,iostream、vector、memory等不同头文件都使用std命名空间,每个头文件声明命名空间的某些功能。客户也可以对该命名空间进行自己的补充。
- 为预防客户可能出现的错误而设定新类型
class Date {
public:
Date(int month, int day, int year);
};
Date类中提供了构造函数的接口,如果客户严格按照月、日、年的顺序书写函数参数,那就没什么问题。但一些客户可能会不小心用年、月、日的顺序提供参数。严重的是,这种错误并不会被接口所识别(年、月、日都是 int 类型)。
更优秀的接口则为年、月、日提供了不同的数据类型。
class Month{
public:
explicit Month(int m) : val(m) {}
int val;
};
class Day;
class Year;
class Date {
public:
Date(Month, Day, Year);
};
这种方式下,如果客户错误指定了年、月、日的顺序就会报错。
- ** 消除客户的资源管理责任**
工厂函数是形容一些返回值提供单一的标准化对象的函数。可将工厂函数的返回值类型设置为智能指针,使客户不用顾忌 delete。如:
shared_ptr<Investment> createInvestment();