顺应公司的开发趋势,我们团队选择了golang作为后台开发的主语言,但golang所推崇的简洁,原生,轻量,对于我这种java出身的同学来说,太过原生也就意味着配套的工具链和开发框架的缺失。而恰巧,我们又是一个电商类后台团队,电商后台业务如果用一个词来概括的话,我想应该是复杂。那么如何用简洁的开发模式来应对复杂的业务系统,是我们所面临的一个很大的挑战。
应对复杂系统的解决之道总的来说就是两个:分而治之,抽象模型。
是的,无论多么复杂的事情,这两个就是我们拆解和应对的最大武器,将复杂的系统拆解,并抽象出具体的业务模型,借助这两者,我们才能够很好的抵抗外部的复杂性,并聚焦在最核心的事情上。而这也是为何我们需要ddd,需要设计模式,需要领域模型的原因。
因此,我们团队是在近一年前很多工具缺失的情况下,启动了电商后台的开发。这里分享一下我们是如何应对和解决的
1.分而治之:
无论是系统的微服务划分,还是细粒度到每个函数的定义,分而治之的思想都横贯在开发的始终,具体到工程代码的维度上,我们的第一个方式是分包。
对于一个典型的后台服务来说,它的分包模式我们设计如下图所示:
我们首先明确的是,各个不同子包负责不同的功能,保证了职责的独立和分离;
service:
服务层,作用是对外提供的服务,是数据的入口,包括trpc的接口以及各类消息的消费者等等;
ao:
防腐层,作用是参数转换,逻辑编排,以及部分errocode的封装;
do:
业务领域层,作用是业务的核心领域,负责最核心的业务逻辑实现;
repo:
数据仓库层,负责数据存储查询等功能,包含mysql,redis等各类实现,这里均以接口形式对外提供服务;
integration:
第三方服务层,负责收拢所有的外部接口依赖,包括trpc接口和cmq的生产者等等;
config:
配置层,包括tconf配置,常量,错误码等等
2.抽象模型:
我们将复杂的产品需求抽象成聚焦的业务模型,并把对业务模型的抽象和定义放在do层,用来表示最聚焦的业务模型。
这里我们提供一个提现的case,来说明下各个子包的功能以及具体的实践。
我们业务上会有用户个人钱包的概念,这个钱包要支持收钱、转账、提现等的能力,那我们以提现为例,看下这个代码的具体实现:
面向领域模型开发的一个最重要的地方在于,最早设计开发的不是数据库,而是do层的领域模型;
这里我们设计了三个核心的领域实体:
1.订单;
2.用户账户余额;
3.流水记录;
试想下,无论是怎样的业务场景(收,转账,提现等)都是由这三个“原子”型的模型所组合和定义的。
因此,我们首先在do层设计了这三个实体:
账户:
代码语言:javascript复制type VirtualAssetBalanceDo interface {
/**
* @Description 资产状态有效
* @Author yang
* @Date 2020/12/1
*/
IsValid(ctx context.Context) bool
/**
* @Description 获取
* @Author yang
* @Date 2020/6/30
*/
Get(ctx context.Context) VirtualAssetBalance
/**
* @Description 扣除
* @Author yang
* @Date 2020/5/29
*/
Del(ctx context.Context, Amount uint64) error
/**
* @Description 增加
* @Author yang
* @Date 2020/5/29
*/
Add(ctx context.Context, Amount uint64) error
....
}
订单:
代码语言:javascript复制type WithDrawOrderCertificateDO interface {
Get(ctx context.Context) WithDrawOrderCertificate
ChangeCurStatus(ctx context.Context, status string) error
Create(ctx context.Context) error
AddVersion(ctx context.Context) error
FillWXPayInfo(ctx context.Context, item *comm.DetailOrderItem)
}
流水:
代码语言:javascript复制type RecordInfo struct {
Id uint64
VirtualAssetID uint64
VirtualAccountBusinessType uint64 //账户业务类型 1:小鹅农场金币 10:现金账户
VirtualAssetType uint64 //资产类型: 1:现金类账户 2:交易类账户 3:负债类账户
BeforeBalance uint64 //更新前余额
AfterBalance uint64 //更新后余额
LastRecordID uint64 //上次流水的ID
Amount uint64 //本次增加或减少的额度
RecordType RecordInfoEnum // 0:nil 1:收入 2:支出 3.转账
RecordID uint64 //
FromID uint64 //来源ID: income:fromid outcome:orderid,transfer:transferorderid,
FromName string //收入来源/支出去向
RuleID uint64 //渠道规则ID
RuleName string //渠道规则名称
ExtMsg string //
}
这三个核心领域模型是基石,同时,注意,我们使用的是接口/实现类的模式,我们是面向接口编程的模式,为啥这样,也是为了方便扩展,比如订单,可能会有多种实现类;
在此基础上,我们会设计聚合类:
提现聚合接口:
代码语言:javascript复制type WithDrawAggregateDO interface {
WithDraw(ctx context.Context) (*WithDrawRspInfo, error)
}
提现聚合接口实现:
代码语言:javascript复制/**
* @Description 提现聚合
* @Author yang
* @Date 2020/12/2
*/
type WithDrawAggregateDOImpl struct {
AssetBalanceDo do.VirtualAssetBalanceDo
PayRecord *do.RecordInfo
CertificateOrderDO do.WithDrawOrderCertificateDO
}
代码语言:javascript复制func (s *WithDrawAggregateDOImpl) WithDraw(ctx context.Context) (*do.WithDrawRspInfo, error) {
//1.创建提现订单并预扣,并创建超时任务
err := s.createOrderAndLockBalance(ctx)
if err != nil {
xlog.WarnContext(ctx, 0, "CreateOrderAndLockBalanceError", "err", err)
return nil, err
}
//2.调用接口,若出现error则什么都不做,静待超时任务重试
certificate := s.CertificateOrderDO.Get(ctx)
rsp, err := integration.WXPayWithDraw(ctx, certificate.WithDrawOrderID, certificate.UserID, certificate.Amount)
if err != nil {
xlog.WarnContext(ctx, 0, "WXPayWithDrawError", "err", err)
return nil, err
}
//rsp为空即可认为rsp为fail
if rsp == nil {
xlog.WarnContext(ctx, 0, "WXPayWithDrawError", "err", err)
return nil, errors.New("WXPayWithDrawEmptyRSP")
}
//3.rebuild VirtualAssetBalanceDo 和WithDrawOrderCertificateDO,主要是前面持久化了之后需要version 1,省去查询
s.AssetBalanceDo.AddVersion(ctx)
s.CertificateOrderDO.AddVersion(ctx)
//4.根据接口返回决定实扣、回滚或什么都不做
err = DealWithByStatus(ctx, s, rsp.Item)
if err != nil {
xlog.WarnContext(ctx, 0, "更新失败", "e", err)
return nil, err
}
return &do.WithDrawRspInfo{
WithDrawOrderID: s.CertificateOrderDO.Get(ctx).WithDrawOrderID,
Status: s.CertificateOrderDO.Get(ctx).CurState,
WxErrorMsg: s.CertificateOrderDO.Get(ctx).PayErrDesc,
WxErrorCode: s.CertificateOrderDO.Get(ctx).PayErrCode,
}, nil
}
这里可以看到,很明显的,接口实现类中有刚刚上面提到的三个实体,订单、余额和流水,同时在这个聚合实现类里面做具体的实现;
OK,那么问题来了,这个接口实现类是怎么生成的呢?这三个实体哪里来的呢?
这个时候,就是工厂模式的思想出现了,我们作为do层的业务领域,是不需要关注所依赖的实体是从哪里来的,只要关心在拥有了实体之后具体的业务逻辑怎么处理。
这个道理有点像是在厨房做菜,大厨就是我们的do层的聚合类,他只要加工食材就好,至于食材的采购,择洗,他不需要关心,这就是所谓的构建和执行分离,也是工厂模式的思想核心;
那么回到问题里来,实体哪里来呢?
我们将实体的构建和生成,放在factory里面,是的,golang并没有类似spring的bean管理框架(或者有了我也不知道