news 2026/5/30 23:55:36

CppCon 2024 学习: Dependency Injection in C++ A Practical Guide(续)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CppCon 2024 学习: Dependency Injection in C++ A Practical Guide(续)

data structure preconsolidation

#include<iostream>#include<set>#include<limits>#include<gtest/gtest.h>#include<gmock/gmock.h>#include<memory>// unique_ptr#include<optional>//// OptionalT 模板结构:用于同时存储 bid 与 ask 两个可选字段// 使用 std::optional<T> 表示该字段可能存在,也可能为空(无数据)//template<typenameT>structOptionalT{std::optional<T>bid_;// 可选的买方向(bid)std::optional<T>ask_;// 可选的卖方向(ask)};//// Data 类:构建结果的数据类型// 在实际生产系统中,通常包含证券代码、价格等各种字段// 这里只是一个非常简化的示例//classData{public:std::stringsecName()const{return"IBM";}};//// Tick 类:输入行情信息// append 用于补充文本字段(示例)//classTick{public:voidappend(conststd::string,conststd::string){}};//// 占位类型:在真实系统中这些结构表示买卖方向、经纪商、收益率等信息。// 在本示例中,它们只是空结构,表示类型的存在。//structSide{};structBroker{};structYield{};//// Collector:负责根据 Tick + 各种可选字段拼装出 Data。// 通过函数重载提供两个版本:// 1) 基础字段版(仅 bid/ask/localBid/localAsk)// 2) 扩展字段版(额外包含 Broker、Yield 信息)//// 在真实系统中,这里会包含大量业务逻辑。// 在此示例仅展示“依赖内容扩展”的模式。//structCollector{// 基础版 getData(5 参数版本)DatagetData(constTick&,conststd::optional<Side>&bid,conststd::optional<Side>&ask,conststd::optional<Side>&localBid,conststd::optional<Side>&LocalAsk)const;// 扩展版 getData(9 参数版本)DatagetData(constTick&,conststd::optional<Side>&bid,conststd::optional<Side>&ask,conststd::optional<Side>&localBid,conststd::optional<Side>&LocalAsk,conststd::optional<Broker>&bidBroker,conststd::optional<Broker>&askBroker,conststd::optional<Yield>&bidYield,conststd::optional<Yield>&askYield)const;};//// Builder 基类:// 提供最基础行情构建逻辑,只使用最基础的可选字段(bid/ask/localBid/localAsk)//// build() 调用基础版 Collector::getData()// 使用了虚函数,以便子类可以扩展构建逻辑(典型的面向对象扩展模式)//classBuilder{public:// 虚函数,允许派生类覆盖virtualvoidbuild(constTick&tick)const{// 调用 5 参数版本 getData()Data info=Collector().getData(tick,bid_,ask_,localAsk_,localBid_);(void)info;// 避免未使用变量警告}protected:// 基础 Side 信息,均为可选(std::optional)std::optional<Side>bid_;// 买方向 Sidestd::optional<Side>ask_;// 卖方向 Sidestd::optional<Side>localBid_;// 本地买方向std::optional<Side>localAsk_;// 本地卖方向};//// DBuilder:从 Builder 扩展的派生类// 新增 Broker 与 Yield 两类扩展字段//// 覆盖 build(),调用扩展版 getData(9 参数版本)//// 这是面向对象扩展(Vertical Abstraction)的典型示例:// - 基类定义通用构建逻辑// - 派生类增加字段与功能//structDBuilder:publicBuilder{// 覆盖 build(),使用扩展字段virtualvoidbuild(constTick&tick)constoverride{// 调用 9 参数版本 getData()// ★ 注意:下面代码中第二个 Broker 使用 bidBroker_,属于明显的代码错误。// ★ 正确写法应传入 askBroker_,此处保留原样,仅添加注释说明。Data info=Collector().getData(tick,bid_,ask_,localAsk_,localBid_,bidBroker_,// 正确:bidBroker_bidBroker_,// 错误:应为 askBroker_bidYield_,askYield_);(void)info;}protected:// 扩展字段(均为 optional)std::optional<Broker>bidBroker_;// 买方向经纪商std::optional<Broker>askBroker_;// 卖方向经纪商std::optional<Yield>bidYield_;// 买方向收益率std::optional<Yield>askYield_;// 卖方向收益率};//// 程序入口// Builder 和 DBuilder 可分别用于构建不同粒度的数据模型//intmain(int,char**){// Builder builder;// DBuilder d_builder;// builder.build(tick);// d_builder.build(tick);return0;}

整体结构讲解

这段代码模拟一个“行情构建器(Builder)”体系:

  • Builder:基础构建器,拥有基础字段(bid/ask、本地 bid/ask)
  • DBuilder:扩展构建器,加入更多字段(Broker、Yield 等)
  • Collector:负责根据传入字段收集Data
  • build():根据 tick 和各种 optional 字段构建数据
    关键点是:
  • 同名函数Collector::getData(...)重载
  • BuilderDBuilder通过虚函数实现“依赖注入式的字段扩展”
  • std::optional<T>表达可能缺失的字段
  • DBuilder 调错参数(bug)
    下面开始逐段解释。

模板 OptionalT

template<typenameT>structOptionalT{std::optional<T>bid_;std::optional<T>ask_;};

理解:

这是一个用于存储“买(bid)”与“卖(ask)”可选值的模板结构。

  • 使用std::optional<T>表示这个字段可能存在,也可能不存在
  • 适用于诸如:
    • 价格
    • 交易方向
    • 商品
    • Broker
    • Yield
      这种“一买一卖”的情况。
      数学上:
  • optional 表示一个集合要么是x{x}x,要么是∅\varnothing

Data 与 Tick 类

classData{public:std::stringsecName()const{return"IBM";}};classTick{public:voidappend(conststd::string,conststd::string){}};
  • Data是最终构建的数据对象
  • Tick是输入行情
    简单示例,实际生产环境会非常复杂。

Side / Broker / Yield

structSide{};structBroker{};structYield{};

这些只是占位类型(placeholder),示意 bid/ask 方向信息、经纪商信息、收益率信息。

Collector:用于收集数据的组件

Collector 有两个重载:

DatagetData(constTick&,conststd::optional<Side>&bid,conststd::optional<Side>&ask,conststd::optional<Side>&localBid,conststd::optional<Side>&LocalAsk)const;

基础版:传入 bid/ask + 本地 bid/ask。
第二个重载:

DatagetData(constTick&,conststd::optional<Side>&bid,conststd::optional<Side>&ask,conststd::optional<Side>&localBid,conststd::optional<Side>&LocalAsk,conststd::optional<Broker>&bidBroker,conststd::optional<Broker>&askBroker,conststd::optional<Yield>&bidYield,conststd::optional<Yield>&askYield)const;

扩展版:加入 Broker 和 Yield 信息。
这体现了函数重载 + 可选字段扩展

Builder 基类:基础字段构建器

classBuilder{public:virtualvoidbuild(constTick&tick)const{Data info=Collector().getData(tick,bid_,ask_,localAsk_,localBid_);(void)info;}protected:std::optional<Side>bid_;std::optional<Side>ask_;std::optional<Side>localBid_;std::optional<Side>localAsk_;};

解释:

  • Builder::build()调用的是五参数版 getData()
  • 使用基础字段:
    • bid_
    • ask_
    • localBid_
    • localAsk_
      一种典型生产场景是:
  • Builder部署在市场数据系统中,处理基础行情数据
  • 字段不一定完整,因此用std::optional来表达“不确定性”
    数学意义:
  • 假设SSS是交易方向集合(side)
  • 对任意字段xxx,其值属于集合:
    x∈S∪∅x \in S \cup {\varnothing}xS

DBuilder:扩展字段构建器

structDBuilder:publicBuilder{virtualvoidbuild(constTick&tick)constoverride{Data info=Collector().getData(tick,bid_,ask_,localAsk_,localBid_,bidBroker_,bidBroker_,// ← 这里明显有 bugbidYield_,askYield_);(void)info;}protected:std::optional<Broker>bidBroker_;std::optional<Broker>askBroker_;std::optional<Yield>bidYield_;std::optional<Yield>askYield_;};

解析:

DBuilder 扩展了更多 optional 字段,这很符合生产环境“字段随业务增加”的需求。
但:
BUG:传入参数错误

Collector().getData(...,bidBroker_,bidBroker_,// 第二个应该是 askBroker_

正确应该是:

Collector().getData(...,bidBroker_,askBroker_,

为什么使用 std::optional?

因为行情系统中的真实数据“不连续、不完整”,每个字段可能存在或缺失。
举例:

  • timestamp 有值
  • bid 可能没有(比如只有卖盘)
  • yield 可能只有 ask 端
    因此 optional 表示一个“半定义的字段集合”:
    F=f1,f2,f3,…∣fi∈Vi∪∅F = { f_1, f_2, f_3, \ldots \mid f_i \in V_i \cup {\varnothing} }F=f1,f2,f3,fiVi

主函数

intmain(int,char**){// Builder builder;// DBuilder d_builder;}

只是示例。

这段代码展示的设计思想(非常重要)

1.依赖注入(DI)

Collector()是外部依赖,通过调用方式 DI 进入 Builder。

2.层次化扩展(Vertical Abstraction)

Builder → DBuilder 通过虚函数扩展功能。

3.横向扩展字段(Horizontal Abstraction)

optional 字段可以组合成多种数据模型。

4.函数重载实现多版本数据提取

基础 getData
扩展 getData

data structure consolidation 1

  • 为什么要做结构整合(consolidation)
  • 每个类型的用途
  • 为什么 OptionalT 被升级成 SidePair / BrokerPair / YieldPair
  • 为什么 Collector 现在只需要两个(或四个)参数,而不是很多参数
  • Builder / DBuilder 的面向对象扩展逻辑,以及与 DI 的关系

已添加详细注释的代码(带结构整合解释)

#include<iostream>#include<set>#include<limits>#include<gtest/gtest.h>#include<gmock/gmock.h>#include<memory>// unique_ptr#include<optional>//// OptionalT<T>// 这是一个用于统一表示“成对字段”的结构,例如:// - bid / ask// - 本地 bid / 本地 ask// - bidBroker / askBroker// - bidYield / askYield//// 在真实行情或交易系统中,bid/ask 成对出现非常常见,使用 OptionalT 可以:// 1. 避免两个字段到处散落// 2. 降低 Collector::getData 参数数量,增强可维护性// 3. 强化数据语义:这些字段本来就是成对的(Pair)//template<typenameT>structOptionalT{std::optional<T>bid_;// 买方向(bid)std::optional<T>ask_;// 卖方向(ask)};//// Data:用于封装 Collector 的结果。// 在真实系统中可能包含多种数据字段,这里仅用于展示结构。// secName() 代表结果中可能包含证券信息。//classData{public:std::stringsecName()const{return"IBM";}};//// Tick:行情数据输入。// append 用于附加字串信息,此处只是示意。// 在生产系统中 Tick 通常包含时间、成交价、盘口等丰富字段。//classTick{public:voidappend(conststd::string,conststd::string){}};//// 以下结构为业务中常见的数据类型:// - Side:买卖方向// - Broker:经纪商// - Yield:收益率(或利率)// 在此示例中简化为空结构,仅用于标记类型用途。//structSide{};structBroker{};structYield{};//// 使用别名让 OptionalT<T> 语义更清晰:// SidePair = { bid_:Side, ask_:Side }// BrokerPair = { bid_:Broker, ask_:Broker }// YieldPair = { bid_:Yield, ask_:Yield }//// 这是“数据结构整合(consolidation)”的重要一步:// - 将大量散乱的字段聚拢为语义明确的组件// - 减少 Builder / Collector 的方法签名复杂度// - 强化类型安全//usingSidePair=OptionalT<Side>;usingBrokerPair=OptionalT<Broker>;usingYieldPair=OptionalT<Yield>;//// Collector:// 负责将 Tick + 多种输入字段整合为 Data(数据构建步骤)。// 本示例展示:Collector 的参数结构从“5~9 个离散参数”// 重构成“两个结构化 Pair + 两个扩展结构 Pair”//// getData 现在有两个版本:// 1) 基础版本:只处理 SidePair// 2) 扩展版本:处理 SidePair + BrokerPair + YieldPair//// 这样的设计让函数签名语义更清晰,也使 Builder 的扩展更容易。//structCollector{// 基础版:仅包含方向信息(sides + localSides)DatagetData(constTick&tick,constSidePair&sides,// 全局 bid/askconstSidePair&localSides// 本地 bid/ask)const;// 扩展版:额外包含经纪商 + 收益率DatagetData(constTick&tick,constSidePair&sides,constSidePair&localSides,constBrokerPair&brokers,// bid/ask 对应的经纪商constYieldPair&yields// bid/ask 对应的收益率)const;};//// Builder:// ==========// 负责构建最基础的数据处理逻辑(只需要 SidePair)。//// 特点:// - build() 是虚函数,允许派生类扩展(典型 OOP 垂直抽象)// - 使用结构整合后的 Pair 类型,减少多个 optional 字段// - 依赖 Collector,但无须知道 Collector 内部实现//classBuilder{public:// 虚函数:子类 DBuilder 会覆盖该方法virtualvoidbuild(constTick&tick)const{// 调用基础版 getData()Data info=Collector().getData(tick,sides_,localSides_);(void)info;// 避免未使用变量}protected:// 使用结构整合的 SidePair 替代4个 optional 字段SidePair sides_;// 全局 bid / askSidePair localSides_;// 本地 bid / ask};//// DBuilder:// ==========// Builder 的扩展版本——增加了 BrokerPair 和 YieldPair。// 使用数据结构整合,让扩展字段的添加非常自然,不必在 getData 中新增 4 个参数。//// 如果不做结构整合,你会得到:// getData(tick, bid, ask, localBid, localAsk, bidBroker, askBroker, bidYield, askYield);//// 这种签名非常难维护。使用结构整合后,扩展字段变为组合式:// getData(tick, sides, localSides, brokers, yields);//// 架构可读性和扩展性大幅增强.//structDBuilder:publicBuilder{// 覆盖 build(),调用包含扩展字段的 getData()virtualvoidbuild(constTick&tick)constoverride{Data info=Collector().getData(tick,sides_,localSides_,brokers_,yields_);(void)info;}protected:BrokerPair brokers_;// bid / ask 对应经纪商YieldPair yields_;// bid / ask 对应收益率};//// main()// 演示 Builder 与 DBuilder 的使用场景。//(在实际系统中会有更多功能与测试)//intmain(int,char**){// Builder builder;// DBuilder d_builder;// builder.build(tick);// d_builder.build(tick);return0;}

结构整合(Data Structure Consolidation)背后的核心思想(解释)

你这一页 PPT(或课程内容)主要想表达的是:

1. 将多个散乱字段整合为语义明确的数据结构

例如:
原来:

std::optional<Side> bid_; std::optional<Side> ask_; std::optional<Side> localBid_; std::optional<Side> localAsk_;

整合为:

SidePair sides_; SidePair localSides_;

功能一致,但代码整洁许多。

2. 减少参数数量,增强 Collector API 的可维护性

以前 Collector::getData 可能有 8~9 个参数:
这很容易出错,并且难以扩展。
整合后只需要:

  • 两个 pair(基础版)
  • 四个 pair(扩展版)
    可读性提高非常明显。

3. 为 Dependency Injection / Refactoring 做结构上的准备

结构整合后的 Builder 体系:

  • 更容易引入依赖注入(Template DI / Interface DI)
  • 更容易 mock Collector
  • 更容易扩展(增加字段不需要改很多代码)
    这是 DI 重构的典型实践方式。

理解这段代码的设计意图、类型结构、模板使用方式、继承行为、重载解析规则等。

#include<iostream>#include<set>#include<limits>#include<gtest/gtest.h>#include<gmock/gmock.h>#include<memory>// unique_ptr#include<optional>// std::optional//-------------------------------// OptionalT<T>// 模板结构:封装两个可选值(bid_ / ask_)// 用于描述一类数据的买/卖两个方向,例如 Side、Broker、Yield 等。//-------------------------------template<typenameT>structOptionalT{std::optional<T>bid_;// "买"方向可选值std::optional<T>ask_;// "卖"方向可选值};//-------------------------------// Data:示例数据结构// secName() 返回固定字符串,仅用于演示。//-------------------------------classData{public:std::stringsecName()const{return"IBM";}};//-------------------------------// Tick:行情信息容器// append() 是示例函数,实际中会向 Tick 增加一个字段。//-------------------------------classTick{public:voidappend(conststd::string,conststd::string){}};// 示例空类型:实际场景可能包含多种属性structSide{};structBroker{};structYield{};//-------------------------------// 类型别名:将 OptionalT<T> 用于不同的数据类型// SidePair = OptionalT<Side>// 代表两个可选 Side:bid/ask//-------------------------------usingSidePair=OptionalT<Side>;usingBrokerPair=OptionalT<Broker>;usingYieldPair=OptionalT<Yield>;//-------------------------------// SideInfo:与“买/卖方向”相关的所有信息// sides -> 全局 Side 数据// localSides -> 本地 Side 数据(例如订单本地状态)//-------------------------------structSideInfo{SidePair sides;// 全局方向信息SidePair localSides;// 本地方向信息};//-------------------------------// ExtraFields:扩展字段,用于 DBuilder// 包含与 Broker(经纪商)和 Yield(收益率)相关的额外信息。//-------------------------------structExtraFields{BrokerPair brokers_;YieldPair yields_;};//-------------------------------// Collector:数据收集器// 提供两个重载版本的 getData:// (1) 只需要 Tick 和 SideInfo// (2) 需要 Tick、SideInfo 和 ExtraFields//// 这是典型的函数重载(overload)。//-------------------------------structCollector{// 版本一:无 ExtraFields,基础构造DatagetData(constTick&,constSideInfo&)const;// 版本二:带 ExtraFields,用于 DBuilder 的扩展信息DatagetData(constTick&,constSideInfo&,constExtraFields&)const;};//-------------------------------// Builder:构建器基类。// build() 使用基础信息(SideInfo)调用 getData()。//// 设计重点:// - build() 是 virtual,支持派生类 override。// - sides_ 在基类内部维护。//// 编译时解析:// 调用 Collector().getData(tick, sides_) 时,// 因为参数数量为 2,编译器选择第一个 getData 重载。//-------------------------------classBuilder{public:// 基类版本:只根据 SideInfo 构建 Datavirtualvoidbuild(constTick&tick)const{Data info=Collector().getData(tick,sides_);(void)info;// 避免未使用变量警告}protected:SideInfo sides_;// 基础方向相关信息};//-------------------------------// DBuilder:派生类// 扩展 Builder,引入 ExtraFields。// 重写 build(),调用带 3 个参数的 getData()。//// 原理:// 因为 build() 中传入 (tick, sides_, extraFields_)// 参数数量为 3 → 编译期自动选择 getData(Tick, SideInfo, ExtraFields)//-------------------------------structDBuilder:publicBuilder{// 覆盖基类 build()virtualvoidbuild(constTick&tick)constoverride{Data info=Collector().getData(tick,sides_,extraFields_);(void)info;}protected:ExtraFields extraFields_;// 扩展字段,由派生类维护};intmain(int,char**){// 示例:实际中可以构造不同 Builder 并选择调用不同版本的 build()// Builder builder;// DBuilder d_builder;return0;}

整体代码结构概览

这段代码演示了:

  1. 使用std::optional<T>和模板封装的OptionalT<T>
  2. Builder 设计模式的雏形
  3. 数据收集器 Collector 的函数重载
  4. 派生类 DBuilder覆盖(override)基类 Builder 的 build
  5. 在 build 中根据是否存在 ExtraFields调用不同的Collector::getData(...)重载
    可以看成下面的图示关系:
+----------------+ | Collector | +----------------+ / getData(a) / getData(a,b) +----------------+ +----------------------+ | Builder | -------> | sides_ (SideInfo) | +----------------+ +----------------------+ ^ | +------------------+ | DBuilder | +------------------+ | extraFields_ (...)|

1. OptionalT 模板结构

template<typenameT>structOptionalT{std::optional<T>bid_;std::optional<T>ask_;};

理解

OptionalT<T>是一个模板类型,内部存放:

  • 一个可选值bid_
  • 一个可选值ask_
    类似于:
    OptionalT(T)=(optional(T), optional(T)) \text{OptionalT}(T) = (\text{optional}(T),\ \text{optional}(T))OptionalT(T)=(optional(T),optional(T))
    也就是说,每种类型都可以对应两个可选的数据位:bid(买)和 ask(卖)
    例如:
SidePair=OptionalT<Side>;

等价于

SidePair { optional<Side> bid_; optional<Side> ask_; }

2. Data、Tick、Side/Broker/Yield 等简单结构

classData{public:std::stringsecName()const{return"IBM";}};

一个简单的数据类,返回证券名称。
Tick、Side、Broker、Yield 都是空类,只是占位符(示意数据结构)。

3. Collector:两个 getData 重载

structCollector{DatagetData(constTick&,constSideInfo&)const;DatagetData(constTick&,constSideInfo&,constExtraFields&)const;};

理解

Collector提供两种不同签名的getData

  • 无 ExtraFields 时:
    getData(Tick, SideInfo) \text{getData}( \text{Tick},\ \text{SideInfo} )getData(Tick,SideInfo)
  • 有 ExtraFields 时:
    getData(Tick, SideInfo, ExtraFields) \text{getData}( \text{Tick},\ \text{SideInfo},\ \text{ExtraFields} )getData(Tick,SideInfo,ExtraFields)
    这是典型的函数重载(overloading)
    C++ 会根据传入参数个数选择正确的版本。

4. Builder 基类

classBuilder{public:virtualvoidbuild(constTick&tick)const{Data info=Collector().getData(tick,sides_);(void)info;}protected:SideInfo sides_;};

理解

Builder:

  • 有一个SideInfo sides_成员
  • build() 默认使用两个参数版本的 getData:
    getData(tick, sides) \text{getData}(tick,\ sides_)getData(tick,sides)

5. DBuilder 派生类

structDBuilder:publicBuilder{virtualvoidbuild(constTick&tick)constoverride{Data info=Collector().getData(tick,sides_,extraFields_);(void)info;}protected:ExtraFields extraFields_;};

理解

DBuilder:

  • 在基类 fields 之外,扩展了ExtraFields extraFields_
  • 覆盖(override)了 build()
    使用三个参数版本:
    getData(tick, sides, extraFields) \text{getData}(tick,\ sides_,\ extraFields_)getData(tick,sides,extraFields)
    也就是说:
    | 类 | 调用的 getData 重载版本 |
    | -------- | ---------------- |
    | Builder | 两参数版本 |
    | DBuilder | 三参数版本 |

6. 继承与重载选择如何工作?

在 C++ 中,重载解析(overload resolution)发生在编译期。
当你在 Builder 的 build() 中写:

Collector().getData(tick,sides_);

编译器看到参数数量是 2 个 → 自动选择两参版本getData(Tick, SideInfo)
在 DBuilder 的 build() 中写:

Collector().getData(tick,sides_,extraFields_);

参数数量是 3 → 自动选择三参版本。
没有运行时开销,也没有动态多态参与(因为不是 virtual)。

7. 这段代码的设计意图

总结来说:

Builder 处理基础数据(仅 SideInfo)

DBuilder 扩展 Builder,可以处理更多的数据(ExtraFields)

这是一种增量式构建器(Incremental Builder Pattern)的典型结构。

8. main 函数

intmain(int,char**){// Builder builder;// DBuilder d_builder;}

这里只是示意,不实际执行任何逻辑。

整段代码的总结(关键点)

  • OptionalT<T>使用模板将 optional 封装成买/卖双值结构。
  • Collector::getData()有两个重载,一个处理基础数据,一个处理扩展数据。
  • Builder只有SideInfo,只能调用两参数的 getData。
  • DBuilder增加了ExtraFields,可以调用三参数的 getData。
  • build() 是虚函数,派生类覆盖它以实现额外的数据构建逻辑。
  • 重载选择基于参数数量,发生在编译期。

Lazy Initialization, no DI

#include<iostream>#include<set>#include<limits>#include<gtest/gtest.h>#include<gmock/gmock.h>#include<memory>// 提供 std::unique_ptr 智能指针管理动态对象生命周期// 表示某个动作或操作的结构体structActionX{ActionX(conststd::string&action):action_(action){};// 构造函数初始化动作名称std::string action_;// 保存动作标识或内容};// 数据缓存结构体,当前为空,实际使用中可存储业务数据或状态structDataCache{};// 数据加载辅助类,用于加载交易或行情数据structDBHelper{DBHelper(conststd::string&){};// 构造函数接受索引名,当前为空实现voidload(int){};// 加载指定 key 的数据,当前为空实现DataCache data_;// 内部存储加载后的数据};// 工厂函数,用于根据 index_name 创建 DBHelper 对象// 在懒加载场景中延迟对象构造DBHelpercreateDbHelper(conststd::string&index_name){};// Trade 交易对象,支持懒加载 DBHelperstructTrade{// 构造函数,初始化 key 和 index_nameTrade(intkey,conststd::string&index_name):key_(key),index_name_(index_name){};// 确保 DBHelper 已经加载(懒加载机制)voidensureLoaded(){// 如果 db_helper_ 为空,则创建 DBHelper 对象if(!db_helper_)createDbHelper(index_name_);// 注意:这里实际未赋值给 db_helper_,懒加载未完成// 调用 load 方法加载 key_ 对应的数据db_helper_->load(key_);}// 执行某个动作voidapply(constActionX&action){ensureLoaded();// 确保数据已经加载// 实际处理动作逻辑}intkey_;// 交易 keyconststd::string index_name_;// 交易索引名称std::unique_ptr<DBHelper>db_helper_;// DBHelper 智能指针(懒加载依赖)};// 测试示例(已注释)// TEST(ActionXHandlerTests, test_msg) {// Request req;// ActionX action{"2"};// ActionXHandler handler;// EXPECT_NO_THROW(handler.execute(action));// EXPECT_EQ(TestRequest.msg_, "2");// }intmain(int,char**){// 示例初始化,实际测试调用// ActionXHandler handler;// return RUN_ALL_TESTS();}
#include<iostream>#include<set>#include<limits>#include<gtest/gtest.h>#include<gmock/gmock.h>#include<memory>// unique_ptr
  • 引入标准库和 Google Test / Mock。
  • unique_ptr用于管理动态分配对象的生命周期。
structActionX{ActionX(conststd::string&action):action_(action){};std::string action_;};
  • ActionX:动作封装类型。
  • 构造时传入动作名称。
  • 成员action_存储动作内容。
structDataCache{};
  • DataCache:示例缓存结构,空壳。
  • 在真实系统中可能存储行情、交易数据等。
structDBHelper{DBHelper(conststd::string&){};voidload(int){};DataCache data_;};
  • DBHelper:数据库辅助类。
  • 构造函数接收索引名,但这里为空实现。
  • load(int)用于加载数据(这里空实现)。
  • 内部持有DataCache data_,用于存储已加载数据。
DBHelpercreateDbHelper(conststd::string&index_name){};
  • 工厂函数:根据index_name创建DBHelper
  • 目前为空实现。
  • 在懒加载方案中通常用于延迟实例化依赖对象。
structTrade{Trade(intkey,conststd::string&index_name):key_(key),index_name_(index_name){};
  • Trade:业务对象,例如处理交易或订单。
  • 构造函数接受:
    • key_:交易标识
    • index_name_:索引名
  • 成员初始化列表用于初始化key_index_name_
  • 注意db_helper_未在构造函数中初始化 → 支持懒加载。
voidensureLoaded(){if(!db_helper_)createDbHelper(index_name_);db_helper_->load(key_);}
  • ensureLoaded()
    • 检查db_helper_是否已创建。
    • 若未创建,则调用createDbHelper(index_name_)
    • 然后调用db_helper_->load(key_)进行数据加载。
  • 问题点
    • 这里调用createDbHelper()后,没有把返回的对象赋值给db_helper_,懒加载实际上未真正完成。
    • 懒加载依赖实例化延迟,通常通过db_helper_ = createDbHelper(index_name_);完成。
voidapply(constActionX&action){ensureLoaded();}
  • apply()
    • 执行某个动作前,确保依赖db_helper_已经加载。
    • 这是典型的懒加载模式
      • 依赖对象的创建延迟到首次使用。
      • 但这种方式不能通过依赖注入传入已构建对象
intkey_;conststd::string index_name_;std::unique_ptr<DBHelper>db_helper_;};
  • 成员变量:
    • key_:交易标识
    • index_name_:交易索引
    • db_helper_:依赖对象,动态创建,用unique_ptr管理生命周期。
  • 关键点
    • 懒加载模式下,db_helper_在第一次调用ensureLoaded()时创建。
    • 缺点:无法外部注入,依赖内部工厂函数 →不符合依赖注入原则
intmain(int,char**){// ActionXHandler handler;// return RUN_ALL_TESTS();}
  • 空主函数示例。
  • 目前未执行任何测试。
  • 原理上可以用 Google Test 对Trade类的apply()进行单元测试。

理解总结

  1. Lazy Initialization(懒加载)
    • 目标:延迟创建DBHelper,直到第一次真正使用。
    • 优点:节省启动/构造开销。
    • 缺点:无法通过依赖注入(DI)提供已构建对象。
    • 注意:本示例ensureLoaded()未正确赋值给db_helper_,懒加载不完整。
  2. 依赖注入(DI)对比
    • 如果想做依赖注入
      • 可以通过构造函数注入:Trade(int key, const std::string& idx, std::unique_ptr<DBHelper> db)
      • 或者通过Provider Injection:传入std::function<std::unique_ptr<DBHelper>(const std::string&)>
  3. 典型问题
    • 内部依赖工厂创建 → 违反 “控制反转(IoC)” 原则。
    • 测试困难:无法传入 MockDBHelper,只能依赖真实创建逻辑。

Lazy Initialization, proper DI

#include<iostream>#include<set>#include<limits>#include<gtest/gtest.h>#include<gmock/gmock.h>#include<memory>// 提供 std::unique_ptr 智能指针#include<functional>// 提供 std::function// 表示某个动作或操作的结构体structActionX{ActionX(conststd::string&action):action_(action){};// 构造函数初始化动作std::string action_;// 保存动作标识};// 数据缓存结构体,用于存储 DBHelper 加载的数据structDataCache{};// 数据加载辅助类,模拟交易或行情数据加载structDBHelper{DBHelper(conststd::string&){};// 构造函数接受索引名称voidload(int){};// 加载指定 key 的数据DataCache data_;// 存储加载的数据};// 工厂函数,用于创建 DBHelper 对象std::unique_ptr<DBHelper>createDbHelper(conststd::string&index_name){returnstd::make_unique<DBHelper>(index_name);}// 依赖提供者类型:提供 DBHelper 对象的函数类型usingProvideDBHelper=std::function<std::unique_ptr<DBHelper>(conststd::string&)>;// Trade 交易对象,支持懒加载和依赖注入structTrade{// 构造函数// 参数:// key:交易标识// index_name:交易索引名称// provide_dbhelper:DBHelper 提供者,默认使用 createDbHelper(实现依赖注入)Trade(intkey,conststd::string&index_name,ProvideDBHelper provide_dbhelper=createDbHelper):key_(key),index_name_(index_name),provide_dbhelper_(provide_dbhelper){};// 确保 DBHelper 已经加载voidensureLoaded(){if(!db_helper_){// 如果 db_helper_ 为空,则使用提供者创建db_helper_=provide_dbhelper_(index_name_);// 依赖注入点}db_helper_->load(key_);// 加载 key_ 对应的数据}// 执行动作voidapply(constActionX&action){ensureLoaded();// 确保 DBHelper 已加载// 后续可加入动作处理逻辑}intkey_;// 交易 keyconststd::string index_name_;// 交易索引名称ProvideDBHelper provide_dbhelper_;// DBHelper 提供者(依赖注入)std::unique_ptr<DBHelper>db_helper_;// 懒加载 DBHelper};// 示例测试代码(已注释)// TEST(ActionXHandlerTests, test_msg) {// Request req;// ActionX action{"2"};// ActionXHandler handler;// EXPECT_NO_THROW(handler.execute(action));// EXPECT_EQ(TestRequest.msg_, "2");// }intmain(int,char**){// 这里可以实例化 Trade 对象并调用 apply 测试// Trade trade(1, "IBM");// trade.apply(ActionX("Buy"));}

理解要点:

  1. 懒加载(Lazy Initialization)
    • db_helper_只有在首次调用ensureLoaded()时才创建对象。
    • 避免在对象构造时立即创建依赖,提高性能和灵活性。
  2. 依赖注入(Dependency Injection, DI)
    • 通过构造函数将ProvideDBHelper注入Trade
    • 测试时可以传入 Mock 的提供者,实现单元测试与替换。
  3. 智能指针管理
    • std::unique_ptr自动管理 DBHelper 生命周期,避免内存泄漏。
  4. 灵活性和可测试性
    • 默认使用createDbHelper提供者。
    • 可在测试中注入不同实现(如 MockDBHelper),不改变Trade类代码。
  5. 调用逻辑
    • apply()方法负责动作执行,同时确保依赖已加载。
    • 核心逻辑与依赖解耦,利于维护和扩展。

Inheritance problem with template

#include<iostream>#include<set>#include<limits>#include<gtest/gtest.h>#include<gmock/gmock.h>#include<memory>// 提供 std::unique_ptr// 枚举类型,表示不同模型标签enumclassModelTag{CompoundModel};// 枚举类型,表示不同类型编号enumclassTypeNum{A,B,C};// Calc 类:计算相关的基类classCalc{public:// 获取股息,虚函数,可被 Mock 或子类覆盖virtualdoublegetDividend(doublerate)const;// 设置模型,虚函数,可被 Mock 或子类覆盖virtualvoidsetModel(constModelTag&,intmodelID);// 模板成员函数,判断对象类型template<typenameT>TypeNumisType(constT&)const;// 注意:模板成员函数不能是 virtual};// 模板函数定义,返回默认 TypeNum::Atemplate<typenameT>TypeNumCalc::isType(constT&)const{returnTypeNum::A;}// Processor 类,用于处理 Calc 对象classProcessor{public:voidapply(){// 示例操作intval=0;Calc calc;// 调用模板成员函数// 模板函数在编译期实例化,不依赖虚函数表TypeNum typenum=calc.isType(val);// typenum 此时为 TypeNum::A}};// MockCalc 类,继承自 Calc,用于单元测试classMockCalc:publicCalc{// 使用 gmock 宏模拟虚函数MOCK_CONST_METHOD1(getDividend,double(double));MOCK_METHOD2(setModel,void(constModelTag&,int));// 问题点:// 模板成员函数 isType 无法被 MOCK,因为模板函数不能是 virtual// 所以对于 isType 的行为无法通过 MOCK 直接覆盖// 需要其他方式处理(如 type erasure 或将模板函数改为非模板特化版本)};intmain(int,char**){// 示例注释:// const Base& obj = Derived();// std::cout << "X= 2, result = " << getter(2, obj) << std::endl;// ActionXHandler handler;// return RUN_ALL_TESTS();}

理解要点:

  1. 模板函数与继承
    • template<typename T> TypeNum isType(const T&) const是模板成员函数。
    • 模板函数不能是 virtual,因为虚函数表在编译时固定,而模板函数在实例化时才生成代码。
    • 这意味着MockCalc无法直接对模板函数做 gmock 模拟。
  2. 继承问题
    • 对于Calc的非模板虚函数(如getDividendsetModel),可以直接用 gmock MOCK 宏。
    • 对于模板函数,只能通过类型擦除(type erasure)策略函数/回调函数来实现测试时注入行为。
  3. Processor 中调用模板函数
    • calc.isType(val)是在编译期实例化的模板函数调用,不依赖虚函数表。
    • 所以即便Calc被 Mock,也无法通过 MOCK 直接改变模板函数行为。
  4. 解决方案思路
    • 如果模板函数只用于有限类型,可提供显式特化或非模板重载:
      virtualTypeNumisType(int)const{returnTypeNum::A;}virtualTypeNumisType(conststd::string&)const{returnTypeNum::B;}
    • 或者将模板函数改为模板策略函数注入(Template DI),在调用时传入不同的策略。

Inheritance problem with template fixed

#include<iostream>#include<set>#include<limits>#include<gtest/gtest.h>#include<gmock/gmock.h>#include<memory>// 提供 std::unique_ptr#include<functional>// 提供 std::function// 枚举类型,表示模型标签enumclassModelTag{CompoundModel};// 枚举类型,表示类型编号enumclassTypeNum{A,B,C};// Calc 类:计算相关的基类classCalc{public:// 获取股息,虚函数,可被 Mock 或子类覆盖virtualdoublegetDividend(doublerate)const;// 设置模型,虚函数,可被 Mock 或子类覆盖virtualvoidsetModel(constModelTag&,intmodelID);// 模板成员函数,用于判断类型template<typenameT>TypeNumisType(constT&)const;// 模板函数不能是 virtual};// 模板函数实例化,默认返回 TypeNum::Atemplate<typenameT>TypeNumCalc::isType(constT&)const{returnTypeNum::A;}// 定义一个独立函数 test_typenum,用于测试/替代模板函数TypeNumtest_typenum(constCalc&,int){returnTypeNum::A;}// 定义一个真实行为函数 real_typenum,调用 Calc 的模板函数TypeNumreal_typenum(constCalc&calc,intval){returncalc.isType(val);}// Processor 类,用于处理 Calc 对象,解决模板函数无法虚拟化问题classProcessor{public:// 定义类型别名:函数类型 std::functionusingis_type_fn=std::function<TypeNum(constCalc&,int)>;// 构造函数,支持依赖注入// 默认使用 real_typenum,也可以注入 test_typenum 或其他策略函数Processor(is_type_fn istype=real_typenum):istype_(istype){}voidapply(){// 示例操作intval=0;Calc calc;// 使用函数对象调用 isType,避免模板函数无法 MOCK 的问题TypeNum typenum=istype_(calc,val);// typenum 此时为 TypeNum::A}is_type_fn istype_;// 保存注入的函数};// MockCalc 类,继承自 Calc,用于单元测试classMockCalc:publicCalc{// gmock 模拟虚函数MOCK_CONST_METHOD1(getDividend,double(double));MOCK_METHOD2(setModel,void(constModelTag&,int));// 模板函数无法直接 MOCK,所以用 Processor 注入策略函数解决};intmain(int,char**){// 可以在这里创建 Processor 并注入 test_typenum// Processor proc(test_typenum);// proc.apply();}

理解要点

  1. 问题背景
    • 原先 Calc 类中的template<typename T> isType(const T&)是模板函数。
    • 模板函数不能是 virtual,因此无法通过继承和 gmock MOCK 来直接替换行为。
    • 这是 C++ 模板与虚函数机制的限制。
  2. 解决方案
    • 定义独立的函数real_typenum,在内部调用模板函数。
    • 定义一个函数类型别名is_type_fn(std::function<TypeNum(const Calc&, int)>)。
    • Processor 类通过构造函数注入is_type_fn,实现依赖注入
    • 这样在单元测试时,可以注入test_typenum替代真实行为,实现对模板函数行为的可控测试。
  3. 依赖注入特点
    • 将模板函数调用从类内部“抽象出来”,通过函数对象注入。
    • 保持原模板函数的灵活性,同时解决了 MOCK 和继承测试的问题。
    • 体现了策略模式 + 依赖注入的设计思想。
  4. 总结
    • 模板函数无法直接虚拟化是 C++ 的限制。
    • 使用函数对象(策略函数)注入,是解决模板函数依赖注入和测试问题的有效方法。
    • 在设计生产代码时,可以通过这种方式将“编译时模板”和“运行时测试注入”解耦。

Inheritance DI

我们对这段“Inheritance DI”的代码做详细解析,并结合依赖注入(DI)的思想说明。

#include<iostream>#include<set>#include<limits>#include<gtest/gtest.h>#include<gmock/gmock.h>#include<memory>// 提供 std::unique_ptr

引入标准库和 Google Test / Google Mock,用于单元测试和模拟。

// CalcEngine 类:计算引擎基类structCalcEngine{virtualboolexecute(){returntrue;}// 执行操作virtualboolapply(){returntrue;}// 应用某种规则virtualboolcalculate(){returntrue;}// 执行计算};
  • 说明
    • CalcEngine是一个虚函数类,定义了三个核心操作。
    • 这些方法都可以在测试或子类中覆盖,实现多态。
    • 返回true作为默认行为,方便生产代码运行。
// MockCalcEngine 类,用于单元测试structMockCalcEngine{MOCK_METHOD0(execute,bool());// 模拟 execute 方法MOCK_METHOD0(apply,bool());// 模拟 apply 方法MOCK_METHOD0(calculate,bool());// 模拟 calculate 方法};
  • 说明
    • 这是一个模拟类,用于在测试中替代真实的CalcEngine
    • 使用 gmock 的MOCK_METHOD0宏定义,模拟无参数的成员函数。
    • 可以在测试中设置预期调用次数、返回值等。
// process 函数:依赖注入示例boolprocess(CalcEngine&engine){// ...engine.apply();// 调用注入的对象// ...returnengine.calculate();// 调用注入的对象并返回结果}
  • 说明
    • 这里的CalcEngine& engine是依赖注入(Dependency Injection)的体现。
    • 关键点
      • process函数不负责创建CalcEngine对象。
      • 对象由外部传入(可以是真实引擎,也可以是 Mock)。
      • 增强了函数的可测试性和可复用性。
// main 函数,示例占位intmain(int,char**){// ActionXHandler handler;// return RUN_ALL_TESTS();}
  • 说明
    • 实际的测试代码未激活。
    • 在完整单元测试中,可以创建MockCalcEngine,注入process,并使用 gmock 验证方法调用。

理解要点

  1. 依赖注入(DI)思想
    • process函数通过引用CalcEngine& engine接收依赖对象。
    • DI 核心思想:不要在函数内部创建依赖对象,而是外部提供
    • 好处:
      • 可测试性强:可以注入 Mock 对象。
      • 可复用性高:不同的 CalcEngine 实现可灵活替换。
      • 解耦函数与依赖实现。
  2. 继承 + DI 的作用
    • CalcEngine提供接口和默认实现。
    • MockCalcEngine继承接口,提供模拟行为。
    • 使用 gmock 可以轻松验证函数是否调用了期望方法。
  3. 函数级别 DI
    • 这里是函数参数注入(Constructor Injection 或 Setter Injection 的一种简化形式)。
    • process完全解耦于具体 CalcEngine 实现。
  4. 总结
    • 这种模式非常适合“函数依赖于接口对象”的场景。
    • 在单元测试中,MockCalcEngine可以注入,验证applycalculate是否被调用。
    • 生产代码中可以注入真实对象,实现业务逻辑。

Inheritance Modern Mock

#include<iostream>#include<set>#include<limits>#include<gtest/gtest.h>#include<gmock/gmock.h>#include<memory>// 提供 std::unique_ptr

引入标准库和 Google Test / Google Mock,用于单元测试和模拟。

// CalcEngine 类:计算引擎基类structCalcEngine{virtualboolexecute(){returntrue;}// 执行某操作,返回成功标记virtualboolapply(){returntrue;}// 应用某规则virtualboolcalculate()const{returntrue;}// 计算结果,const 修饰virtualboolcommit(){returntrue;}// 提交操作};
  • 说明
    • CalcEngine是一个基类接口(虚函数类)。
    • 提供默认实现返回true,方便生产环境直接使用。
    • calculateconst方法,表示不会修改对象状态。
// MockCalcEngine 类:现代 gmock 使用方式structMockCalcEngine:publicCalcEngine{MOCK_METHOD(bool,execute,(),(override));MOCK_METHOD(bool,apply,(),(override));MOCK_METHOD(bool,calculate,(),(override,const));MOCK_METHOD(bool,apply,(),(override));};
  • 说明
    • 现代 gmock 语法为MOCK_METHOD(返回类型, 方法名, 参数列表, 修饰符)
    • override确保模拟方法覆盖基类虚函数。
    • calculate用了const修饰,模拟时也必须加(override, const)
  • 注意
    • 这里apply被重复定义了两次,这是错误的,应删除重复的行。
// process 函数:依赖注入示例boolprocess(CalcEngine&engine){// 依赖注入:process 不创建 CalcEngine,而是使用外部传入对象engine.apply();returnengine.calculate();}
  • 说明
    • process函数通过引用接收CalcEngine对象,实现函数级依赖注入
    • 可以传入真实对象,也可以传入MockCalcEngine,增强测试灵活性。
// main 函数,占位intmain(int,char**){// ActionXHandler handler;// return RUN_ALL_TESTS();}
  • 说明
    • 实际单元测试代码未启用。
    • 在完整测试中,可以创建MockCalcEngine,设置期望调用次数和返回值,并传入process函数验证行为。

理解要点

  1. 依赖注入(DI)
    • 核心思想:不要在函数内部创建依赖对象,而是由外部提供
    • process不依赖具体实现,依赖CalcEngine接口。
  2. 继承 + gmock
    • MockCalcEngine继承CalcEngine,通过现代 gmock 宏MOCK_METHOD模拟虚函数。
    • 支持覆盖const方法(calculate)和非const方法。
    • 可以在测试中控制行为、返回值、调用次数。
  3. 现代 gmock 语法优点
    • 简洁明了,不再使用老式的MOCK_METHOD0/MOCK_METHOD1
    • 支持重载和const修饰符。
    • 自动检查是否正确覆盖虚函数。
  4. 函数可测试性
    • process可接受任意CalcEngine对象,无需修改函数代码即可测试不同场景。
    • 适合单元测试、集成测试。
      注意问题
  • MockCalcEngine中重复定义了apply,应删除其中一个。
  • process函数示例中没有 commit 和 execute 被调用,可以根据业务需求扩展。

template DI

#include<iostream>#include<set>#include<limits>#include<gtest/gtest.h>#include<gmock/gmock.h>#include<memory>// unique_ptr

引入标准库和 Google Test / Google Mock,用于测试和模拟。

structData{std::string data_;};
  • Data 结构体:保存计算结果或处理数据,这里只有一个字符串成员data_
  • 可理解为传递给计算引擎的共享数据结构。
structRealCalcEngine{boolexecute(constData&rdata){(void)rdata;returntrue;}boolapply(constData&rdata){(void)rdata;returntrue;}boolcalculate(constData&rdata){(void)rdata;returntrue;}};
  • RealCalcEngine:真实生产环境的计算引擎。
  • 各方法接收Data&,可以访问或修改数据。
  • 返回值bool表示操作是否成功。
  • (void)rdata;用于避免未使用参数的警告。

模板依赖注入示例

template<typenameCalcEngine>structProcessor{Processor(CalcEngine engine):engine_(engine){};boolprocess(Data&rdata){// 调用依赖注入的引擎方法engine_.apply(rdata);rdata.data_="2";// 修改数据returnengine_.calculate(rdata);}CalcEngine engine_;};
  • 模板 DIProcessorCalcEngine类型是模板参数,不依赖具体实现。
  • 优点
    1. 编译时确定类型,无虚函数开销。
    2. 可以传入任何兼容接口的计算引擎,包括 Mock 引擎。
  • 关键点
    • engine_是模板类型成员,保存注入的依赖。
    • process方法使用engine_Data进行操作。
template<typenameCalcEngine>boolprocess(CalcEngine&engine,Data&rdata){engine.apply(rdata);rdata.data_="2";returnengine.calculate(rdata);}
  • 自由函数模板版本
    • 直接传入模板类型的计算引擎和数据。
    • 和类模板版本功能一致。
    • 优点:不需要包装类即可实现模板 DI。

测试用例

structTestCalcEngine{TestCalcEngine(Data&rdata):rdata_(rdata){};boolapply(constData&rdata){(void)rdata;returntrue;}boolcalculate(constData&rdata){(void)rdata;returntrue;}Data&rdata_;};
  • TestCalcEngine:用于测试的伪计算引擎。
  • 保存对Data的引用,可以在测试中验证数据是否被修改。
  • 不使用虚函数,实现零开销模板 DI。
TEST(ActionXHandlerTests,test_process){Data rdata;TestCalcEnginecalc_eng(rdata);EXPECT_NO_THROW(process(calc_eng,rdata));EXPECT_EQ(rdata.data_,"2");}
  • 说明
    • 使用模板函数process
    • 验证Data.data_是否被process正确修改。
    • EXPECT_NO_THROW确保函数执行不抛异常。
TEST(ActionXHandlerTests,test_processor){Data rdata;TestCalcEnginecalc_eng(rdata);ProcessorProc(calc_eng);// EXPECT_NO_THROW(process(calc_eng, rdata));// EXPECT_EQ(rdata.data_, "2");}
  • 说明
    • 使用类模板Processor
    • 可以在这里调用Proc.process(rdata)测试,但注释掉了。
    • 展示了模板 DI 在类中的应用。
intmain(int,char**){RealCalcEngine calc_eng;Data rdata;process(calc_eng,rdata);}
  • 说明
    • 主函数直接用真实计算引擎调用模板函数process
    • 模板 DI 支持同时在生产环境和测试环境中使用。

理解要点

  1. 模板依赖注入
    • 核心思想:通过模板参数注入依赖,而不是继承虚函数。
    • 优点:
      • 零虚函数开销(编译期静态分发)。
      • 可以传入任意兼容接口的类。
      • 测试时无需 Mock 对象继承,只需实现同名方法。
  2. 类模板 vs 函数模板
    • 类模板:适合包装和复用多次操作。
    • 函数模板:简单直接,适合单次操作。
  3. 测试策略
    • 使用模板 DI 可以轻松传入测试实现。
    • 验证Data被正确修改。
    • 不依赖 Google Mock 或虚函数即可完成。
  4. 对比继承 DI
    • 模板 DI 不需要继承和虚函数。
    • 编译期确定类型,性能更好。
    • 更灵活,适合对小型接口或单个数据操作进行 DI。

template DI with concepts

#include<iostream>#include<set>#include<limits>#include<gtest/gtest.h>#include<gmock/gmock.h>#include<memory>// unique_ptrstructData{std::string data_;// 数据结构,存储计算引擎的输入输出};// C++20 Concepts 定义,要求模板类型必须有 apply 和 calculate 方法,且返回可转换为 booltemplate<typenameT>conceptCalcEngineT=requires(T t,constData&d){{t.calculate(d)}->std::convertible_to<bool>;{t.apply(d)}->std::convertible_to<bool>;};// 真实计算引擎,实现具体逻辑structRealCalcEngine{boolexecute(constData&rdata){(void)rdata;returntrue;}// execute 方法,这里仅占位boolapply(constData&rdata){(void)rdata;returntrue;}// apply 方法boolcalculate(constData&rdata){(void)rdata;returntrue;}// calculate 方法};// 使用模板进行依赖注入的 Processor,要求 CalcEngine 符合 CalcEngineTtemplate<CalcEngineT CalcEngine>structProcessor{Processor(CalcEngine engine):engine_(engine){}// 构造函数注入依赖boolprocess(Data&rdata){engine_.apply(rdata);// 调用 apply 方法rdata.data_="2";// 修改数据returnengine_.calculate(rdata);// 调用 calculate 方法并返回结果}CalcEngine engine_;// 存储计算引擎对象};// 独立函数风格模板 DItemplate<CalcEngineT CalcEngine>boolprocess(CalcEngine&engine,Data&rdata){engine.apply(rdata);// 调用 applyrdata.data_="2";// 修改数据returnengine.calculate(rdata);// 调用 calculate 并返回结果}// 测试专用计算引擎structTestCalcEngine{TestCalcEngine(Data&rdata):rdata_(rdata){}// 构造函数绑定 Data 引用boolapply(constData&rdata){(void)rdata;returntrue;}// apply 方法占位boolcalculate(constData&rdata){(void)rdata;returntrue;}// calculate 方法占位Data&rdata_;// 存储引用,方便测试};// Google Test 单元测试TEST(ActionXHandlerTests,test_process){Data rdata;TestCalcEnginecalc_eng(rdata);EXPECT_NO_THROW(process(calc_eng,rdata));// 调用模板函数 DI,确保不抛异常EXPECT_EQ(rdata.data_,"2");// 验证数据被修改}TEST(ActionXHandlerTests,test_processor){Data rdata;TestCalcEnginecalc_eng(rdata);ProcessorProc(calc_eng);// 构造模板 Processor// EXPECT_NO_THROW(Proc.process(rdata));// EXPECT_EQ(rdata.data_, "2");}intmain(int,char**){RealCalcEngine calc_eng;Data rdata;process(calc_eng,rdata);// 调用真实计算引擎测试模板函数 DI// return RUN_ALL_TESTS();}
#include<iostream>#include<set>#include<limits>#include<gtest/gtest.h>#include<gmock/gmock.h>#include<memory>// unique_ptr
  • 引入标准库、Google Test 和 Google Mock,用于单元测试和模拟对象。
  • memory用于unique_ptr,虽然本例没有用到。
structData{std::string data_;};
  • 定义Data结构体,存储处理数据。
  • 这里仅包含std::string data_字段,可理解为计算引擎输入/输出的数据容器。

定义模板约束 Concept

template<typenameT>conceptCalcEngineT=requires(T t,constData&d){{t.calculate(d)}->std::convertible_to<bool>;{t.apply(d)}->std::convertible_to<bool>;};
  • CalcEngineT是 C++20 的 concept,用于约束模板参数类型。
  • 要求T类型必须有以下方法:
    1. t.calculate(d)返回可转换为bool
    2. t.apply(d)返回可转换为bool
  • 优点
    • 在编译期保证传入模板的类型符合接口要求。
    • 避免运行时的虚函数开销。
    • 提供更清晰的编译期报错,而不是链接期或运行期报错。

真实计算引擎

structRealCalcEngine{boolexecute(constData&rdata){(void)rdata;returntrue;}boolapply(constData&rdata){(void)rdata;returntrue;}boolcalculate(constData&rdata){(void)rdata;returntrue;}};
  • RealCalcEngine:生产环境的计算引擎。
  • execute/apply/calculate接收Data&,返回bool
  • (void)rdata;用于消除未使用参数警告。

类模板 DI

template<CalcEngineT CalcEngine>structProcessor{Processor(CalcEngine engine):engine_(engine){};boolprocess(Data&rdata){engine_.apply(rdata);// 使用依赖引擎rdata.data_="2";// 修改数据returnengine_.calculate(rdata);}CalcEngine engine_;};
  • Processor是模板类,模板参数CalcEngine被约束为CalcEngineT
  • 模板依赖注入
    • 通过构造函数传入具体引擎实例engine_
    • 不依赖继承或虚函数,编译期静态绑定。
    • 支持任意符合 Concept 的计算引擎,包括测试引擎。
  • process方法通过engine_Data进行操作,实现核心逻辑。

函数模板 DI

template<CalcEngineT CalcEngine>boolprocess(CalcEngine&engine,Data&rdata){engine.apply(rdata);rdata.data_="2";returnengine.calculate(rdata);}
  • 自由函数模板版本:
    • 可以直接用任意符合CalcEngineT的对象调用。
    • 不依赖类包装,灵活且零开销。
  • 与类模板版本功能一致,只是使用方式不同。

测试专用计算引擎

structTestCalcEngine{TestCalcEngine(Data&rdata):rdata_(rdata){};boolapply(constData&rdata){(void)rdata;returntrue;}boolcalculate(constData&rdata){(void)rdata;returntrue;}Data&rdata_;};
  • TestCalcEngine用于单元测试:
    • 模拟计算引擎的行为。
    • 可以在测试中验证Data是否被修改。
  • 没有虚函数,实现了轻量级模板 DI。

单元测试

TEST(ActionXHandlerTests,test_process){Data rdata;TestCalcEnginecalc_eng(rdata);EXPECT_NO_THROW(process(calc_eng,rdata));EXPECT_EQ(rdata.data_,"2");}
  • 使用函数模板process
  • 验证:
    1. 函数执行不抛异常。
    2. Data.data_被正确修改为"2"
TEST(ActionXHandlerTests,test_processor){Data rdata;TestCalcEnginecalc_eng(rdata);ProcessorProc(calc_eng);// EXPECT_NO_THROW(process(calc_eng, rdata));// EXPECT_EQ(rdata.data_, "2");}
  • 使用类模板Processor
  • 可以调用Proc.process(rdata)测试。
  • 注释部分表示测试方法可以扩展。

主函数

intmain(int,char**){RealCalcEngine calc_eng;Data rdata;process(calc_eng,rdata);}
  • 在生产环境下使用真实计算引擎调用模板函数process
  • 模板 DI 的好处:
    • 同一套代码同时支持生产引擎和测试引擎。
    • 编译期类型检查,无虚函数开销。

理解总结

  1. 模板 DI (Template Dependency Injection)
    • 核心思想:通过模板参数传递依赖,而不是继承虚函数。
    • 优点
      • 编译期类型检查,性能零开销。
      • 可传入任意符合接口的类型,包括测试对象。
      • 不依赖继承层次,灵活性高。
  2. Concept 约束
    • C++20 Concepts 用于约束模板类型。
    • 保证传入类型在编译期满足接口要求。
    • 提高模板安全性,避免编译器报错过于晦涩。
  3. 类模板 vs 函数模板
    • 类模板:封装依赖,更易复用。
    • 函数模板:轻量,适合单次调用。
  4. 测试策略
    • 用模板 DI 可以轻松传入模拟引擎。
    • 无需继承 Mock 对象,也无需虚函数。
    • 测试更简洁、性能更高。

burying templates in constructor only

// typeErasure.cpp#include<iostream>#include<memory>#include<string>#include<vector>// Object 类实现了类型擦除(Type Erasure),可以存储任意类型对象,只要它有 getName() 方法classObject{// (2)public:// 构造函数模板,可以接收任意类型 Ttemplate<typenameT>// (3)Object(T&&obj):object(std::make_shared<Model<T>>(std::forward<T>(obj))){}// 对外统一接口,调用内部对象的 getName()std::stringgetName()const{// (4)returnobject->getName();}// 抽象基类,定义接口 ConceptstructConcept{// (5)virtual~Concept(){}// 虚析构保证派生类安全销毁virtualstd::stringgetName()const=0;// 纯虚函数接口};// 模板派生类,用于具体类型 T 的实现template<typenameT>// (6)structModel:Concept{Model(constT&t):object(t){}std::stringgetName()constoverride{returnobject.getName();// 调用具体类型的方法}private:T object;// 存储具体类型对象};std::shared_ptr<constConcept>object;// 指向 Concept 的智能指针,实现类型擦除};// 遍历 Object 向量并打印名字voidprintName(std::vector<Object>vec){// (7)for(autov:vec)std::cout<<v.getName()<<std::endl;}// 测试类型 BarstructBar{std::stringgetName()const{// (8)return"Bar";}};// 测试类型 FoostructFoo{std::stringgetName()const{// (8)return"Foo";}};intmain(){std::cout<<std::endl;// 创建 Object 向量,可以存放不同类型对象(Foo、Bar),实现类型擦除std::vector<Object>vec{Object(Foo()),Object(Bar())};// (1)printName(vec);// 打印所有对象的名字std::cout<<std::endl;}

详细理解:

  1. 类型擦除(Type Erasure)思想
    • Object类可以存储任意类型对象,只要对象有getName()方法。
    • 外部代码无需知道对象具体类型,通过统一接口getName()调用。
  2. 实现原理
    • 定义抽象基类Concept,包含纯虚函数getName()
    • Model<T>继承Concept并实现getName(),内部存储具体类型对象T
    • Object持有std::shared_ptr<const Concept>,通过它实现对任意类型对象的统一访问。
  3. 构造函数模板
    • Object(T&& obj)接收任意类型对象,通过std::make_shared<Model<T>>包装。
    • 使用std::forward<T>保持对象的左值/右值属性。
  4. 多态调用
    • Object::getName()调用object->getName(),利用虚函数实现多态。
    • 外部不需要知道对象类型,只需调用统一接口即可。
  5. 应用场景
    • 当需要在容器中存储不同类型对象,但希望统一调用某些方法时非常有用。
    • 避免模板膨胀,提高可维护性。
    • 结合智能指针管理对象生命周期,无需担心手动释放。
  6. 示例
    • vec向量中存储FooBar对象。
    • 调用printName(vec),输出:
      FooBar Foo BarFooBar
    • 完美实现类型擦除和多态访问。

Std:move_only_function example

#include<iostream>#include<set>#include<limits>#include<gtest/gtest.h>#include<gmock/gmock.h>#include<memory>// unique_ptr#include<functional>// std::function// YieldData 结构体,存储收益数据structYieldData{doubledata_;// 收益值};// 使用 std::function 定义传统的可拷贝收益计算器usingCpYieldCalculator=std::function<double(constYieldData&)>;// 使用 move_only_function 定义仅可移动的收益计算器(C++23 或自定义 move-only-function 实现)usingYieldCalculator=std::move_only_function<double(constYieldData&)>;// Processor 类,使用依赖注入方式注入收益计算器structProcessor{// 构造函数:通过依赖注入传入收益计算器Processor(CpYieldCalculator yield_calc):YieldCalculator_(std::move(yield_calc)){}// 将可拷贝计算器存入内部成员// 处理函数,接受 YieldData 并计算收益doubleprocess(YieldData&ydata){// 调用注入的收益计算器autoyield=YieldCalculator_(ydata);// 返回计算结果returnyield;}YieldCalculator YieldCalculator_;// 内部存储收益计算器(仅可移动)};// 测试 ProcessorTEST(Processor,test_process){doubleymultiplier=0.01;// 定义 lambda 收益计算器,捕获 ymultiplierautocalculator=[ymult{ymultiplier}](constYieldData&ydata){returnydata.data_*ymult;// 简单收益计算};Processorprocessor(calculator);// 注入收益计算器YieldData rdata{100};// 测试数据,data_=100autoyield=processor.process(rdata);// 调用 process 计算收益EXPECT_EQ(yield,1);// 验证计算结果是否为 100*0.01 = 1}intmain(int,char**){testing::InitGoogleTest();// 初始化 Google TestreturnRUN_ALL_TESTS();// 运行所有测试}

详细理解:

  1. 核心概念:依赖注入(Dependency Injection)
    • Processor不直接依赖某种固定的收益计算逻辑,而是通过构造函数注入一个函数对象(YieldCalculatorCpYieldCalculator)。
    • 这样可以轻松替换不同的计算策略,例如测试中使用 lambda,生产中可能使用复杂算法。
  2. 类型选择
    • CpYieldCalculator:可拷贝函数对象,使用std::function,可复制和存储。
    • YieldCalculator:仅可移动函数对象(move-only),提高性能和安全性(避免不必要的拷贝),C++23 或自定义实现。
  3. lambda 捕获
    • lambda 捕获ymultiplier,使计算器可以在调用时使用外部参数。
  4. 处理流程
    • Processor::process接收YieldData对象,调用注入的收益计算器YieldCalculator_,返回计算结果。
  5. 测试设计
    • 通过注入测试 lambda,实现对Processor的单元测试。
    • 验证process的输出是否符合预期,确保依赖注入正确工作。
  6. 公式示意
    yield=YieldData.data_×ymultiplier \text{yield} = \text{YieldData.data\_} \times \text{ymultiplier}yield=YieldData.data_×ymultiplier
    • 对应代码中ydata.data_ * ymult
  7. 优点
    • 提高可测试性:可以轻松替换不同计算逻辑进行测试。
    • 提高灵活性和可维护性:不同业务逻辑只需更换注入函数,无需修改Processor内部实现。
    • 支持 move-only 函数对象,避免不必要的内存开销。

be my own mock

#include<iostream>#include<set>#include<limits>#include<gtest/gtest.h>#include<gmock/gmock.h>#include<memory>// unique_ptr// Data 类,表示证券数据classData{public:std::stringsecName()const{return"IBM";}// 返回证券名称};// Tick 类,表示行情信息classTick{public:voidappend(conststd::string,conststd::string){}// 模拟向 Tick 添加字段};// TimesLib 命名空间,提供附加时间数据的功能namespaceTimesLib{voidappendTimes(constData&,Tick&){}// 默认实现}// Builder 类,负责构建 Tick 数据classBuilder{public:// 构建函数,将 Data 信息添加到 Tick 中virtualvoidbuild(constData&data,Tick&tick){tick.append("name",data.secName());// 添加证券名称appendTimes(data,tick);// 调用私有虚函数附加时间数据}private:// 私有虚函数,方便在 MockBuilder 中进行测试替换virtualvoidappendTimes(constData&data,Tick&tick){TimesLib::appendTimes(data,tick);// 默认调用 TimesLib}};// MockBuilder 类,用于单元测试,继承自 BuilderstructMockBuilder:Builder{MOCK_METHOD(void,appendTimes,(constData&data,Tick&tick),(override));// Mock appendTimes};// 测试 Builder::build 是否正确调用 appendTimesTEST(BuilderTests,test_build){Data data;// 测试数据Tick tick;// Tick 对象MockBuilder builder;// 使用 MockBuilderEXPECT_CALL(builder,appendTimes(testing::_,testing::_));// 期望 build 调用 appendTimesbuilder.build(data,tick);// 执行 build,触发 Mock}intmain(int,char**){testing::InitGoogleTest();// 初始化 Google TestreturnRUN_ALL_TESTS();// 运行所有测试}

详细理解:

  1. 核心思想:继承 + Mock 测试
    • Builder类定义了build方法,用于构建Tick对象。
    • appendTimes是私有虚函数,仅在内部被调用,但可以被MockBuilder覆盖,用于单元测试。
    • 这种方式解决了“私有方法可被 Mock”问题,同时保持接口简洁。
  2. 流程分析
    1. 调用builder.build(data, tick)
    2. build内部调用tick.append("name", data.secName())
    3. 随后调用私有虚函数appendTimes
    4. MockBuilder中,这个调用会被 Mock 替换,可验证是否正确调用
  3. 依赖注入与测试
    • appendTimes实际上相当于通过继承和虚函数实现的依赖注入。
    • 在生产环境中,调用的是TimesLib::appendTimes;在测试中,通过 Mock 替换行为。
  4. 公式与数据关系
    • 没有数学公式,本质是方法调用关系:
      Builder::build(data,tick)→tick.append(...)→appendTimes(data,tick) Builder::build(data, tick) \rightarrow tick.append(...) \rightarrow appendTimes(data, tick)Builder::build(data,tick)tick.append(...)appendTimes(data,tick)
  5. 优点
    • 测试灵活:可验证appendTimes是否被正确调用。
    • 保持封装:appendTimes可以是私有,但仍可 Mock。
    • 依赖注入简洁:无需修改原有生产代码,仅通过继承和 Mock 实现测试替换。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!