目录

软件工程的开发方法都是过去行业人实践积累下来的方法,都是根据实际项目情况,总结不同项目,抽象出的一套开发方法概念,本身项目时间过程是一个朴实无华的过程,经过抽象、修饰之后,给出一些看起来高深莫测的概念。敏捷开发、极限编程、微服务、领域驱动等,今天了解下DDD,也就是领域驱动设计方法。

分而治之

我们先了解下分而治之的思想。

复杂的问题,通常的解决方式是分而治之。

产品项目,或者叫软件工程,从一个页面一个功能,到一个模块一个服务,再到一个系统一个平台,系统平台越来越复杂越庞大,大体都可以通过化繁为简,像搭积木搞基建制造高铁飞机火箭工程一样,分而治之是最为有效高效的解决方案。

什么是领域驱动设计

领域常常和业务专业性强关联,比如电商领域、金融领域、医药领域、教育领域、零售领域、家装建材领域、外贸跨境领域、电子竞技领域等,在领域内,有各自的规则、业务知识,面对复杂的领域知识与规则,分而治之的方法可以有效地把控规则变化,应对垂直领域复杂的知识。

领域本身可能很大,可以再细拆分成更小的领域,将一个复杂的问题拆分单独求解,最终将求解汇总得到复杂问题的解答。

在软件工程架构师考核中,会了解到一个新的架构设计模式:领域驱动设计,也就是DDD,Domain Drive Design。

领域驱动设计思想在十几年前就已经被提出了,

过去20年更多的架构设计还是MVC及延伸的架构模式

随着互联网的发展,结合其他行业的产品项目越来越多,融合深度越深,产品项目服务庞大,项目管理及技术架构管理上复杂度难度加大

领域驱动设计要解决什么问题

业务的发展,使得系统从单体发展到了微服务,微服务将大问题拆解成小问题,解决了高可用问题(性能与可用性),但从工程角度上看,微服务在功能维度和工程维度上做的不够,所以就有了领域驱动设计,领域驱动设计可以弥补微服务的这个缺陷。

  • 统一语言,提炼领域知识。

    统一沟通关键词、统一领域知识、统一系统目标、范围及具备的功能。为之后的高效沟通打下基础。

  • 领域拆分

    领域本身所描述的是范围,业务一开始通常是复杂和难以描述的边界,或者说无边际,通过分而治之,将问题逐级细分降低业务逻辑或技术上的难度。

    对于比较大的领域,还可以继续子领域的划分,常用的基于业务功能的拆分,比如营销活动平台,可以通过投票、秒杀、砍价、抽奖、开奖、预约等活动的不同进行拆分子领域。除了按照功能划分出的子领域外,还有比如用户领域、财务领域、会员领域、内容领域、营销推送领域等,这些领域有些是系统的核心领域,有些是提供支撑的支撑领域,而通用领域,可以向其他领域提供服务。

单体架构与微服务 - 9ong

RPC服务 - 9ong

架构图

DDD领域驱动设计架构图

代码组织方式

MVC,按照model、view、controller、service的维度去组织项目代码

DDD,按照领域/实体维度,并结合service、model组织项目代码

依然通过facade门面的方式,对外提供功能服务

设计方法

  • 自底向上

    过去通常的功能设计是自底向上的

    先设计数据模型,比如关系型数据库的表结构,再实现业务逻辑。在前后端配合编码的时候,经常hi听到:“我先把数据库表的字段设计出来先”。这种方式是通过技术的视角,关注点放在数据模型上,其实并没有忽略业务领域模型,而是已经提前了解了业务模型,透过了业务逻辑、service直接开始设计model。

  • 自顶向下

    而自顶向下是拿到一个业务需求,先与需求方确定好输入数据格式,然后实现Controller和ApplicationService,再实现领域模型(此时的领域模型通常已经被识别出来),最后实现持久化。业务逻辑都是在领域模型中,比如用户领域user、订单领域order、分享领域share等,而ApplicationService作为领域模型的门面,提供一个代理领域模型(业务逻辑)的作用,提供不同的Controller调用,而Controller对系统外部来说是系统门面。

自底向上方法像是一种反推的方式去设计编码,而自顶向下更自然,从源头开始,需要什么准备什么提供什么,直到底层数据模型实现,DDD采用的是自顶向下的方法,其实在软件工程中,我们也常讲自顶向下逐步求精的设计方法,但往往在设计与编码的过程中,下意识在脑海里或图或草稿里梳理controller、service,然后真正开始动手的是底层模型的设计。

领域驱动设计模式下,ApplicationService这层代理相对薄一些,我们甚至可以将ApplicationService层及以下领域模型层打包剥离成一个单独的服务,提供给Controller远程调用服务(rpc或http等方式),代码组织与架构调整更加灵活,可以根据项目阶段、体量等维度去调整架构方式。

业务逻辑与控制逻辑

如何区分业务逻辑与控制逻辑

业务逻辑在领域模型中

controller做系统门面提供业务流程大方向上的控制,ApplicationService作为领域模型的门面,提供单个领域服务流程方向上的控制

门面接受用户输入的原始数据,通过领域模型后,转换成模型内的对象数据,方面内部数据检测与处理。

所谓控制,就是对程序流转的与业务无关的代码或系统的控制(如多线程、异步、服务发现、部署、弹性伸缩等)。

所谓逻辑则是实实在在的业务逻辑,是解决用户问题的逻辑。

控制和逻辑控制了整体的软件复杂度,有效的分离控制和逻辑会让系统得到最大的简化。

简单来说,控制逻辑就是判断是否为空值、遍历数据、查找数据、多线程、并发、异步等,业务逻辑则就是用户需要的行为逻辑。

比如说,最常见的注册用户场景,创建用户之后需要异步给用户推送消息通知。创建用户和创建用户后的推送文本是业务,而异步、推送消息、推送失败失败重试机制等是控制逻辑。

有效地分离业务逻辑、控制逻辑 和 数据 是写出好程序的关键所在。软件就是程序、数据、文档的集合,而程序包含业务逻辑与控制逻辑程序。

虽然我们说业务逻辑和控制逻辑的分离有助于简化,但实际操作中,业务逻辑和控制逻辑并不是完全分离的,我们常看到的是控制逻辑控制业务流程,业务逻辑实现业务流程中每个业务环节,而一个业务逻辑里必然也少不了控制逻辑,比如判断、循环等,所以我们说的分离,是大方向的分离,尽量在大方向上将控制逻辑与业务逻辑分离,控制层更多的是流程控制,判断、循环、并发、回调、触发事件等。

概念

上下文

上下文不是一个具体的东西,context和我们文章常说的上下文的意思是一样的,文章里老师经常让我们结合语境,这里的语境我们就可以理解为上下文,而在程序里,比如javascript中的this、java中的this、php的this、golang的方法通过第一个实参recevier接受者,这些都可以看做是方法或函数的上下文。

上下文能够帮助更完整的表达方法或函数中属性或操作。

有人将上下文理解成环境,这也是个想法。

聚合根

业务载体,比如用户user、会员member、订单order、分享share(不同平台、不同分享方式等)、活动、仓储单、相册等等实例/实体,也就是可以围绕实体完成与实体相关的一些业务逻辑。

聚合根,避免出现大而全的根,需要合理的划分模块,生成相应的聚合根。

聚合根,讲究高内聚低耦合一致性,除了类内聚,业务方法也需要内聚,一个业务方法具有原子性、独立性、一致性,内聚到方法不建议再细分为多个方法,也不要做到一个方法实现多个业务功能。

比如说订单聚合根,订单下单的方法Order.newOrder(),下单是一个业务过程,涉及到生成订单号、检查商品信息、写入用户信息、写入订单信息等等,对于ApplicationService层,只需要调用Order.newOrder方法就完成下单,而不需要提前调用Order.getOrderId()获取订单号再给newOrder去完成下单,也不需要调用newOrder后再去调用Order.notify()通知用户,生成订单号与通知用户的逻辑应该是下单的逻辑,也就是需要内聚到newOrder里去,不应暴露给ApplicationService层。

内聚也需要一个度,不能过度内聚,比如newOrder,还支持updateOrder的功能,一个类和一个方法尽量只做一件事。

当然以上都是思想,最终仍然需要结合项目实际情况,没有完美的解决方案,避免不了耦合,也避免不了冗余业务方法。

特性:

  • 聚合根的实现应该与框架无关

    既然DDD讲求业务复杂度和技术复杂度的分离,那么作为业务主要载体的聚合根应该尽量少地引用技术框架级别的设施。比如哪天我们想把框架更换成其他框架,我们只需要将领域模型核心代码迁移过去,这就会是一种很不错的体验。或者说,很多时候技术框架会有“大步”的升级,这种升级会导致框架中API的变化并且不再支持向后兼容,此时如果我们的领域模与框架无关,那么便可做到在框架升级的过程中幸免于难。

  • 聚合根之间的引用通过对应的ID完成

    在聚合根边界设计合理的情况下,一次业务用例只会更新一个聚合根,此时你在该聚合根中去引用另外聚合根的整体并不是很好的选择。我们可以在Order下的OrderItem引用了ProductId,而不是整个Product。

  • 聚合根内部的所有变更都必须通过聚合根完成

    为了保证聚合根的一致性,同时避免聚合根内部逻辑向外泄露,客户方只能将整个聚合根作为统一调用入口。

    如果一个事务需要更新多个聚合根,首先思考一下所有聚合根边界处理是否出了问题,因为在设计合理的情况下通常不会出现一个事务更新多个聚合根的场景。如果这种情况的确是业务所需,那么考虑引入消息机制和事件驱动架构,保证一个事务只更新一个聚合根,然后通过消息机制异步更新其他聚合根。

  • 聚合根独立与基础设施

  • 聚合根内部数据结构对外部可见,也就是说外部不会持有聚合根的数据结构

  • 适当划分聚合根,尽量使用小聚合

实体对象

实体对象表示的是具有一定生命周期并且拥有全局唯一标识(ID)的对象,比如本文中的Order和Product,而值对象表示用于起描述性作用的,没有唯一标识的对象,比如Address对象。

聚合根一定是实体对象,但是并不是所有实体对象都是聚合根,同时聚合根还可以拥有其他子实体对象。

值对象

当一个对象用于对事务进行描述而没有唯一标识时,它被称作值对象。

比如在下单的时候,我们有个地址属性,这个地址,不需要我们去跟踪唯一性,一旦用户有修改,我们就重新生成,不用去维护。在实践中,需要保证值对象创建后就不能被修改,即不允许外部再修改其属性。在不同上下文集成时,会出现模型概念的公用,如商品模型会存在于电商的各个上下文中。在订单上下文中如果你只关注下单时商品信息快照,那么将商品对象视为值对象是很好的选择。

实践过程中,会发现一些领域对象符合值对象的概念,但是随着业务的变动,很多原有的定义会发生变更,值对象可能需要在业务意义具有唯一标识,而对这类值对象的重构往往需要较高成本。因此在特定的情况下,我们也要根据实际情况来权衡领域对象的选型。比如上面说的地址,我们可能需要绑定用户的一些地址,并用于后续快递选择、快递费用决策、用户地域分析等时,这个地址就不仅仅只是用于下单环节,已经发展成了一个实体。

资源库

资源库是聚合根的家,聚合根都有一个自己的资源库repository

资源库的本质就是CRUD,但一般只会看到getById()、save()这样的方法。资源库负责提供一个聚合根对象,负责将聚合根对象的状态从内存同步到持久化机制中,所以只需要save,但从技术角度上看,新增和更新还是有区别的,只是调用者无需关心,他们会被隐藏在save中,比如mysql提供的on duplicate key update,自动判断存在记录时,更新数据,当然也可以手动查询判断再更新或新增。

聚合根业务流程

聚合根生成

聚合根通常通过工厂模式来创建

比如简单工厂方法:

public static function create($config){
    return new Order($config);
}

或者更复杂的工厂类、抽象工厂类

领域服务

并不是所有的业务逻辑代码都放在聚合根内,比如虽然生成订单号,看起来和订单强关联,生成订单号、支付号等等一些看似和业务逻辑相关联,但又可独立复用的,实际上可以引入领域服务。

参考