C++ / a working model

17 / 80   ·   C++11   ·   约 8 分钟

封装、继承与组合:OOP 的边界

先记住这句话

封装维护对象的不变量,公开继承表达可替换的接口关系,组合表达拥有或使用关系。不要因为能复用几行代码就建立继承层次;先定义合法状态与调用契约,再决定是否需要运行期多态。

本篇内容
  1. 封装不是批量生成 getter
  2. 公开继承是一项替换承诺
  3. 实现复用优先考虑组合
  4. 运行示例
  5. 动手练习

封装不是批量生成 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) 操作,拒绝负数,并在相加前检查金额是否超过可表示上限;检查失败保持余额不变。这样余额表示可以替换,支付接口也不必包含退款能力。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录