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:负责根据传入字段收集Databuild():根据 tick 和各种 optional 字段构建数据
关键点是:- 同名函数
Collector::getData(...)重载 Builder和DBuilder通过虚函数实现“依赖注入式的字段扩展”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}x∈S∪∅
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,…∣fi∈Vi∪∅
主函数
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;}整体代码结构概览
这段代码演示了:
- 使用
std::optional<T>和模板封装的OptionalT<T> - Builder 设计模式的雏形
- 数据收集器 Collector 的函数重载
- 派生类 DBuilder覆盖(override)基类 Builder 的 build
- 在 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()进行单元测试。
理解总结
- Lazy Initialization(懒加载)
- 目标:延迟创建
DBHelper,直到第一次真正使用。 - 优点:节省启动/构造开销。
- 缺点:无法通过依赖注入(DI)提供已构建对象。
- 注意:本示例
ensureLoaded()未正确赋值给db_helper_,懒加载不完整。
- 目标:延迟创建
- 依赖注入(DI)对比
- 如果想做依赖注入:
- 可以通过构造函数注入:
Trade(int key, const std::string& idx, std::unique_ptr<DBHelper> db) - 或者通过Provider Injection:传入
std::function<std::unique_ptr<DBHelper>(const std::string&)>
- 可以通过构造函数注入:
- 如果想做依赖注入:
- 典型问题
- 内部依赖工厂创建 → 违反 “控制反转(IoC)” 原则。
- 测试困难:无法传入 Mock
DBHelper,只能依赖真实创建逻辑。
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"));}理解要点:
- 懒加载(Lazy Initialization):
db_helper_只有在首次调用ensureLoaded()时才创建对象。- 避免在对象构造时立即创建依赖,提高性能和灵活性。
- 依赖注入(Dependency Injection, DI):
- 通过构造函数将
ProvideDBHelper注入Trade。 - 测试时可以传入 Mock 的提供者,实现单元测试与替换。
- 通过构造函数将
- 智能指针管理:
std::unique_ptr自动管理 DBHelper 生命周期,避免内存泄漏。
- 灵活性和可测试性:
- 默认使用
createDbHelper提供者。 - 可在测试中注入不同实现(如 MockDBHelper),不改变
Trade类代码。
- 默认使用
- 调用逻辑:
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();}理解要点:
- 模板函数与继承:
template<typename T> TypeNum isType(const T&) const是模板成员函数。- 模板函数不能是 virtual,因为虚函数表在编译时固定,而模板函数在实例化时才生成代码。
- 这意味着
MockCalc无法直接对模板函数做 gmock 模拟。
- 继承问题:
- 对于
Calc的非模板虚函数(如getDividend和setModel),可以直接用 gmock MOCK 宏。 - 对于模板函数,只能通过类型擦除(type erasure)或策略函数/回调函数来实现测试时注入行为。
- 对于
- Processor 中调用模板函数:
calc.isType(val)是在编译期实例化的模板函数调用,不依赖虚函数表。- 所以即便
Calc被 Mock,也无法通过 MOCK 直接改变模板函数行为。
- 解决方案思路:
- 如果模板函数只用于有限类型,可提供显式特化或非模板重载:
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();}理解要点
- 问题背景:
- 原先 Calc 类中的
template<typename T> isType(const T&)是模板函数。 - 模板函数不能是 virtual,因此无法通过继承和 gmock MOCK 来直接替换行为。
- 这是 C++ 模板与虚函数机制的限制。
- 原先 Calc 类中的
- 解决方案:
- 定义独立的函数
real_typenum,在内部调用模板函数。 - 定义一个函数类型别名
is_type_fn(std::function<TypeNum(const Calc&, int)>)。 - Processor 类通过构造函数注入
is_type_fn,实现依赖注入。 - 这样在单元测试时,可以注入
test_typenum替代真实行为,实现对模板函数行为的可控测试。
- 定义独立的函数
- 依赖注入特点:
- 将模板函数调用从类内部“抽象出来”,通过函数对象注入。
- 保持原模板函数的灵活性,同时解决了 MOCK 和继承测试的问题。
- 体现了策略模式 + 依赖注入的设计思想。
- 总结:
- 模板函数无法直接虚拟化是 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 验证方法调用。
理解要点
- 依赖注入(DI)思想:
process函数通过引用CalcEngine& engine接收依赖对象。- DI 核心思想:不要在函数内部创建依赖对象,而是外部提供。
- 好处:
- 可测试性强:可以注入 Mock 对象。
- 可复用性高:不同的 CalcEngine 实现可灵活替换。
- 解耦函数与依赖实现。
- 继承 + DI 的作用:
CalcEngine提供接口和默认实现。MockCalcEngine继承接口,提供模拟行为。- 使用 gmock 可以轻松验证函数是否调用了期望方法。
- 函数级别 DI:
- 这里是函数参数注入(Constructor Injection 或 Setter Injection 的一种简化形式)。
process完全解耦于具体 CalcEngine 实现。
- 总结:
- 这种模式非常适合“函数依赖于接口对象”的场景。
- 在单元测试中,
MockCalcEngine可以注入,验证apply和calculate是否被调用。 - 生产代码中可以注入真实对象,实现业务逻辑。
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,方便生产环境直接使用。 calculate是const方法,表示不会修改对象状态。
// 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)。
- 现代 gmock 语法为
- 注意:
- 这里
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函数验证行为。
理解要点
- 依赖注入(DI):
- 核心思想:不要在函数内部创建依赖对象,而是由外部提供。
process不依赖具体实现,依赖CalcEngine接口。
- 继承 + gmock:
MockCalcEngine继承CalcEngine,通过现代 gmock 宏MOCK_METHOD模拟虚函数。- 支持覆盖
const方法(calculate)和非const方法。 - 可以在测试中控制行为、返回值、调用次数。
- 现代 gmock 语法优点:
- 简洁明了,不再使用老式的
MOCK_METHOD0/MOCK_METHOD1。 - 支持重载和
const修饰符。 - 自动检查是否正确覆盖虚函数。
- 简洁明了,不再使用老式的
- 函数可测试性:
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_;};- 模板 DI:
Processor对CalcEngine类型是模板参数,不依赖具体实现。 - 优点:
- 编译时确定类型,无虚函数开销。
- 可以传入任何兼容接口的计算引擎,包括 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 支持同时在生产环境和测试环境中使用。
- 主函数直接用真实计算引擎调用模板函数
理解要点
- 模板依赖注入:
- 核心思想:通过模板参数注入依赖,而不是继承虚函数。
- 优点:
- 零虚函数开销(编译期静态分发)。
- 可以传入任意兼容接口的类。
- 测试时无需 Mock 对象继承,只需实现同名方法。
- 类模板 vs 函数模板:
- 类模板:适合包装和复用多次操作。
- 函数模板:简单直接,适合单次操作。
- 测试策略:
- 使用模板 DI 可以轻松传入测试实现。
- 验证
Data被正确修改。 - 不依赖 Google Mock 或虚函数即可完成。
- 对比继承 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类型必须有以下方法:t.calculate(d)返回可转换为boolt.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。 - 验证:
- 函数执行不抛异常。
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 的好处:
- 同一套代码同时支持生产引擎和测试引擎。
- 编译期类型检查,无虚函数开销。
理解总结
- 模板 DI (Template Dependency Injection):
- 核心思想:通过模板参数传递依赖,而不是继承虚函数。
- 优点:
- 编译期类型检查,性能零开销。
- 可传入任意符合接口的类型,包括测试对象。
- 不依赖继承层次,灵活性高。
- Concept 约束:
- C++20 Concepts 用于约束模板类型。
- 保证传入类型在编译期满足接口要求。
- 提高模板安全性,避免编译器报错过于晦涩。
- 类模板 vs 函数模板:
- 类模板:封装依赖,更易复用。
- 函数模板:轻量,适合单次调用。
- 测试策略:
- 用模板 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;}详细理解:
- 类型擦除(Type Erasure)思想:
Object类可以存储任意类型对象,只要对象有getName()方法。- 外部代码无需知道对象具体类型,通过统一接口
getName()调用。
- 实现原理:
- 定义抽象基类
Concept,包含纯虚函数getName()。 Model<T>继承Concept并实现getName(),内部存储具体类型对象T。Object持有std::shared_ptr<const Concept>,通过它实现对任意类型对象的统一访问。
- 定义抽象基类
- 构造函数模板:
Object(T&& obj)接收任意类型对象,通过std::make_shared<Model<T>>包装。- 使用
std::forward<T>保持对象的左值/右值属性。
- 多态调用:
Object::getName()调用object->getName(),利用虚函数实现多态。- 外部不需要知道对象类型,只需调用统一接口即可。
- 应用场景:
- 当需要在容器中存储不同类型对象,但希望统一调用某些方法时非常有用。
- 避免模板膨胀,提高可维护性。
- 结合智能指针管理对象生命周期,无需担心手动释放。
- 示例:
vec向量中存储Foo和Bar对象。- 调用
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();// 运行所有测试}详细理解:
- 核心概念:依赖注入(Dependency Injection)
Processor不直接依赖某种固定的收益计算逻辑,而是通过构造函数注入一个函数对象(YieldCalculator或CpYieldCalculator)。- 这样可以轻松替换不同的计算策略,例如测试中使用 lambda,生产中可能使用复杂算法。
- 类型选择
CpYieldCalculator:可拷贝函数对象,使用std::function,可复制和存储。YieldCalculator:仅可移动函数对象(move-only),提高性能和安全性(避免不必要的拷贝),C++23 或自定义实现。
- lambda 捕获
- lambda 捕获
ymultiplier,使计算器可以在调用时使用外部参数。
- lambda 捕获
- 处理流程
Processor::process接收YieldData对象,调用注入的收益计算器YieldCalculator_,返回计算结果。
- 测试设计
- 通过注入测试 lambda,实现对
Processor的单元测试。 - 验证
process的输出是否符合预期,确保依赖注入正确工作。
- 通过注入测试 lambda,实现对
- 公式示意
yield=YieldData.data_×ymultiplier \text{yield} = \text{YieldData.data\_} \times \text{ymultiplier}yield=YieldData.data_×ymultiplier- 对应代码中
ydata.data_ * ymult。
- 对应代码中
- 优点
- 提高可测试性:可以轻松替换不同计算逻辑进行测试。
- 提高灵活性和可维护性:不同业务逻辑只需更换注入函数,无需修改
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();// 运行所有测试}详细理解:
- 核心思想:继承 + Mock 测试
Builder类定义了build方法,用于构建Tick对象。appendTimes是私有虚函数,仅在内部被调用,但可以被MockBuilder覆盖,用于单元测试。- 这种方式解决了“私有方法可被 Mock”问题,同时保持接口简洁。
- 流程分析
- 调用
builder.build(data, tick) build内部调用tick.append("name", data.secName())- 随后调用私有虚函数
appendTimes - 在
MockBuilder中,这个调用会被 Mock 替换,可验证是否正确调用
- 调用
- 依赖注入与测试
appendTimes实际上相当于通过继承和虚函数实现的依赖注入。- 在生产环境中,调用的是
TimesLib::appendTimes;在测试中,通过 Mock 替换行为。
- 公式与数据关系
- 没有数学公式,本质是方法调用关系:
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)
- 没有数学公式,本质是方法调用关系:
- 优点
- 测试灵活:可验证
appendTimes是否被正确调用。 - 保持封装:
appendTimes可以是私有,但仍可 Mock。 - 依赖注入简洁:无需修改原有生产代码,仅通过继承和 Mock 实现测试替换。
- 测试灵活:可验证