17 / 80 · C++11 · 约 8 分钟
封装、继承与组合:OOP 的边界
封装维护对象的不变量,公开继承表达可替换的接口关系,组合表达拥有或使用关系。不要因为能复用几行代码就建立继承层次;先定义合法状态与调用契约,再决定是否需要运行期多态。
封装不是批量生成 getter
封装的目标是让对象始终满足不变量,而不只是把字段写在 private 下。例如余额不能为负,就应提供带检查的扣款操作,而不是返回可修改的余额引用。调用者只需理解操作的前提、结果和失败方式,不必知道余额是整数还是某种账本。
构造函数负责建立初始合法状态,成员函数负责维持它。若每个字段都暴露任意赋值的 setter,检查责任仍被分散给外部调用者。只有实现需要改变、而调用契约不变时,封装的收益才真正体现出来。
公开继承是一项替换承诺
Wallet : public Payment 表示钱包可以在接受支付接口的地方使用。派生类除了满足函数签名,还应遵守接口约定:余额不足返回失败且余额不变。若某个派生类失败时仍扣钱,即使编译通过,也破坏了行为可替换性。
虚函数允许通过基类引用选择具体实现;继承本身并不自动启用动态分派。接口使用者通常不应反复检查具体派生类型,否则每增加一种实现,都要修改原本应独立的调用方。
实现复用优先考虑组合
钱包拥有余额管理器,因此把 Balance 放成成员,比让钱包继承余额管理器更直接。组合不会把被复用类型的所有公开接口自动暴露出去,也避免把内部实现关系误写成外部类型关系。成员随外层对象自动构造、销毁,生命周期容易追踪。
示例同时使用两种关系:钱包公开实现支付接口,内部组合余额对象。只有真正需要从同一接口调用不同实现时才引入虚函数;类型在编译期固定的简单对象,直接组合与普通成员函数通常已经足够。
容易答错的地方
- 公开继承能完成向上转换,不等于派生类自动满足基类的业务契约;失败后的状态也属于契约。
- 返回内部字段的可修改引用会绕过检查;private 不是内存安全或加密机制。
运行一个例子
最低标准 C++11 · 完整程序 · 下载 .cpp
#include <cassert>
#include <stdexcept>
class Balance {
int cents_;
public:
explicit Balance(int cents) : cents_(cents) {
if (cents < 0) throw std::invalid_argument("negative balance");
}
bool withdraw(int amount) {
if (amount < 0 || amount > cents_) return false;
cents_ -= amount;
return true;
}
int value() const { return cents_; }
};
struct Payment {
virtual bool pay(int cents) = 0;
virtual ~Payment() = default;
};
class Wallet final : public Payment {
Balance balance_;
public:
explicit Wallet(int cents) : balance_(cents) {}
bool pay(int cents) override { return balance_.withdraw(cents); }
int remaining() const { return balance_.value(); }
};
int main() {
Wallet wallet(100);
Payment& payment = wallet;
const bool paid = payment.pay(30);
const bool rejected = payment.pay(80);
assert(paid && !rejected);
assert(wallet.remaining() == 70);
}
在本地编译
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread objects-oop.cpp -o example && ./example预期结果
预期:正常退出、无输出;所有 assert 通过。
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
给 Wallet 增加退款功能时,为什么不直接公开 Balance&?应如何限定接口?
查看参考答案
公开 Balance& 会把内部实现及未来新增操作一并暴露。应增加明确的 refund(int) 操作,拒绝负数,并在相加前检查金额是否超过可表示上限;检查失败保持余额不变。这样余额表示可以替换,支付接口也不必包含退款能力。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。