顺应公司的开发趋势,我们团队选择了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层设计了这三个实体:

账户:

订单:

流水:

这三个核心领域模型是基石,同时,注意,我们使用的是接口/实现类的模式,我们是面向接口编程的模式,为啥这样,也是为了方便扩展,比如订单,可能会有多种实现类;

在此基础上,我们会设计聚合类:

提现聚合接口:

提现聚合接口实现:

这里可以看到,很明显的,接口实现类中有刚刚上面提到的三个实体,订单、余额和流水,同时在这个聚合实现类里面做具体的实现;

OK,那么问题来了,这个接口实现类是怎么生成的呢?这三个实体哪里来的呢?

这个时候,就是工厂模式的思想出现了,我们作为do层的业务领域,是不需要关注所依赖的实体是从哪里来的,只要关心在拥有了实体之后具体的业务逻辑怎么处理。

这个道理有点像是在厨房做菜,大厨就是我们的do层的聚合类,他只要加工食材就好,至于食材的采购,择洗,他不需要关心,这就是所谓的构建和执行分离,也是工厂模式的思想核心;

那么回到问题里来,实体哪里来呢?

我们将实体的构建和生成,放在factory里面,是的,golang并没有类似spring的bean管理框架(或者有了我也不知道😀),那我们就造一个factory出来,专门用来做实体的构建。

比如在factory文件中,存在如下方法作为余额实体的构建:

那么,问题又来了,这个baseAccountDAO又是在哪里定义的呢?

是在repo层的factory文件中维护所有的数据层的接口与实现类:

所以为什么要有factory?

是为了将构建与执行二者分离开,让构建的管构建,执行的管执行,这样,复杂实体的构建就被收拢了。

最后,我们会发现那么这个factory的方法在哪里调用呢?参数校验在哪里做呢?入参和出参怎么转换呢?

这就是ao的作用,像我们之前说的,如果说do是厨房里的大厨,那么ao就是传菜员+洗菜工,负责这些杂活累活,这种事情往往琐碎而且容易变化,所以我们将它放在了ao层,因为它并不聚焦在我们的业务模型上,反而是琐碎且容易变化的。这也是将变化的和不变的分离开,将重要的和无关的分离开。

具体代码如下所示(出于风险考虑省略了部分代码):

我们思考一下,为啥要有设计模式,要做ddd? 其实最终的目的我认为,是为了降低我们自己的开发心智。

试想一下,如果什么都没有,把所有的业务逻辑都写在sql脚本里面,能否满足业务需求呢?

其实也不是不行,但当业务变更时,或者需要定位问题时,对个人的要求是非常高的;但如果我们分门别类的将那些魔鬼般的细节关在自己的盒子里,也就意味着我们可以非常方便的聚集在自己关注的模块里,是大大节省了自己的开发心智的,从团队的角度上来说,这种统一的开发范式也能够大幅度的降低团队成员之间的交接成本。