探秘微信业务优化:DDD从入门到实践

引言 | 本文作者从微信团队保护的带货类项目所遇卡点动身,尝试用范畴驱动规划办法(简称DDD),确保在快节奏、多人协作的项目迭代中,维持体系的可保护性、可拓展性、高内聚低耦合和安稳性。作者首先解剖相关概念原理,之后代入亲自参与的微信团队实践项目、环绕DDD办法进行优化实操。

DDD 全称Domain-Driven Design,中文叫范畴驱动规划,是一套应对杂乱软件体系剖析和规划的面向目标建模办法论。它由Eric Evans于2003年提出,但一开始不愠不火。直到MartinFowler于2014年发表论文《Microservices》,引起大家对微服务的重视,至此DDD从头慢慢的回到了群众的视界中。

DDD这几年升温的一同,也受到了许多行业人员对DDD的负面定见。首要原因大概有“晦涩难明过于笼统”、“很难找到实践的事例参阅”、“不知道怎样落地”等。

在学习DDD的进程中,咱们也遇到上述卡点。但经过几个月继续学习和实践DDD,咱们对其思维、价值、运用办法有更深化了解。这儿尝试用白话去总结咱们DDD从入门到实践的全进程,尽量每一个概念都用咱们的详细完成做出比方,期望能对想一同学习DDD的开发者们有所帮助。

探秘微信业务优化:DDD从入门到实践

一个保护中的事务体系引出的考虑

我地点微信团队由后台和前端工程师一同保护某带货类的项目,这个项目咱们用了最传统的三层模型来建立,大概是如下的模型:

探秘微信业务优化:DDD从入门到实践

当这个项目保护几年之后,逐渐出些了一些有意思的状况,我挑选一些首要环节发现的代表性问题介绍下:

状况1(代码层面):少部分代码可读性在长时间不同人员的修改下变得越来越差。如某个带货的中心rpc逻辑没有任何嵌套平铺在一个函数,单函数代码行数达到几百行,可读性和保护性极差,成功化身为“技术护城河”。

状况2(微服务层面):某些微服务初始功用区分较为简略,导致少数模块在后续高频迭代中快速胀大。如其间的mp模块,本来功用是用来承接B端门户的功用;当咱们决定拆分这个巨大的模块时,这个模块已经承载了204个rpc。过多的才能承当让它编译变慢、变成链路单点、改动较多、一旦呈现问题影响较大。

状况3(事务团队层面):带货项目会运用一些其他事务体系的接口和数据结构。当这些事务体系想要修改这些接口和数据结构的时分 ,一方面或许未察觉这儿的依靠将导致线上问题, 另一方面或许在事务间交流后发现耦合处比较多,不简单改动。

保护这个项意图进程中,咱们进行了一些考虑:在一个杂乱事务体系中,代码结构要怎么规划、微服务的横/纵向功用要怎么区分、事务团队之间怎么交互,才能确保在快节奏、多人协作的项目迭代中,维持体系的可保护性、可拓展性、高内聚低耦合和安稳性。

而传统的开发模式不管是面向进程(POP)仍是面向目标(OOP)的思维,都没办法从微服务层面辅导咱们找到这些问题的答案。但咱们发现,有两种办法解决这个问题:

1.寻觅一个总是有时间、总能做出正确决议计划的中心节点搭档,介入每一处大局/细节的规划并一致做出决议计划。

2.寻觅一个新的规矩/规范来做辅导,让每一位开发工作者都能有做出正确决议计划的依据。

在Tencent的气氛和环境中,第二个办法无疑是更合理的,所以咱们想到了范畴驱动规划(DDD)。

探秘微信业务优化:DDD从入门到实践

DDD的分层架构

DDD最有标志性的一点,便是将传统软件规划三层模型转化为了四层模型,这个转化如下图所示:

探秘微信业务优化:DDD从入门到实践

乍看之下,四层架构引入了许多概念,如范畴服务、范畴目标、 DTO、仓储等等。咱们先不用在意这些细节概念,由于下一节会逐个剖析并列举咱们的完成比方。咱们先重视这几个关键的层:用户界面层、运用层、范畴层、根底设施层。咱们来看下他们的功用分工:

  • 用户界面层:网络协议的转化/一致鉴权/Session管理/限流配置/前置缓存/反常转化

  • 运用层: 事务流程编列(仅编列,不能存在事务逻辑)/ DTO出入转化

  • 范畴层:范畴模型/范畴服务/仓储和防腐层的接口界说

  • 根底设施层:仓储和防腐层接口完成/存储等根底层才能

这儿必须要说的是,这四层纷歧定是指物理四层,也能够在一个微服务中拆分逻辑四层。四层架构有许多变种,如六边形架构、洋葱架构、整齐架构、明晰架构等等。这些繁多的概念咱们这儿不过多谈论,而是仅以洋葱架构为例。此处咱们将着重强调DDD中的依靠倒置(DIP),以便后面更简单介绍仓储/防腐层等概念。

依靠倒置(DIP):

1.高档模块不应依靠于初级模块。两者都应依靠笼统。

2.笼统不应依靠细节。细节应依靠于笼统。

探秘微信业务优化:DDD从入门到实践

如上,洋葱架构越往里依靠越低,越是中心才能。根底设施层在最外面,依靠其他层,这是是由于DDD中其他层等需求界说自己需求的根底才能接口,而根底设施层担任依靠并完成这些接口,然后完成整体依靠倒置。这表现了DDD的由大局入细微、自顶层向下层的规划思维。

探秘微信业务优化:DDD从入门到实践

DDD的概念和实践

一、战略和战术

DDD的落地进程,其实便是战略建模和战术建模。

战略建模,是指:经过DDD的理论,对事务需求进行拆解剖析,区分子域,整理限界上下文,经过范畴言语从战略层面进行范畴区分以及构建范畴模型。并且在在构建范畴模型的进程中整理出事务对应的聚合、实体、以及值目标。

战术建模,是指:以范畴模型根底,经过限界上下文作为服务区分的鸿沟进行微服务拆分,在每个微服务中进行范畴分层,完成范畴服务,然后完成范畴模型关于代码映射意图,终究完成DDD的落地施行。

探秘微信业务优化:DDD从入门到实践

当然,战略和战术的建模除了要考虑事务形状,还要考虑到安排架构,就好像康威定律中的表达,交流架构会影响技术架构

康威定律:任何安排在规划一套体系(广义概念上的体系)时,所交付的规划方案在结构上都与该安排的交流结构保持一致。

二、范畴

DDD在解决杂乱的问题的时分,运用的是分而治之的思维。而这个分而治之的思维,便是从范畴开始,一个范畴便是一个问题空间,而咱们在拆分这个问题空间的时分,也便是在区分子范畴和寻觅它的解体系的进程。

实践比方:

如咱们某个新的增值事务,便是看成是的大的增值事务域,接下来咱们经过DDD来辅导拆分它。

三、子域

假如一个范畴太大太杂乱,涉及到的事务规矩、交互流程、范畴概念太多,就不能直接针对这个大的范畴进行建模。这时就需求将范畴进行拆分,本质上便是把大问题拆分为小问题,把一个大的范畴区分为了多个小的范畴(子域)。

子域能够分为三类:

中心子域:事务成功的中心竞争力。

通用子域:不是中心,但被整个事务体系所运用 。

支撑子域:不是中心,不被整个体系运用,完成事务的必要才能。

子域的区分除了分治了大的问题空间,也划定了工作的优先级。咱们应该给予中心域最高的优先级和最大的资源。在施行DDD的进程中,咱们也是首要重视于中心域。

实践比方

子域的区分,需求比较强的事务知识和产品研发团体谈论,精确和深化的事务见解在这一阶段尤为重要。这儿咱们不对事务知识深化谈论,仅展示下咱们的对增值事务域的拆解结果。

探秘微信业务优化:DDD从入门到实践

这儿要说的是,套餐域在完成的进程中由于产品需求变化概念被抛弃了,但是由于咱们的子域拆分,套餐域和其他域完成上没有任何耦合,所以抛弃套餐域概念的抛弃就像拆掉一个积木相同,对整套体系没有任何影响,也不会遗留任何不必要的包袱代码。

四、限界上下文

要了解限界上下文,首先要先介绍通用言语。通用言语是DDD非常重要的一点。比方产品这个概念,在产品域里是指备上架的产品, 包含了id、介绍、文档等。在买卖域里其实是指订单中被买卖的实体,重视的是id、成交时间的售价等参数、成交数量。而假如不能清晰这些概念和他们的联系就会让开发人员的完成变的随心所欲和含糊。

而限界上下文是便是区分一个鸿沟,当范畴模型被一个显示的鸿沟所包围时,其间每个概念的含义应该是清晰且有仅有的含义。

我觉得初学者最常碰到的问题,必定有”分明已经有子域了,为什么还会有限界上下文这个概念“。子域是一个子问题空间,而限界上下文的作用是辅导怎么规划这个问题空间的解体系。换句话说,限界上下文才是实在用来辅导微服务区分。一般来说一个子域对应一个或多个限界上下文。

区分限界上下文能够参阅如下的规矩:

1.概念是否有歧义:假如一个模型在一个上下文里边有歧义,就阐明能够继续拆分限界上下文。

2.外部体系:能够把与外部体系交互的那部分拆分出去降低外部体系对咱们咱们的中心事务逻辑的影响。

3.安排架构:不同团队最好在不同的限界上下文里边开发,防止交流不顺利、集成困难等问题。能够参阅上述”康威定律”。

实践比方1

如上所述,产品这个概念,是需求用限界上下文在不同场景区分隔的。当然这也会导致两个限界上下文之间会有依靠。经过DDD的概念能够辅导咱们进行如下完成。

探秘微信业务优化:DDD从入门到实践

其间gateway/gatewayimpl是防腐层的完成,DTO是指数据传输目标,APP是指产品运用层。两个不同颜色的产品是指两个上下文中别离进行界说的不同的实体或值目标。

实践比方2

买卖域中,有两个订单的概念,其间第一个订单的概念是指事务层订单, 第二个订单的概念是指内部根底层订单。事务订单更重视发生买卖的成交产品信息,这个订单是用户需求的。根底层订单更重视买卖底层的进程信息,这个订单更多是咱们内部人员需求的,用户不了解。

当时有个思路是想让根底层团队的同学额外开发直接支持根底层订单存储事务信息,这显着是不符合DDD限界上下文区分规矩1)和3)的,是需求经过限界上下文解耦开的。所以咱们在买卖域中拆分两个上下文,后续从微服务层面也是相互独立的微服务,各自管理各自的范畴实体和值目标。

五、防腐层

当两个限界上下文相互调用的时分,需运用防腐层(ACL)来进行两个限界上下文的阻隔,并完成value object的转化。防止不同上下文直接相互调用,否则一旦被调用上下文被修改则或许产生较大影响。

实践比方

完成链路能够参阅3.4的比方1,在产品域中,咱们的防腐层是依照如下的目录方式完成的, 范畴层来界说范畴层需求的防腐接口,根底设施层承继并完成防腐接口,在根底设施层直接调用其他限界上下文。

productdomainsvr (产品限界上下文)
├── domain(范畴层)
│   ├── aggregate
│   │   ├── spu.cpp                        //1)spu范畴目标需求调用其他限界上下文生成id
│   │   └── spu.h
│   └── gateway
│       └── gen_id_gateway.h         //2)范畴层界说调用其他限界上下文生成id的防腐接口
├── infrastructure(根底设施层)
│   └── gatewayimpl
│      └── acl(防腐层)
│         ├── gen_id_gateway_impl.cpp //3)根底设施层完成范畴层界说的防腐接口,实在调用其他上下文
│         └── gen_id_gateway_impl.h

六、范畴工作

两个限界上下文除了经过运用防腐层直接调用,更多的时分是经过范畴工作来进行解耦。

并不是一切范畴中发生的工作都需求被建模为范畴工作,咱们只重视有事务价值的工作。范畴工作是范畴专家所关怀的(需求盯梢的、期望被告诉的、会引起其他模型目标改动状况的)发生在范畴中的一些工作。

其实,范畴工作的本质便是工作,咱们常见的kafka、wq等都能够作为范畴工作的完成基建。经过范畴工作,能够把很轻松两个限界上下文解耦。

实践比方

在咱们的增值事务中,买卖域的”支付成功”便是一个范畴工作,计费域订阅这个范畴工作,然后能够根据这个工作调整客户的计费资源包实体。

探秘微信业务优化:DDD从入门到实践

能够幻想,假如这儿没有采用范畴工作, 而是买卖域直接调用计费域的rpc告诉买卖成功,那么当后续有其他域需求承受“支付成功”这个工作,或者,计费域被调用的接口呈现毛病。都会让买卖域堕入麻烦,前者需求买卖域不停的堆叠调用外部rpc的代码并让体系变得不安稳,后者则直接会让计费域的毛病影响到用户买卖。

七、实体/值目标

实体是指上下文中仅有的且可继续变化的根底单元,在其生命周期中能够经过安稳的仅有id来标识。实体在咱们代码中以范畴目标的形状存在,一同具备特点和办法,实体是DDD用来完成充血编程、解决贫血症的关键

与实体相对应的便是值目标,假如没有仅有标识便是值目标。值目标一般是嵌套在实体里边的。

实践比方

产品域中的实体和值目标如下

实体

描绘

关键值目标

SPU

指一个被上架的服务。

spu_id, spu_type,状况等。

SKU

指一个服务详细的单项套餐。

sku_id, 标准,价格等。

扣头

自界说扣头。

扣头id,扣头类型,扣头比例等。

八、聚合/聚合根

把联系紧密的实体放到一个聚合中,每个聚合中有一个实体作为聚合根,一切关于聚合内目标的拜访都经过聚合根来进行,外部目标只能持有对聚合根的引证。每个聚合都能够有一个独立的上下文鸿沟。

聚合应区分的尽量小,一个聚合只包含一个聚合根实体和密不可分的实体,实体中只包含最小数量的特点。规划这样的小聚合有助于进行后续微服务的拆分。

假如一个rpc所完成的功用是跨聚合的,那跨聚合的编列协调工作应该放在运用层来完成。

实践比方

咱们能够在6)中的比方区分如下的聚合。

聚合

实体

是否是根

聚合1

服务SPU

服务SKU

聚合2

扣头

在底层存储落表上, spu实体/扣头实体作为表的一行, 而sku实体在这种聚合建模的指引下咱们规划成spu聚合根的一列。

在微服务拆分上,假如想拆到最细粒度, 能够把两个聚合依照各自上下文拆成独立的微服务。当然这种落地完成并不是DDD强行要求的,我以为一些时分咱们也能够从开发保护功率的视点考虑, 将一些有相关的小上下文放在一个为微服务上。咱们在处理产品域上挑选了后者。

九、DTO/范畴目标/Data object

当一个恳求进入DDD所规划的体系中,这个恳求的形状会根据地点的层级发生如下改换,DTO<->范畴目标<->Data object。

DTO是指对外传输的其他服务需求了解的结构,范畴目标是指一同包含了特点和办法的范畴实体封装,Data object则是实在用于终究存储的数据结构。

探秘微信业务优化:DDD从入门到实践

这儿其实很简单发现,DTO的存在虽然符合其他调用方最少知识准则(LKP),但假如连最简略的查询恳求都需求做这三级的转化,那无疑是会加重开发的杂乱度,变成为了规划模式而规划模式。

最少知识准则(迪米特法则,LKP):一个软件实体应当尽或许少地与其他实体发生相互作用。这儿的软件实体是一个广义的概念,不只包含目标,还包含体系、类、模块、函数、变量等。

所以DDD在这儿一般会运用CQRS(读写责任别离)架构,来确保一些简略的查询恳求不会由于范畴建模而变得过于杂乱。CQRS(读写责任别离)基于CQS(读写别离),运用了CQRS的DDD目标转化流程如下:

探秘微信业务优化:DDD从入门到实践

实践比方

咱们的完成是在范畴目标中封装了转化的convert函数(当然也能够在根底设施层将convert办法拆分出来做独自的封装),用于将DTO转化为范畴目标,或者将范畴目标转化为DO。下面是咱们明细域的实践转化代码和转化进程。

//1.范畴目标中界说convert办法
class DetailRecord {
public:
int ConvertFromDTO(const google::protobuf::Message& oDto);
int ConvertToDO(detailrecordinfrastructure::DetailRecordDO & oDo);
/*...*/
};
//2.运用层调用办法将DTO转化为范畴目标, 然后调用仓储接口进行耐久化
int DetailrecordApplication::InsertDetailRecord(unsigned int head_uin, const InsertDetailRecordReq& req,  InsertDetailRecordResp* resp) {
int iRet = 0; 
class DetailRecord oRecord;
   iRet = oRecord.ConvertFromDTO(req); //生成范畴目标,能够一同使用范畴目标的办法进行自检等操作
/*...*/
   iRet = m_oDetailRecordGateway->Save(oRecord); //调用仓储接口进行耐久化
/*...*/
return iRet;
}
//3.在仓储中将范畴目标转化为Dataobject,进行落存储操作,并发布范畴工作
int DetailRecordGatewayImpl::Save(DetailRecord & oEntity){
   detailrecordinfrastructure::DetailRecordDO oDo;
int iRet = oEntity.ConvertToDO(oDo);
/*...*/
   iRet = oKvMapper.insert(oDo);      //实践落存储
/*...*/
   iRet = oEventMapper.publish(oDo);  //发送范畴工作 
/*...*/
return iRet;
}

十、仓储

仓储是范畴层由界说接口,它笼统了事务逻辑中对实体的拜访(包含读取和存储)的技术细节。它的作用便是经过阻隔详细的存储层技术完成来确保事务逻辑的安稳性。注意,仓储只是接口的界说是在范畴层,但是它的完成是在根底设施层

仓储不是数据库Dao!!!

仓储不是数据库Dao!!!

仓储不是数据库Dao!!!

重要的工作说三遍,仓储是从事务逻辑的视点笼统出来的接口,所以仓储的接口在完成上,一般是一个聚合对应一个仓储完成**,仓储的需求用范畴目标做参数**。仓储接口的命名也能够取save这种更事务的命名, 而防止传统dao的insert/set等这种分明。

实践比方

经过3.9的比方,咱们能够发现,仓储用于耐久化的接口里,不光包含了写kv的操作,还包含了发布范畴工作等操作,这便是由于仓储是从事务逻辑视点笼统出来的接口,范畴层只需求了解save这个事务操作,而不应该了解save的进程包含了落存储、发布范畴工作等详细流程。

//1.范畴层界说DetailRecord仓储的接口
class DetailRecordGateway {
public:
/*...*/
virtual int Save(DetailRecord & oEntity) = 0;
/*...*/
};
//2.根底设施层承继范畴层的仓储接口进行完成
class DetailRecordGatewayImpl : public DetailRecordGateway {
public:
/*...*/
virtual int Save(DetailRecord & oEntity);
/*...*/
 };
//3.仓储save接口详细完成
int DetailRecordGatewayImpl::Save(DetailRecord & oEntity){
   detailrecordinfrastructure::DetailRecordDO oDo;
int iRet = oEntity.ConvertToDO(oDo);
/*...*/
   iRet = oKvMapper.insert(oDo);      //实践落存储
/*...*/
   iRet = oEventMapper.publish(oDo);  //发布范畴工作 
/*...*/
return iRet;
}

十一、范畴服务

当一些才能不适合放在某个范畴目标中完成,又由于过于杂乱不应该放在运用层来完成。能够把这些操作封装成范畴服务的中办法,由运用层编列范畴层的范畴目标和范畴服务办法来完成详细的事务功用。

探秘微信业务优化:DDD从入门到实践

DDD的代码脚手架

咱们基于对DDD的了解和WXG的svrkit结构,设定咱们的代码脚手架。**脚手架的目录如下所示,期望能够给想一同实践的开发者们抛砖引玉,也欢迎大家在谈论区一同谈论~

项目目录
├── adapter(物理用户界面模块)
├── domainsvr(范畴微服务)
│   ├── detailrecorddomainsvr(明细域微服务)
│   │   ├── adapter(用户界面层)
│   │   ├── application(运用层)
│   │   │   ├── detailrecord_application.cpp(运用层办法)
│   │   ├── domain(范畴层)
│   │   │   ├── aggregate(聚合根)
│   │   │   │   ├── detail_record.cpp(范畴目标)
│   │   │   │   └── detailrecordaggregate.proto(聚合根的值目标)
│   │   │   ├── entity(非根实体)
│   │   │   │   └── detailrecordentity.proto(非根实体的值目标)
│   │   │   ├── gateway
│   │   │   │   └── detail_record_gateway.h(仓储接口)
│   │   │   └── detailrecord_domain_service.cpp(范畴服务)
│   │   ├── infrastructure(根底设施层)
│   │   │   ├── gatewayimpl
│   │   │   │   ├── acl(防腐层完成)
│   │   │   │   └── detail_record_gateway_impl.cpp(仓储完成)
│   │   │   └── detailrecordinfrastructure.proto(Data object界说)
│   │   └── detailrecord.proto(DTO界说)
└── infrastructuresvr(物理根底设施模块)

阅读原文