领域驱动设计之二

2022-05-01  本文已影响0人  gregoriusxu

前言

在这个时代,国人很少注重理论知识的积累,俗话说理论指导实践,好的理论都是在实践的基础之上积累下来的,是前人经验的总结。一个好的设计开发人员就体现在这些上面了,如果不注重知识积累,那么就只会一些花拳绣腿,技术上是很难有所提升的,我们先来看看常用的架构模式及演进过程,从中我们可以体会出领域驱动设计的由来以及好处。

架构模式

三层架构
三层演化架构

用户界面层,应用层,领域层,基础设施层

改进分层架构

依赖颠倒原则:高层模块不应该依赖低层模块,两者都应该依赖于抽象抽象不应该依赖于细节,细节应该依赖于抽象。我们应该将关注点放在领域层上,采用依赖颠倒原则,使领域层和基础设施层都只依赖于领域模型所定义的抽象接口。由于应用层是领域层的直接客户,它将依赖于领域层接口,并且间接地访问资源库和由基础设施层提供的实现类。应用层可以采用不同的方式来获取这些实现,包括依赖注入,服务工厂和插件

六边形架构(端口与适配器)

不同的客户通过“平等”的方式与系统交互,如果需要新的客户,只需要添加新的适配器将客户输入转为成能被系统API所理解的参数就行了。同时,系统输出,比如图形界面、持久化和消息等都可以通过不同的方式实现。所以我们有充足的理由认为,这将是一种具有持久生命力的架构。很多声称使用分层架构的团队实际上使用的是六边形的架构。这是因为很多项目都使用了某种形式的依赖注入。并不是说依赖注入天生就是六边形架构,而是说使用依赖注入的架构自然地具有了端口与适配器风格。

面向服务架构
REST

例如,一个简单的网络商店应用,列举所有商品,
GET http://www.store.com/products 呈现某一件商品,
GET http://www.store.com/products/12345 下单购买,
POST http://www.store.com/orders

<purchase-order>
<item>...</item>
</purchase-order>
CQRS(命令与查询责任分离)

CQRS最早来自于Betrand Meyer(Eiffel语言之父,开-闭原则OCP提出者)在Object-Oriented Software Construction 这本书中提到的一种命令查询分离(Command Query Separation.CQS)的概念。其基本思想在于,任何一个对象的方法可以分为两大类:

CQRS是对CQS模式的进一步改进成的一种简单模式。它由Greg Young在CQRS,Task Based UIs,Event Sourcing agh!这篇文章中提出。

“CQRS只是简单的将之前只需要创建一个对象拆分成了两个对象,这种分离是基于方法是执行命令还是执行查询这一原则来定的(这个和CQS的定义一致)"。

CQRS使用分离的接口将数据查询操作(Queries)和数据修改操作(Commands)分离开来,这也意味着在查询和更新过程中使用的数据模型也是不一样的。这样读和写逻辑就隔离开来了。

主数据库处理CUD,从库处理R,从库的的结构可以和主库的结构完全一样,也可以不一样,从库主要用来进行只读的查询操作。在数量上从库的个数也可以根据查询的规模进行扩展,在业务逻辑上也可以根据专题从主库中划分出不同的从库,从库地可以实现成RepgrtingDatabase,根据查询的业务需求,从全库中抽取一些必要的数据生成一系列查询报表来存储。

使用ReportingDatabase的一些优点通常可以使得查询变得更加简单高效:

当命令处理器执行结束后,一个聚合实例将被更新,同时命令模型还将发布一个领域事件。对于更新查询模型来说,这样的领域事件是至关重要的。值得注意的是,所发布的领域事件还可能导致另一些受同一命令影响的聚合实例的同步更新,最终,这些聚合实例都将与本次事务所修改的聚合实例保持最终一致性。

在命令模型更新之后,如果我们希望查询模型也得到相应的更新,那么从命令模型中发布的领域事件也是关键所在。在使用事件源时,领域事件也被用于持久化修改后的聚合。然而,事件源并不一定与CQRS一起使用。除非事件日志包含在业务需求之中。不然命令模型是可以通过ORM等方式进行持久化的。不管如何,我们都需要发布领域事件以更新查询模型。

用户界面处理:

  1. 用户提交命令时,同时更新页面数据。
  2. 在用户界面上显示出当前查询模型的日期和时间。要达到这样的目的,查询模型的每一条记录需要维护最后更新时的日期和时间,用户自己决定是否更新。
  3. Comet(Ajax Push)
  4. 分布式缓存网格(Coherence,Gemfire)的事件订阅
  5. 直接通知用户需要等一会。

术语解释

通用语言(Common Language)

限界上下文(Bound Context)

从广义上讲,领域即是一个组织所做的事情以及其中所包含的一切。
由于“领域模型”包含“领域”这个词,我们会认为应该为整个业务系统创建一个单一的、内聚的、全功能式的模型。然而,这并不是我们使用DDD的目标。正好相反,在DDD中,一个领域被分为若干子域,领域模型在限界上下文中完成开发。事实上,在开发一个领域模型时,我们关注的通常只是这个业务系统的某个方面。试图创建一个全功能的领域模型是非常困难的,并且很容易导致失败。一个限界上下文并不一定只包含在一个子域中。但这是可能的。一个限界上下文不应该包含岐义的领域特定术语。原则上是一个子域包含一个限界上下文。

实体(Entity)

实体标识的生成方式:

  1. 用户提供唯一标识
  2. 应用程序生成唯一标识
  3. 持久化机制生成唯一标识
  4. 另一个限界上下文生成唯一标识

值对象(Value Object)

领域事件(Domain Event)

领域事件作为领域模型的重要部分,是领域建模的工具之一。用来捕获领域中已经发生的事情。并不是领域中所有发生的事情都要建模为领域事件,要忽略无业务价值的事件。领域事件是领域专家所关心的(需要跟踪的、希望被通知的、会引起其他模型对象改变状态的)发生在领域中的一些事情。简而言之,领域事件是用来捕获领域中发生的具有业务价值的一些事情。它的本质就是事件,不要将其复杂化。在DDD中,领域事件作为通用语言的一种,是为了清晰表述领域中产生的事件概念,帮助我们深入理解领域模型。

引入领域事件的目的主要有两个,一是解耦,二是使用领域事件进行事务的拆分,通过引入事件存储,来实现数据的最终一致性。

事件存储(Event Store)和事件溯源(Event Sourcing)

为什么要持久化事件?

事件溯源的本质亦是如此,不过它存储的并非聚合每次变化的结果,而是存储应用在该聚合上的历史领域事件。当需要恢复某个状态时,需要把应用在聚合的领域事件按序“重放”到要恢复状态对应的领域事件为止。

聚合(Aggregation Root)

  1. 聚合设计的原则:
    聚合是用来封装真正的不变性,而不是简单的将对象组合在一起;聚合内强一致性,聚合之间最终一致性;聚合应尽量设计的小;聚合之间的关联通过ID,而不是对象引用;聚合内强一致性,聚合之间最终一致性;聚合是用来封装真正的不变性,而不是简单的将对象组合在一起这个原则,就是强调聚合的真正用途除了封装我们本身所关心的信息外,最主要的目的是为了封装业务规则,保证数据的一致性。在我看来,这一点是设计聚合时最重要和最需要考虑的点;当我们在设计聚合时,要多想想当前聚合封装了哪些业务规则,实现了哪些数据一致性。
  2. 聚合应尽量设计的小
    这个原则,更多的是从技术的角度去考虑的。通过一个例子来说明,该例子中,一开始聚合设计的很大,包含了很多实体,但是后来发现因为该聚合包含的东西过多,导致多人操作时并发冲突严重,导致系统可用性变差;后来开发团队将原来的大聚合拆分为多个小聚合,当然,拆分为小聚合后,原来大聚合内维护的业务规则同样在多个小聚合上有所体现。所以实现了既能解决并发冲突的问题,也能保证让聚合来封装业务规则,实现模型级别的数据一致性;聚合设计的小还有一个好处,就是:业务决定聚合,业务改变聚合。聚合设计的小除了可以降低并发冲突的可能性之外,同样减少了业务改变的时候,聚合的拆分个数,降低了聚合大幅重构(拆分)的可能性,从而能让我们的领域模型更能适应业务的变化

资源库(Repository)

资源库的是封装所有获取对象引用所需的逻辑。领域对象不需处理基础设施,以得到领域中对其他对象的所需的引用。只需从资源库中获取它们,于是模型重获它应有的清晰和焦点。

资源库会保存对某些对象的引用。当一个对象被创建出来时,它可以被保存到资源库中,然后以后使用时可从资源库中检索到。如果客户程序从资源库中请求一个对象,而资源库中并没有它,就会从存储介质中获取它。换种说法是,资源库作为一个全局的可访问对象的存储点而存在。

服务(Services)

当我们在分析某一领域时,一直在尝试如何将信息转化为领域模型,但并非所有的点我们都能用Model来涵盖。对象应当有属性,状态和行为,但有时领域中有一些行为是无法映射到具体的对象中的,我们也不能强行将其放入在某一个模型对象中,而将其单独作为一个方法又没有地方,此时就需要服务.

服务是无状态的,对象是有状态的。所谓状态,就是对象的基本属性:高矮胖瘦,年轻漂亮。服务本身也是对象,但它却没有属性(只有行为),因此说是无状态的。

服务存在的目的就是为领域提供简单的方法。为了提供大量便捷的方法,自然要关联许多领域模型,所以说,行为(Action)天生就应该存在于服务中。

服务具有以下特点:

模块(Moudles)

对于一个复杂的应用来说,领域模型将会变的越来越大,以至于很难去描述和理解,更别提模型之间的关系了。模块的出现,就是为了组织统一的模型概念来达到减少复杂性的目的。而另一个原因则是模块可以提高代码质量和可维护性,比如我们常说的高内聚,低耦合就是要提倡将相关的类内聚在一起实现模块化。

模块应当有对外的统一接口供其他模块调用,比如有三个对象在模块a中,那么模块b不应该直接操作这三个对象,而是操作暴露的接口。模块的命名也很有讲究,最好能够深层次反映领域模型。

总结

理论往往比实践全面的多,它只是对实践进行指导,并不是说你套弄了所有理论那么你才是领域驱动设计的,你的架构才是正统的领域驱动设计架构,不能以偏概全。但是你的架构如果想说是符合领域驱动设计的也不是那么简单,就像画画一样,如果没有抓住重点和主要特征,那么就是画猫为虎了。

参考

上一篇下一篇

猜你喜欢

热点阅读