继承深度超三层应警惕,因修改基类引发高维护成本且共性抽取失效;策略模式通过将“变体逻辑”收拢为独立对象,实现从“谁是我”到“怎么做”的设计校准。

过深的继承结构(比如四层以上、子类超过 10 个)通常不是设计问题,而是抽象错位——你本该用组合解决“行为变化”,却用继承硬扛“算法差异”。策略对象能直接切掉中间冗余层级,但不是简单把虚函数换成 std::function 就完事。
为什么继承深度超过三层就该警惕
继承链越长,修改一个基类成员(比如加个 timestamp 字段或改个 validate() 行为)引发的连锁编译、测试和回归成本越高。更麻烦的是:很多派生类只重写一两个函数,其余全是透传,这说明“共性抽取”已经失效,而“行为差异”才是主线。
此时策略模式不是“另一种实现”,而是对设计意图的重新校准:把“谁是我”(is-a)的问题,转成“我现在要怎么做”(do-this)的问题。
- 典型症状:
dynamic_cast频繁出现、override函数里大量return BaseClass::func();、新增子类时要改七八个头文件 - 根本原因:把运行时可变逻辑(如计费规则、序列化格式、校验策略)塞进了编译期类型系统
- 策略替代的关键动作:把原本分散在各层
virtual函数里的“变体逻辑”,收拢到独立的策略对象中,由上下文类持有并调用
用策略对象替换继承链的三步实操
假设你有一条 Shape → Polygon → Quadrilateral → Rectangle → Square 继承链,其中只有 area() 和 serialize() 在不同子类中真正不同,其余全是重复代码。
立即学习“C++免费学习笔记(深入)”;
别重构整个类图,先做最小侵入式替换:
- 第一步:抽离策略接口,只针对真正在变的行为。例如定义
class IAreaStrategy { public: virtual double compute(const ShapeContext&) const = 0; };,不带任何与Shape数据成员耦合的参数 - 第二步:让最顶层基类(
Shape)持有一个std::unique_ptr<iareastrategy></iareastrategy>,删掉所有子类中的area()重写,统一走m_areaStrategy->compute(ctx) - 第三步:把原来每个子类里硬编码的面积逻辑,挪进独立策略类(
RectangleArea、SquareArea),构造时传入所需数据(如width,height),而非依赖 this 指针访问成员
这样,Rectangle 和 Square 类可以退化为仅负责数据承载的 POD 类型,甚至合并为一个 Quad 类,靠注入不同策略区分行为。
std::function vs 抽象基类:选哪个取决于生命周期和性能敏感度
用 std::function<double double></double> 看似简洁,但容易埋坑:
- 捕获局部变量时生命周期失控:
[&ctx]() { return ctx.width * ctx.height; }—— 若策略被缓存复用,ctx可能早已析构 - 虚函数调用开销约 1–3 ns,
std::function调用约 5–12 ns;高频路径(如每帧调用千次的渲染策略)建议坚持抽象基类 +unique_ptr - 若策略逻辑极简且无状态(如
[](double x) { return x * 0.9; }),std::function更轻量,避免虚表和动态分配 - 混合使用可行:上下文类提供
setStrategy(std::unique_ptr<istrategy>)</istrategy>和setStrategy(std::function<...>)</...>两个重载,后者内部包装成匿名策略类
策略对象如何接管原继承链中的状态依赖
老代码里常见 Rectangle::area() 直接读 this->width 和 this->height。策略对象没有 this,必须显式传参——这是解耦的代价,也是好处。
正确做法不是让策略去访问上下文的私有成员,而是由上下文决定“暴露什么”:
- 定义
struct AreaInput { double width; double height; bool isSquare; };这样的瘦 DTO,由Shape构造后传给策略 - 拒绝在策略中做类型判断:
if (input.isSquare) {...}是允许的;if (dynamic_cast<Square*>(this)) {...}是倒退 - 若策略需要访问复杂状态(如整个订单上下文),传
const OrderContext&引用,而不是裸指针或智能指针——避免策略意外延长对象生命周期
真正难的不是写策略类,而是厘清“哪些数据属于策略计算,哪些属于策略选择”。前者进参数,后者进上下文配置或工厂逻辑。


















