DDD落地思考
起因
最近两周一直在网上寻找各种DDD落地文章,也一直在思考,因此做个记录
DDD概念整理
1.限界上下文
用来区分一个实体的不同的应用场景,以商品的生命周期为例,在生产上下文作为产品,在仓库上下文作为货物,在买卖上下文作为商品 等等。
2.应用服务
非常薄的一层,不包含业务逻辑,只包含调度逻辑。
3.领域服务
包含跨聚合的业务逻辑。
4.聚合根
包含聚合内的业务逻辑。
5.实体
实体内的业务逻辑。
6.仓储
以聚合为单位,进行存和取。
落地
DDD概念上其实不难,但落地太难,这两者之间有巨大的鸿沟。
CQRS的职责划分之落地
Q1.是否采用CQRS?
A:因为业务逻辑和展示逻辑的巨大不同,倾向于采用CQRS区分。
Q2.采用CQRS,怎么解决领域逻辑业务上多样的搜索请求?
即使CQ分离,因为我们要做各种业务校验,Command端领域逻辑业务的多样且实时的Query请求还是不可避免,Query端Repo本为多变视图而存在,那么Command端Repo是否需要提供Query接口?
A: 可以并只提供一个Query接口并实现规约模式来封装Predicate函数 以组合搜索条件。不同于Query端Repo,这样返回的对象是个领域聚合,有利于领域模型的处理。
聚合根,领域服务和应用服务的职责划分之落地
怎么避免一层一层空壳调用?
怎么解决多变的命令和多层调用的矛盾?
怎么统一处理校验逻辑,避免散落到各个层?怎么处理业务逻辑,避免泄露到外层?
这个其实一直是痛点,我想了很久,最后想了一个大致的方案。如下:
其实主要的关注点在于聚合根的方法设计上,为了不让逻辑泄露,我们要想方设法给聚合增加业务逻辑,但重要的矛盾点在于其实大部分业务逻辑,
最终还是跨聚合的,许多方法执行前都要验证或者获取信息,因为聚合严格限制了内部数据的权限,这个步骤许多人会交给领域服务甚至应用服务去做。
但我认为这样不合理,就像一个小组长(聚合根)要做某件事,难道事事都要大组长(领域服务)来协调其他小组长,把数据喂到嘴边麽?
大组长做的事应该是给各个小组长下达命令并根据结果成功与否来判断下一步才对,那么某个聚合需要协调怎么办,聚合内定好规则(只引用领域对象),找代理帮忙执行(适配器:屏蔽与外界的交互)。
1.聚合的设计:
1. 新建
- 聚合只提供一个全参构造函数, 通过EntityFactory.createFromXXX来接收并转换各种奇怪的参数对象。
- 如果聚合内还有Entity,那么就在父EntityFactory中调用子EntityFactory进行创建。
- Factory负责参数的验证和参数转换: 注:此过程可能需要依赖外部服务或者Repo来进行,此时由EntityFactory负责调用代理。
- 权限验证: 即requestor是否拥有该聚合实例对应该操作的创建权限,比如请假单聚合,student只有对自己请假单的create权限。
- 有效性验证: 即各个参数的格式有效性和业务有效性,比如订单聚合的创建,要检查userId对应的用户是不是disable,或者注册用户的username,是否已存在。
- 执行转换: 如果和外部服务通信,则需要 把VO转换成DO对象。
- 业务一致性验证:Factory组装新建完成后通过调用Entity.Validate方法
2. 重建
- 这个是发生在Repo层的实现层,从Repo重建聚合,可以通过框架完成,也可以通过EntityFactory.createFromXXX来组装不同的PO对象
3. 一致性验证方法
- 只在新建的时候验证,之后的状态转换的一致性由对应的业务方法来保证
- 实现逻辑,利用规约模式把不同条件组装起来,由Entity.validate方法统一调用
4. Get方法
- 外部/内部调用会希望用各种方式各种参数来拿到聚合内部状态,并且调用可能发生在聚合持久化前,因此无法调用Command端的Query接口得到结果。
- 怎么解?1.直接通过聚合根调用GetXXX方法,传递Prediction条件 2.外部循环调用Get方法: AggRoot.getEntity1(Conditions).getEntity2(Conditions), 只返回拷贝对象。
5. 业务方法
- 方法粒度: 不提供通用的update,只提供针对聚合的原子粒度的业务方法,比如请假单聚合的approve,submit方法,账户聚合的debit(price),credit方法
- 方法参数: Command(成员也是最小粒度:Id),
- Preprocess:
- 权限验证: 即requestor是否拥有该聚合实例对应该操作的权限,比如请假单聚合,student只有对自己请假单的submit权限,teacher有对所属学生请假单的approve权限。
- 有效性验证: 即各个参数的格式有效性和业务有效性,比如订单聚合的submit方法,要检查各个参与方: userId(是不是账户被冻结), itemId(是不是下架)
- 业务一致性验证: 比如订单状态为待付款的,就不能执行退货操作。
- 实现逻辑: 上面的每种验证都用规约模式组合,最后用责任链模式串联起来,在业务执行之前调用。
- 返回值:返回值分两部分1.方法返回值 2.聚合事件。错误则抛聚合业务异常。
注:上面的每个验证逻辑都有可能依赖外部服务,由各自Validator负责调用代理,但业务逻辑不应该调用外部服务。
2. 领域服务
1.方法的粒度: 聚合协调的最小粒度,也是一个事务的粒度。比如转账(两个account聚合),购物车创建订单(cart聚合的扣除和order聚合的新建)。
2.方法的参数:Command(成员也是最小粒度:Id))
3.负责从Repo中存取聚合
4.不负责参数验证和转换,只负责传递参数给各个聚合。 注1: 验证全权委托给聚合,只负责协调
注2:假如聚合是个外部服务,此时由领域服务负责调用代理。
5.收集聚合执行结果并根据结果,若成功执行下一步,若异常则抛给应用层。 注:可使用函数编程对流程建模
6. 返回值: 没有返回值,错误则抛领域服务业务异常。
*7 事务一致性: 一致性怎么保证?XA? Saga?
3. 应用服务
- 执行方法: 用例,协调不同模块,比如 下单用例: 执行下单->分发优惠券->短信通知等。
- 参数: Command,不验证,负责转换成不同Command传递给不同领域服务。
- 返回值: 无返回值,错误则抛应用服务业务异常。
- 只读方法: 将该应用下所有聚合的每个 聚合和跨聚合的 只读方法以RESTFUL的形式包装: 以服务于 领域服务的跨服务调用和聚合的跨服务新建&验证逻辑。
1.参数: Query。
2.返回值: VO对象
- 事务一致性: 弱一致性怎么保证? Saga?
在划分出限界上下文之后,如何应对新增一个全新业务线的需求
为了避免大泥球的单体,我们用DDD把它划分开了,但绕不开的问题是假如有一个新业务线,大部分流程是一致的,但许多细节是不同的。
为了避免烟囱式的应用,如何最大程度的复用已有的服务?
这个问题其实在整洁架构有提到,如果架构设计不好,拆分了之后,可能会造成更大的麻烦。作者只简单提了下,在每个服务提供多个扩展点,使用每个扩展点为不同业务线进行插件化的形式来解决。
举例
打车应用,可能有正常打车,顺风车,之后可能承接货运,搬家,送宠物(from Clean Architecture)等业务, 怎么解?
思路
但具体如何解决还有许多疑问,如下:
- 如何在一开始确定扩展点?
- 如何实现扩展点?
- 流程变动还好解决,但如果牵扯到数据变动,比如不同的订单显示的字段不一样,如何解决?
对于问题1,目前有个模糊的思路:
假如把用例抽象成一个流程Pipeline,由不同Stage组成。那么变化点有:
- 同一业务线里,参与者(人或物)的角色不同: 比如购买流程,user可能是企业用户/个人用户, 购买的商品可能是单个商品/商品组合套餐。
- 不同业务线里,参与者(人或物)的角色不同: 比如打车业务线里,运送的是人,货运业务线里,运送的是物。
- 不同业务线里,Stage之间的组装顺序和调用逻辑不同:比如审批流程,有的只要直系领导审批,有的要多层领导审批。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战