Effective C++ 条款22-23 封装性相关

条款22 : 将成员变量声明为private

本条款中, 作者明确声明, 如果没有必要的原因, 务必将成员变量声明为private, 而非public/protected, 并苦口婆心地列举出了非常多使用private的优势, 让我们逐一阅览 :

  • 保证了语法一致性

    当客户通过对象调用时, 如果将成员变量声明为private, 那么就省去了客户思考要不要加小括号的问题, 只要无脑加括号就行了.

  • 对成员变量的处理更加精确

    当你把成员变量声明为public时, 客户拥有对该变量的所有特权, 可读可写, 然而我们一定不希望这样, 如果我们把成员变量声明为private, 通过public函数, 我们希望客户读时就返回一个const reference, 希望写就让客户传入修改值, 在函数内部修改, 如果不希望读写就不暴露在外置接口中.

  • 保证了封装性

    书中提出 : Public意味不封装, 不封装意味不可改变, 因为成员变量很大程度上是会被广泛使用的, 会出现在无数客户代码中, 如此牵一发而动全身, 就算你以后想改, 可客户就不一定想改了. protected也是同理, protected成员变量就像public成员变量一样缺乏封装性, 虽然成员变量并不开放给客户, 但是如果你之下有无数的派生类, 带来的修改难度也是非同一般, 由此可以得出一个结论 : 成员变量的封装性与”成员变量的内容改变所破坏的代码数量”成反比.

    而当我们使用private成员变量, 我们的修改范围就仅仅只局限在类内, 非常易于事后的修改. 就算外部可以通过函数访问该成员变量, 日后希望对这个变量进行替换或修改, 外部也全然不知. 也就是说, 把成员变量隐藏在函数接口背后, 可以为所有可能的实现提供弹性.

    书中给出了一个速度收集器的类, 保存每次测量到的速度, 可以返回当前状态下的速度平均值, 雏型如下 :

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class SpeedDataCollection {
    private:
    vector<int> data;
    ...
    public:
    void addValue(int speed); // 将收集到的速度存入data中
    double averageSoFar() const; // 通过某些算法求得平均值并返回
    ...
    };

    我们来假定一个情景, 这个类使用于一个嵌入式模板内, 一开始我们的可使用内存很少, 也不是很要求高效, 于是我们在averageSoFar()中的策略就是遍历data, 求出总和再求平均值.

    当我们技术不断提高, 可使用内存变大了, 对计算效率有了需求, 我们就可以多加一个private成员变量sum额外记录data的总和, 然后修改averageSoFar()中的处理逻辑, 让其直接用sum求平均值, 效率可以从O(N)提到O(1).

    然而不管我们修改了什么, 外部averageSoFar()的使用者都不会知道.

    这种把成员变量隐藏在函数接口背后的方式, 除了以上行为, 还可以使得成员变量被读写时轻松同时其他对象, 验证class的约束条件以及函数的前提和事后条件, 实现在多线程环境中执行同步控制.


请记住 :

  • 切记将成员变量声明为private. 这有助于一致性, 访问控制, 允许约束条件获得保证, 提升类的实现弹性.
  • protected并不比public更具封装性.

条款23 : 宁以non-memeber non-friend替换member函数

本条款告诉我们, 如果一个成员函数没有直接访问成员变量的需求, 那么推荐将其替换为non-memebernon-friend的函数.

想理解本条款可以先对封装性有进一步的认知 :

书中提出, 封装的本质在于隐藏内部实现和暴露有限接口, 至于我们为什么推崇封装, 是因为它使我们能改变的事务只影响有限客户, 就是说大大降低了我们后期做出改变时的维护成本.

一个类的封装性和它的数据, 也就是成员变量息息相关, 愈少的代码可以看到数据, 愈多的数据就可以被封装, 封装性也就越强.

然而我们通常情况下认为数据以及操作数据的那些函数应该被捆绑在一起, 这样子是在实现所谓的封装, 然而正如我们前面所说, 愈多成员函数可以访问数据, 数据的封装性就越低.

成员函数是实现封装的必要手段, 但并不代表我们可以毫无顾虑地增加成员变量, 每有一个函数可以访问到内部数据, 我们的维护成本就会提高一些, 因为你不一定会记得哪个函数有没有在什么不起眼的地方调用了我们的底层数据. 当然如果你预想的成员函数必须调用成员变量, 那完全没有问题.

书中给出了一个用于管理网络浏览器的类, 其中有一些用来清理的函数, 有的用来清理高速缓冲区, 有的用来清理历史记录, 有的用来移除cookie, 让我们看看书中的代码 :

1
2
3
4
5
6
7
8
class WebBrowser {
public:
...
void clearCache();
void clearHistory();
void removeCookies();
...
};

我们在大多数情况下是希望同时调用这三个函数的, 于是我们可以写一个统合函数clearBrowser(), 按照习惯我们可能就把它写为成员函数了, 但有了上面的认知, 我们可以直接把它写成一个non-memeber non-friend函数, 代码如下 :

1
2
3
4
5
6
7
8
9
10
11
12
class WebBrowser {
public:
...
// void clearBrowser(WebBrowser& wb); // 请放弃这种写法
};

void clearBrowser(WebBrowser& wb)
{
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}

因为我们知道clearBrowser内部不会以任何形式直接调用数据, 所以干脆直接把clearBrowser排除在外, 这不只是告诉未来的自己, 也是在告诉将要维护这份代码的其他人, 这个函数从机理上就不可能触及底层数据, 是绝对安全的, 封装性因此得到提升.

当然还是要把他俩放在同一个命名空间内, 不然就真互相找不到了 :

1
2
3
4
5
6
namespace WebBrowserStuff {
...
class WebBrowser { ... };
void clearBrowser(WebBrowser& wb);
...
}

书中还提出 : 像是这种clearBrowser不触及底层数据的伪成员函数, 我们一般称其为便利函数, 将所有的便利函数放在多个头文件内但隶属于同一个命名空间, 可以使编写者轻松找到并扩展某些方向的便利函数, 示例如下 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
namespace WebBrowserStuff {
class WebBrowser { ... }; // 核心机能
...
}

// header "webbrowserclear.h"
namespace WebBrowserStuff {
void clearBrowser(WebBrowser& wb); // 有关内存清理的便利函数
...
}

// header "webbrowserbookmarks.h"
namespace WebBrowserStuff {
... // 有关书签日志的便利函数
}

// header "webbrowsercookies.h"
namespace WebBrowserStuff {
... // 有关cookie的便利函数
}
...

请记住 :

  • 宁以non-memeber non-friend替换member函数, 如果这个函数没有访问底层数据的必要的话. 这样子可以增加封装性, 包裹弹性和可扩展性.

by 天目中云


Effective C++ 条款22-23 封装性相关
http://example.com/2024/12/02/[Effective C++]条款22-23 封装性相关/
作者
天目中云
发布于
2024年12月2日
许可协议