本文转载自微信公众号「Java艺术」,作者wujiuye。转载本文请联系Java艺术公众号。
DDD为什么难推行?因为我们习惯了舒适,并不是我们不想接受新事物,而是因为我们懒思考,习惯了以往一贯的流程开发、面向数据库CRUD开发,很难转换思维。
DDD要求我们根据产品原型建模,识别领域、限界上下文、子域,这些需要时间思考的问题就像一座座大山,让我们望而却步。且由于项目前期看不到DDD带来的高效,反而没那么敏捷了,并且前期的建模还可能要推倒重来,这让更多人一开始就想放弃,而只有随着需求的不断迭代,DDD才会显示出它的优势。
本篇笔者就以近期的一个项目跟大家分享笔者为该项目识别领域、限界上下文、子域,以及建模的过程。当然,分享的只是最初的一个版本。
领域通常指的就是业务范围,每个公司都有自己明确的业务范围。通常每个公司内部都有很多个系统,如一家电商公司可能会有物流系统、电商系统、直播系统等等,每个系统做的事情则是更细分的领域。
茉莉红交所项目是笔者入职茉莉数科集团后做的第一个项目,也是一个新的项目,由于没有太多历史包袱,笔者选择从零开始搭建整个项目。并且由于初期业务简单,所以才选择在该项目试行DDD,寄希望于随着业务的不断迭代,能够看到DDD发挥出的优势。
最初从产品那里了解到该项目要做的业务就是OTO(线上到线下)探店,那么OTO探店就是我们要了解的领域。
探店其实是达人帮助商家做推广的一种有偿活动,商家通过免费让达人品尝美食或是免门票游玩景点,达人最终通过短视频、直播、图文内容等方式为商家做推广。无论是探美食店、探游乐园,探店都是这个领域的核心。
在探店这个领域中,核心的业务名词有:商家、达人、店铺、订单、任务。而行为有:商家发布探店订单,订单关联店铺,商家认证店铺,达人接单(任务),商家发布订单时通知达人,达人完成任务时通知商家。
现在,我们需要为业务划分限界上下文。
限界上下文是业务概念的边界,是业务问题最小粒度的划分。在OTO探店业务领域中会包含多个限界上下文,我们通过找出这些确定的限界上下文对系统进行解耦,要求每一个限界上下文其内部必须是紧密组织的、职责明确的、具有较高的内聚性。
我们划分出的限界上下文如下图所示。
为什么将任务和订单拆分为不同限界上下文(任务不是作为订单聚合根的实体,而是作为一个独立聚合的聚合根)?这是因为商家发布的一个订单允许有不同的多个达人接单,一个达人也可以接不同商家的订单,这并不是简单的一对多关系。这更像是商品与订单的关系,而不是订单与订单item的关系。
在划分出限界上下文后,还需要根据限界上下文识别出问题子域。问题子域是对业务问题的划分,相对限界上下文来说,是对业务问题更大粒度的划分。
- 核心(子)域:产品的核心竞争力、盈利来源;
- 通用子域:常见的,不同领域都可共用的,可通过购买就能使用的;
- 支撑子域:非核心域、又非通用域,具有个性化需求,用于支撑核心域运作;
根据限界上下文,我们划分出的子域如下图所示。
OTO探店核心域:商家创建订单、平台对订单审核,达人接单后生成任务、平台对任务审核,达人完成任务回填内容链接;
商家支撑子域:商家注册、商家审核;
达人支撑子域:达人注册、达人档案管理、达人粉丝数据提取;
店铺支撑子域:商家注册店铺、店铺审核;(根据下版本的需求,将把店铺当作商家聚合根的实体)
消息通知通用子域:短信通知、应用内通知、小程序消息推送。
在划分出子域后,我们就可以为领域建模了。
领域建模是通过将业务抽象为聚合、实体、聚合根、值对象模型的方式,封装和承载全部的业务逻辑,保持业务的高内聚和低耦合。
聚合:负责封装业务逻辑,内聚决策命令和领域事件,容纳实体、聚合根、值对象。
- 聚合根:也是一种实体,是聚合的根节点,如订单;
- 实体:聚合的主干,具有唯一标识和生命周期,如订单Item;
- 值对象:实体的附加业务概念,用于描述实体所包含的业务信息,如订单收件地址。
在技术实现上,一个聚合就是一个包,里面存放领域服务、工厂、资源库、聚合根、实体、值对象。
领域层包的划分规则通常为:
- --domain
- ----限界上下文
- ------聚合
- -------- (聚合根、值对象、实体、领域服务、资源库、领域事件)
- ------聚合
- -------- (聚合根、值对象、实体、领域服务、资源库、领域事件)
特别的,一个限界上下文可能包含多个聚合,但一个聚合只能存在于一个限界上下文。 如果一个限界上下文只有一个聚合,这种情况下我们通常省略限界上下文这一层。
以订单限界上下文、任务限界上下文为例:
- --domain
- ----ordercontext
- ------orderType(订单类型聚合(特殊的聚合,用于管理订单分类):美食(早餐/午餐/晚餐/下午茶)、...)
- ------order
- ----task
由于订单存在两个聚合,因此我们没有省略订单限界上下文这一层,而任务只有任务聚合,所以省略了任务限界上下文这一层。
以上包的划分只是领域层的划分,要求聚合根、值对象、实体、领域服务、资源库、领域事件等类存放在聚合包下,无论是使用DDD经典四层架构,还是六边形架构。
我们并非采用DDD经典四层架构,也非六边形架构,我们实际对项目包的划分如下。
当我们需要按限界上下文拆分订单和任务为两个微服务时,只需要copy一份项目代码,一个项目中去掉ordercontext包,一个项目中去掉task包,并且将两个限界上下文应用层之间的依赖调用改为通过远程RPC调用。上图中的xxxGateway类就是用于封装远程调用的,UserApplicationServiceGateway、RabbitmqConfiguration之所以放在最外层,因为两个限界上下文都会用到。
以任务聚合为例,展开后的包结构如下。
以上全部就是我们最初对OTO探店业务识别限界上下文、拆分子域、领域建模的过程,根据目前需求排期来看,这个模型我们即将要推倒重来一次,但对代码的改动应该不大。
因为DDD缺少权威性的实践指导和代码约束,我们只能是通过实践慢慢积累经验。