封装与接口设计 C++类与对象

封装

  1. 封装的定义:将类的的具体实现细节隐藏在接口之后,可以说封装的工作就是设计接口。
  2. 接口的定义:类的可访问元素(public成员、友元等),还包括全局函数。
  3. 服务客户:作为类的设计者,我们的代码大多数时候并不是给自己使用的,而是要给客户(借助我们的代码进行开发的另一批程序员)使用,他们对我们类的实现方式没有概念,他们的工作依赖我们高质量的接口。
  4. 更新维护:大型程序的更新维护必不可少,而良好的封装可以降低更新维护的代价。封装使得我们可以改变实现而只影响有限客户。

我们往往需要改变我们类的具体实现方式(下面有例子)。如果不进行封装,假设我们有一个public成员变量然后取消了它。那么客户代码中所有涉及该成员变量的代码都被破坏了,客户需要重新编写代码、重新测试、重新编写文档等等。但如果进行封装,虽然实现方式的改变但接口不变,那么客户实际上什么也不用做。

对同一种问题有多个实现方式。比如一个计算汽车平均速度的软件,我的实现方法可能有两种:

a. “随时维持一个当下平均速度”
b. “每次需要时才计算平均速度”

a方案的空间开销大,但可以立即返回平均速度。b方案的空间开销小,但平均速度获取的实时性较差。两种方案各有千秋,要命的是,我们可能需要多次在两种方案中做切换。

将成员变量声明成private

  1. 有利于接口的一致性。成员函数的访问加(),成员变量不加。如果是我们类的使用者是我们自己,固然可以,但我们的客户往往不知道类中哪些是成员变量哪些是成员函数。使用private接口,要求客户统一使用()的方式进行访问。

  2. 减少客户出错的几率。有些成员我们需要向客户隐藏它们的存在。有些成员函数客户需要进行读写,我们为其提供get、set方法。如果将成员变量设为public访问,客户对成员变量的随意使用很容易出现错误。

  3. 有利于更新维护。任何public的改变都是一场灾难。如果本来有一个public变量然后我们决定将删除,那么客户所有涉及该变量的代码都需要重写、重测。这就是我们推崇封装的首要原因:它能使我们改变事物而只影响有限客户。

  4. protected不是封装。protected成员会被派生类使用,如果我们改变了protected对象,那么不可预知的大量的派生类的代码也要重写。从封装的角度看,只有两种访问权限:private与非private。

接口的设计

接口的设计可概括为一句话:让接口容易被使用,不容易被误用。

  1. 与内置类型保持一致,各接口的使用方法保持一致
    这个法则重点体现在运算符重载与STL上。比如STL每个容器都有size、begin、end这些固定操作,重载 = 返回值为引用,重载 + - * / 返回值为常量对象,重载流操作运算符返回值为 ostream& 或 istream& 。

  2. 宁以非成员函数代替成员函数和友元
    面向对象守则要求数据应该尽可能地被封装。

//想象有个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命名空间,每个头文件声明命名空间的某些功能。客户也可以对该命名空间进行自己的补充。

  1. 为预防客户可能出现的错误而设定新类型
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);
};

这种方式下,如果客户错误指定了年、月、日的顺序就会报错。

  1. ** 消除客户的资源管理责任**
    工厂函数是形容一些返回值提供单一的标准化对象的函数。可将工厂函数的返回值类型设置为智能指针,使客户不用顾忌 delete。如:
shared_ptr<Investment> createInvestment();
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值