1 背景
我们需要重构一个旧系统,新系统采用微服务架构,其中有几个关键服务逻辑复杂,对性能要求很高,因此经决策后计划采用 C++ 实现。
此前有耳闻领域驱动设计(DDD)可以从设计上解耦,低成本变更,在复杂业务逻辑系统中有较好的应用前景,因此我们计划在新系统的关键服务中应用 DDD。
不过目前接触到的 DDD 案例基本上都是 JAVA 的,他们或多或少利用了 Spring 相关的特性,这对我们在 C++ 应用十分不友好,毕竟 C++ 并没有一个成熟的 DDD 框架。于是,在我们了解了 DDD 的核心思想后,我们根据自身理解,在 C++ 服务中应用了 DDD,并形成了一套较为稳定的标准模板。
有关 DDD 相关概念的说明可以参考 领域驱动设计-DDD 一文,在这里,我主要介绍洋葱架构(Onion Architecture)的实践。
2 实践
为方便理解,本文以 资金测算服务
举例,介绍如何基于 DDD 实现一个简单的服务。
资金测算服务介绍
资金测算服务是目前会员资金风控系统中的一个
微服务
,其职能是根据会员账本和会员清算单测算出会员的新账本。
2.1 业务梳理
我们梳理一下 资金测算服务
的主线流程:
graph TD
A(收到测算请求)-->B(从redis获取当前会员账本)-->C(从redis获取当前会员清算单)-->D(测算会员新账本)-->E(上传会员新账本到redis)
可以看得出,这个服务的功能非常简单,我们可以根据 IPO( 输入
、处理
、输出
) 分类方式,将业务梳理成三个部分:
- 输入 Input
- 接受测算请求
- 获取当前会员账本
- 获取当前会员清算单
- 处理 Process
- 测算会员新账本
- 输出 Output
- 上传会员新账本
这三个部分我们就可以认为是该服务的核心业务逻辑了。
我们接下来,就将:
- 业务流程放到
application
中 - 业务实现放到
domain
中 - 与外部交互实现放到
adapter
中
形成以下目录结构:
2.2 目录结构
.
├── adapters # 放各类适配器
├── app # 放程序入口、程序流程
├── domain
│ ├── impl # 放业务实现、业务流程
│ └── interface # 放业务接口、交互接口
│ └── entity # 放公共类型、结构体定义等
└── third_party # 放三方库
2.3 domain 设计实现
简化代码
为了简化代码,下面对部分逻辑进行了省略:
CHECK_RET [XXXXX]
:如果返回值为false
,则return false
。DECLEAR [XXXXX]
:声明一个变量XXXXX
,这个变量的类型需要结合上下文判断。
按照业务梳理结果,我们可以先设计 IPO 每个业务逻辑的接口:
- 接受测算请求
domain/interface/settlement_request.h
using OnSettlementRequestCallBackFuncType = std::function<bool()>;
class ISettlementRequest{
public:
virtual void setOnSettlementRequestCallBack(OnSettlementRequestCallBackFuncType) = 0;
};
using ISettlementRequestPtr = std::shared_ptr<ISettlementRequest>;
- 获取当前会员账本
domain/interface/firm_fund_loader.h
class IFirmFundLoader{
public:
virtual bool getFirmFund(FirmContainer<FirmFundDetail>& firm_fund_map) = 0;
};
using IFirmFundLoaderPtr = std::shared_ptr<IFirmFundLoader>;
- 获取当前会员清算单
domain/interface/clear_result_loader.h
class IClearResultLoader{
public:
virtual bool getClearResult(FirmContainer<FirmClearDetail>& firm_clear_map) = 0;
};
using IClearResultLoader = std::shared_ptr<IClearResultLoader>;
- 测算会员新账本
domain/interface/settlement.h
(包含业务逻辑)
class ISettlement{
public:
virtual bool calc() = 0;
};
- 上传会员新账本
domain/interface/settlement_result_uploader.h
class ISettlementResultUploader{
public:
virtual bool upload(const FirmContainer<FirmFundDetail>& firm_fund_map) = 0;
};
using ISettlementResultUploaderPtr = std::shared_ptr<ISettlementResultUploader>;
我们希望在 测算会员新账本
时将整个业务流程串起来,就需要完成 class ISettlement
的实现:
domain/impl/settlement_impl.h
class SettlementImpl : public ISettlement{
private:
ISettlementRequestPtr m_request{nullptr};
IFirmFundLoaderPtr m_fund_loader{nullptr};
IClearResultLoaderPtr m_clear_result_loader{nullptr};
ISettlementResultUploaderPtr m_uploader{nullptr};
public:
bool init(ISettlementRequestPtr request,
IFirmFundLoaderPtr fund_loader,
IClearResultLoaderPtr clear_result_loader,
ISettlementResultUploaderPtr uploader){
m_request = request;
m_fund_loader = fund_loader;
m_clear_result_loader = clear_result_loader;
m_uploader = uploader;
m_request->setOnSettlementRequestCallBack([&](){
return calc();
});
return true;
}
virtual bool calc() override{
// 获取当前会员账本
DECLEAR firm_fund_map;
CHECK_RET m_fund_loader->getFirmFund(firm_fund_map);
// 获取当前会员清算单
DECLEAR firm_clear_map;
CHECK_RET m_clear_result_loader->getClearResult(firm_clear_map);
// 测算会员新账本
DECLEAR result;
CHECK_RET calcFund(firm_fund_map, firm_clear_map, result);
// 上传会员新账本
CHECK_RET m_uploader->upload(result);
return true;
}
private:
bool calcFund(const FirmContainer<FirmFundDetail>& in_firm_fund_map,
const FirmContainer<FirmClearDetail>& in_firm_clear_map,
FirmContainer<FirmFundDetail>& out_firm_fund_map){
// 业务逻辑,计算出的新账单为out_firm_fund_map
return true;
}
};
接下来可以对 SettlementImpl
做单元测试,以验证流程的正确性。
2.4 adapters 设计实现
当完成 domain
部分的验证后,就可以着手于 adapters
部分的实现了。
我们可以留意到,domain
中存在一些未实现的接口,这些接口大多与外部交互有关,这也是我们的目的——将与外部交互有关的逻辑放到 adapters
中实现,将与业务相关的逻辑在 domain
中实现,这样业务是业务,技术是技术,二者只要满足接口设计,变动时就不会相互影响。
我们选 ISettlementResultUploader
举例:
adapters/settlement_result_uploader/settlement_result_uploader_adapter.h
class SettlementResultUploaderImpl : public ISettlementResultUploader
{
public:
bool init(const Config& config){
// 这里根据配置初始化一些信息
return true;
}
virtual bool upload(const FirmContainer<FirmFundDetail>& firm_fund_map) override{
// 这里实现将结果上传到redis中
return true;
}
};
using SettlementResultUploaderImplPtr = std::shared_ptr<SettlementResultUploaderImpl>;
在这里,我们可以意识到,如果我们希望将 redis
换成其他的存储中间件,如 Oceanbase
、Oracle
等,我们只需要修改这个实现就行。甚至在前期测试时,我们可以直接将其做成假接口,返回特定的数据,达成测试目的。
2.5 app 设计实现
当我们完成 adapters
的实现后,我们可以将其整合起来了,也就是我们现在需要完成程序流程。
在这个阶段,我们将不再面临抽象的接口,而是将其实现组装起来。
为让代码干净整洁,我专门用一个 SettlementMgr
来干整合的事情:
app/settlement_mgr.h
class SettlementMgr{
private:
SettlementResultUploaderImplPtr m_result_uploader{std::make_shared<SettlementResultUploaderImpl>()};
// 其他的实现都像示例一样声明
SettlementRequestImplPtr m_request{....};
FirmFundLoaderImplPtr m_firm_fund_loader{....};
ClearResultLoaderImplPtr m_clear_result_loader{....};
SettlementImpl m_settlement_impl{....};
public:
bool init(const Config& config){
// 初始化各类adapters
CHECK_RET m_result_uploader->init(config.get("Uploader"));
// .....
// 初始化SettlementImpl
CHECK_RET m_settlement_impl->init(m_request, m_firm_fund_loader, m_clear_result_loader, m_result_uploader);
return true;
}
bool run(){
m_request->start(); // 开始监听服务
return true;
}
// 这里就先不实现stop之类接口了
};
然后就可以在程序入口对 SettlementMgr
进行管理了:
app/main.cpp
int main(){
// 初始化配置
auto config = loadConfig();
// 初始化日志
CHECK_RET initLogger(config);
SettlementMgr sm;
CHECK_RET sm.init(config);
CHECK_RET sm.run();
return 0;
}
这样,一个符合 DDD 的 C++ 服务就实现出来了。
3 遗留问题
3.1 adapters 单元测试问题
在设计实现 adapters
时,我们发现为了保证接口的稳定性,一切输入输出的数据转换逻辑都需要在 adapters
中实现,同时,数据转换中可能也存在着少量的业务逻辑。
在早期实践中,我们认为应该对 domain
着重测试,adapters
中由于和交互相关,测试相对困难,于是没有做完整的场景覆盖,这导致 domain
逻辑“很稳健“,但涉及到 adapters
经常会出现问题。
起初我们想将 adapters
中剩余的小部分业务逻辑也迁入 domain
中,但很快发现这样不妥:
- 这部分业务逻辑依赖交互方式,可能换一种交互方式就不存在该逻辑。
- 一旦放入
domain
中,就会导致domain
非常臃肿,与使用 DDD 的初衷相悖。
目前只能说又开发人员在编码 adapters
时留意此类逻辑,并单独做单元测试,还没想到有什么合适的办法。
4 感想
从一开始接触 DDD 到开始尝试上手实践,程序的结构改变了不知道多少次,最终才能有一个大家相对满意的结果。
个人认为,DDD 并不是一个框架,它只是设计方式的指导。
受益于 单一职责原则 和 依赖倒置原则,我们在技术方案变动、接口改动和业务改动时(全新系统还未稳定),可以非常快速地定位到修改点,并将修改范围缩小到非常精确的位置,修改后单元测试也可以很稳定地覆盖。个人认为它可以延缓一个项目走向屎山的步伐。
当然,这一切都是主观感受,每个项目都有它所面临的主要问题,
希望能给你带来帮助。