DDD 的全称是 Domain-driven Design ,即领域驱动设计。2004 年埃里克·埃文斯发表了《领域驱动设计》这本书,从此领域驱动设计)诞生。DDD 核心思想是通过领域驱动设计方法定义领域模型,从而确定业务和应用边界,保证业务模型与代码模型的一致性,如今已经发展为一种针对大型复杂系统的领域建模与分析方法。
DDD 是一种致力于降低或隐藏整个系统业务复杂性,让系统具有更好扩展,应对纷杂繁多的现实业务问题的架构方法。
领域
领域就是用来确定范围的,范围即边界,这也是 DDD 在设计中不断强调边界的原因。
在领域不断划分的过程中,领域会细分为不同的子域,子域可以根据自身重要性和功能属性划分为三类子域,它们分别是:核心域、通用域和支撑域。
- 核心域:产品的核心的模块,能够给产品提供核心竞争力。
- 支撑域:则具有企业特性,但不具有通用性,例如电商系统中的物流,仓储等模块。
- 通用域:有一定通用性,比如认证、权限、日志处理等服务,不是业务的核心,但是没有他们业务也无法运转。
关于子域的划分也是参考业务属性,可以把核心域理解为最关键的业务场景,并且需要资源倾斜以应对其不断的发展;支撑域可以理解为相对稳定的业务;通用域偏向系统架构层面的公共能力;通过对领域的拆分实现业务分治,这与微服务的拆分思想相符合,两种模式在业务角度是比较统一的。
实体和值对象
所谓领域,反映到代码里就是模型。模型分为实体和值对象两种。
- 实体:是有标识(Identity)的,两个拥有相同属性的实体不是相等的,除非它们的标识相等;而不同实体的标识不能相等。实体具有生命周期,它们的内容可能在这期间会发生改变,但是标识是永远不会变化的。
- 值对象:没有任何标识,用于描述或度量一个东西,只要两个值对象的属性相等,那么它们就是相等的。值对象没有生命周期。
不同的领域需求可能会催生不同的建模。例如:对于售票系统,如果需求是对号入座,那么座位就是实体,一旦某张演出票关联了某个座位,那么这个座位就再也不能被其它的演出票所关联了;如果需求是先到先坐,那么座位就是值对象,我们只关心卖了多少张演出票,不要超过座位上限即可,而并不用关心哪个座位被哪张票所关联了。
实体里可以包含值对象,值对象里也可以包含实体。值对象和实体一样,都需要有自己的方法,方法名来自于通用语言。通过这些方法来保证自己始终是一致的状态,而非被调用者set来set去。例如:people.runTo(x, y),而非 people.setX(x);people.setY(y);
聚合和聚合根
聚合就是一组应该待在一起的对象,聚合根(Aggregate Root)就是聚合在一起的基础,并提供对这个聚合的操作。
领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合。聚合是持久化的一个单位,我们需要保证在实现共同的业务逻辑时,以聚合为单位的数据一致性。
聚合根必须是实体而非值对象,因为它需要整体持久化,所以一定会有标识。而聚合根里的各个元素,既可能是实体,也可能是值对象。例如:一个订单(聚合根)一般会有订单明细(实体)和送货地址(值对象)。这些元素里可以有对聚合根的引用,但是不能相互引用。任何对其它元素的操作都必须通过聚合根来进行。聚合根拥有自己独立的生命周期,其实体的生命周期从属于其所属的聚合,值对象因为只是值而已,并没有生命周期。
聚合的特点:高内聚、低耦合,它是领域模型中最底层的边界,可以作为拆分微服务的最小单位。
PS:实体/值对象是类,聚合是包。
- 意义:边界。一个聚合包提供一个根实体作为访问入口,外部不能直接操作或持有非根实体。
- 划分思路:UML –组合/继承 –> 聚合
- 代码:合理代码形式应该是一个聚合独立一个模块,而不是现在这种垂直的分包。但无论哪种代码结构,都要遵从一个域(聚合)的访问要以聚合根为入口。
- 错误:dao包(AAADao,BBBDao),service包(AAAService,BBBService)
- 正确:AAA包(AAADao,AAAService),BBB包(BBBDao,BBBService),仅提供聚合根为public
通用语言
我们知道,每个人对一个事物的理解可能都是有一些微小的差别的。比如在谈到“商品”的时候,在“店铺”这个业务场景可能比较关注的是商品的标题、图片、价格等等,但在库存这个业务场景,关注的可能是商品的编号、存量、入库时间和出库时间等等。
所以同一个词可能要不同的意思。尤其是一些比较宽泛的词,如“商品”、“订单”、“用户”等等。这样就可能会造成歧义,我说的模型和你说的模型可能不是同一个模型。如果不加以区分,最终设计出来的模型可能就是一个过于复杂和混乱的模型。
所以我们需要统一语言,比如在销售上下文中,商品叫做“销售商品”,在库存上下文中,叫做“库存货物”,在物流上下文中,叫做“物流货物”等等。这样便可以在不同的业务场景关注不同的属性,有不同的操作,从而设计出更能准确表达业务意义的模型。
领域驱动开发让业务专家(Domain Expert)和开发人员一起来梳理业务,而双方有效沟通的方式是使用通用语言,在这个项目里,一开始我们就定义了很多词汇表, 就是我们自己的通用语言。
限界上下文
限界上下文主要用来封装通用语言和领域模型,显式地定义了领域模型的边界。当模型被一个显示的边界所包围时,其中每个概念的含义便是确定的了,因此,限界上下文主要是一个语义上的边界。
原则上,一个上下文就对应一个子域。但这并不是绝对的,限界上下文的目的是为了更加明确领域模型的职责和范围,是从“解决方案空间”来考虑问题的。通常来说,一个限界上下文可以是一个微服务,或者一个 Module。
上下文划分
那么如何划分限界上下文呢,有以下三个原则可以参考:
- 概念相同,含义不同:即上面所说的“通用语言”的例子,如果一个模型在一个上下文里面有歧义,那有歧义的地方就是边界所在,应该把它们拆到不同的限界上下文中。
- 外部系统:有时候系统需要同外部系统打交道,这个时候可以把与外部系统打交道的那部分拆分出去以实现更好的扩展性。这样一旦外部系统发生了变化,就不会影响到我们的核心业务逻辑。
- 向组织扩展:尽量不要两个团队一起在一个限界上下文里面开发,因为这样可能会存在沟通不顺畅、集成困难等问题。
上下文映射
当我们划分好了限界上下文后,就需要考虑一下上下文的映射关系了。为什么要考虑它们之间的映射关系呢?因为这样可以让我们从宏观上看到每个上下文之间的关系,从而能够更好地指导我们后续的程序设计。另一方面,如果一旦发现某个限界上下文与过多的其它限界上下文具有联系,那可能需要考虑拆分这个限界上下文了。
对于不同的限界上下文之间,通过上下文映射图(Context Map)来进行交互,用 Upstream 的首字母U来标识“上游”,用 Downstream 的首字母D来标识“下游”。那怎么去判断“上游”还是“下游”呢?这里有一个原则:下游的模型依赖于上游的模型和服务。
我们对上面的图进行限界上下文的映射之后可以得到:
由于上下游的限界上下文模型不同,实现时,可以用 RPC、Restful、消息机制等集成方式。另外,下游需要防腐层(Anticorruption Layer)来将上游的返回内容翻译为下游的领域模型。如果防腐层过多地使用了各种赋值,从而导致上下游的模型非常类似,那就需要看看是否下游过多地使用了上游的数据,从而导致自己的模型不清晰。
领域服务
有些操作不属于实体或者值对象,那就不用强塞给它们,创建领域服务来提供这些操作吧。留意通用语言,如果里面出现了名词,那一般就是实体或值对象;如果里面出现了动词,那通常就意味着领域服务。例如:支付,这是一个比较明显的业务操作。
另外,如果有什么操作会让实体变得臃肿,也可以使用领域服务来解决。但是,不能把所有的东西都堆到领域服务里,过度使用领域服务会导致贫血对象的产生。
据 Eric Evans所言,设计良好的领域服务具有以下三个特征:
- 操作不是实体/值对象的一个自然的部分
- 接口根据领域模型的其它元素定义
- 操作无状态
还需要注意的是,不要把领域服务和应用服务混起来了。我们在领域服务里处理业务逻辑,而并不在应用服务里处理。应用服务是领域模型的直接客户,负责处理事务、安全等操作。
领域事件
领域事件是一个定义了领域专家所关心的事件的对象。当关心的状态由于模型行为而发生改变时,系统将发布领域事件。
如果通用语言里出现了:“当……的时候,需要……”通常就意味着一个领域事件。例如:当订单完成支付时,商品需要出库。这里的订单完成支付就预示着一个 OrderPaidEvent,里面持有着这个订单的标识。
领域事件代表的是已经发生的事,所以命名上通常都使用过去时(如Paid)。对领域事件的处理就像是一个观察者模式,由领域事件的订阅方来决定。订阅方既可以是本地的限界上下文,也可以是外部的限界上下文。
今天的文章数据驱动与模型驱动的区别_数据驱动与模型驱动的区别分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/70948.html