领域驱动设计实践,精通业务,面向对象编程,面条编程,过程编程

 

背景介绍

近年来,苏宁集团业务不断扩大,用户快速增长,线上线下融合不断深入,系统的复杂性越来越高,技术的广度和深度都在不断拓展。

在整个集团技术不断迭代演进的过程中,集团内各个系统也同步更新、迭代、重构,快速适应技术的发展,满足业务增长的需求。

苏宁金融会员系统作为苏宁金融的一级系统,从易付宝诞生开始就作为基础支撑系统为整个金融业务系统提供会员服务。经过多年的演化和业务版本的迭代维护,到如今代码调用错综复杂,各个逻辑散落在代码的各个角落,牵一发而动全身。而且这些业务逻辑基本都集中落在了代码的 Biz 层中,导致 Biz 层臃肿庞大。

为了适应苏宁业务的快速发展,跟进苏宁集团多活架构的演进,金融会员系统的技术架构需要再一次跃迁。

 

架构选型

重构系统的架构选型是一个仁者见仁智者见智的事情,没有哪一种模式是标准答案,只能追求更适合的选项。本次对金融会员系统重构,从框架选型到架构选型都做了新的选择,选择了 Spring+Mybatis+Mycat+MySQL 的技术框架和 DDD+CQRS+ 插件的架构模式。

领域驱动设计(DDD,Domain-Driven Design)作为这一次系统重构的架构选型,主要考虑到以下因素:

  1. DDD 模式更加关注业务领域,能够使得苏宁金融会员系统更加聚焦会员产品的核心业务。

  2. DDD 模式采用面向对象的设计,将系统模块化,有利于实现软件模块的高内聚和低耦合,使得会员系统更加适合应对苏宁业务的快速迭代。

 

技术实现

领域驱动设计实践

DDD 模式的最大优势在于聚焦产品核心业务,最难搞定的也在此处。那么该如何实现呢?领域驱动设计的关键在领域模型,如果把领域模型拆开来看,如下图,就不难理解了。

领域驱动设计实践,精通业务,面向对象编程,面条编程,过程编程

图 1 领域驱动设计拆分

那么,理解领域驱动设计就变成如下四点内容:

 1. 精通业务

精通业务,需要业务专家,对于互联网产品,产品经理就是业务专家。技术人员作为重构发起方,需要不断和产品经理讨论业务,梳理出业务流程中隐藏的数据信息。例如会员系统的开户服务,产品经理给出的业务流程如下:

领域驱动设计实践,精通业务,面向对象编程,面条编程,过程编程

图 2 面向过程的开发模式

上面流程看似很清晰,按着常规思路,上面每一步对应一段代码,按这种方式写出来的代码,就是大家常说的面条代码(或者事务脚本)。

如果采用领域驱动设计的模式来做的话,会怎么样?首先,和产品经理讨论,注册流程涉及哪些操作步骤,各个步骤涉及哪些数据;然后,将各个步骤的数据和对应的操作包装起来成为一个一个对象;最后,和产品经理讨论这些对象还应该具有哪些功能,各个业务功能模块分属于哪些对象。和产品经理的沟通不再是基于业务流程,而是基于业务模型。那么注册流程应该如下图所示:

领域驱动设计实践,精通业务,面向对象编程,面条编程,过程编程

图 3 面向对象的开发模式

 2. 精通面向对象编程

在 DDD 模式中将对象分为 ValueObject 和 Entity。ValueObject 代表的是值对象,比如一个地址“南京市玄武区徐庄软件园”,该地址没有生命周期,可以通过对象拷贝关联到任何一个在徐庄软件园的个人账户,这就是一个 ValueObject。而 Entity 对象是有生命周期的,可以唯一标识的,该对象只能属于某一个业务,比如 LoginPassword,一个 LoginPassword 对象只能属于某一个 Account 所有,不能任意拷贝,并伴随 Account 注册而初始化,随着 Account 注销而删除。

采用面向对象的编程,合理的组织对象之间的关联、聚合、组合关系,能够更好的遵循 SOLID 原则,能够更好管理对象。例如易购账号(CustNo)和易付宝账号(UserNo)绑定关系,对于易付宝来讲一个账号要么建立绑定关系要么没有建立绑定关系。如果建立绑定关系了,一个易购账号一定对应一个易付宝账号,那么当我们在易付宝会员侧建立 CustNo 领域对象时,和 UserNo 对象之间就是聚合的关系。当一个绑定关系建立时,该绑定关系对应的绑定关系控制器(EgoBindCtrl)也同时创建,但是一个 EgoBindCtrl 只对应一个绑定关系,如果绑定关系不存在了,那么 EgoBindCtrl 也没有存在的必要了,此时 CustNo 对象和 EgoBindCtrl 对象之间就是组合的关系,如下图:

领域驱动设计实践,精通业务,面向对象编程,面条编程,过程编程

图 4 对象关系示意图

 3. 对象创建

通过上面两步,有了领域建模的思路,接下来需要考虑对象怎么创建的问题了。苏宁金融会员系统已经运行超过 8 年时间,拥有超过 3 亿用户,这么大的数据量,如果对表结构进行重构,是不太现实的,保持现有的数据结构,对于表结构和领域对象之间的映射关系是复杂的。我们采用 Repository 对 Domain 进行数据转化,在 Repository 中将 DMO 转化为 Domain,这里有两种模式可选择:

领域驱动设计实践,精通业务,面向对象编程,面条编程,过程编程

图 5 领域模型对象创建模式对比

如上方式中 Application,DomainFactory,Repository,Dao 都是采用 Spring 单例的方式管理,通过注入的方式集成,Domain 是根据业务需要 new 出来的。

如图 A 的方式,在应用层(Application)注入 Repository 服务,在 Repository 中转化 Domain 对象,这种方式简单直接,但是很容易将 Repository 的服务做成事务脚本的模式,结果将业务由 Domain 转移到 Repository 的服务中来,做成了伪 DDD 模式。如图 B 的方式,在应用层(Application)注入 DomainFactory 服务,在 DomainFactory 中构建 Domain 对象时将 Repository 服务导入到 Domain 对象中。Application 无法直接调用 Repository 服务,只能通过 Domain 来操作 Repository 服务,这样避免了 Repository 作为上帝之手的角色。将业务封装在 Domain 中,最大可能的避免 Repository 的臃肿。

 4. 对象的聚合

做到上面三点之后,发现这不就是面向对象编程吗?为什么起一个领域驱动设计这样高大上的名字呢?没错,完成上面三项之后,就解决了 DDD 模式的大部分问题,还剩下的一个问题就是业务聚合。我们已经将业务封装在模型中,但是不可能把一个领域的所有业务都封装在一个模型中,为了完成一个领域业务会创建一系列模型,还需要考虑这些模型之间的关系,将一个模块的业务聚合在一个聚合根下面,同一个聚合根下的所有对象只能拥有唯一的访问入口,来保证聚合内部的一致性。例如 PaymentPassword 业务,同时还需要 PayPwdCtrl 来对支付密码进行校验控制,对 PayPwdCtrl 的访问只能通过 PaymentPassword 的入口来完成。

 

如何避免低效的查询服务

苏宁金融会员系统,不仅对外提供注册、**、帐密安全管理等用户生命周期的动作,同时还对多个外系统提供数据查询服务。很多查询服务查询的数据会跨越多个聚合领域,如果查询服务经过领域模型,势必存在效率问题。因此,有必要引入另外一个设计模式读写分离设计(CQRS)。

领域驱动设计实践,精通业务,面向对象编程,面条编程,过程编程

图 6 CQRS 设计图

业内有比较成熟的 CQRS+Event Sourcing 模式,但是事件溯源(Event Sourcing)比较复杂,而且对数据存储需要重新设计,所以在会员系统重构设计上抛弃了事件溯源模式,单独采用 CQRS 模式。

 

如何做读写分离设计

读写分离本身是一个比较朴素的设计,在系统中我们常用到缓存读写分离,数据库读写分离,那么服务读写分离应该如何设计呢?在系统架构上,通常采用水平拆分来提高程序的伸缩性,采用垂直拆分来提高程序的可扩展性。垂直拆分应当是按业务来拆分,下图 B 按读写分离进行垂直拆分打破了业务内聚属性,会增加后期维护难度。

领域驱动设计实践,精通业务,面向对象编程,面条编程,过程编程

图 7 读写分离设计

为了保证业务的内聚,会员服务系统采用图 A 这种方式,所有业务落在一个系统内部。在代码上实现读写分离,使用插件结构,将读写在设计上分离开来,对读写代码分开维护,独立演化,业务上保持一个系统的内聚。

 

图 8 基础插件设计图

 

如何实现插件模式设计。。。。。

 

如果你喜欢本文,查看全文,更多领域驱动设计文章,交流群

请长按二维码,关注 ,回复“领域”

领域驱动设计实践,精通业务,面向对象编程,面条编程,过程编程

转发至 朋友圈,是对我最大的支持