领域驱动设计part3

(点击蓝字可查看Part2篇)


接下来是仓储,负责怎么去存取领域对象,以前有人经常问我,领域对象的结构和数据表的结构看起来很像,我为什么还要去建立领域模型,直接建立E-R图不就好了?究竟有什么差别?在某一些情况之下,确实很像,但多数时候,领域模型是对象在内存中的结构关系,而且不仅是数据结构,更是行为结构关系,为了优化行为的高内聚低耦合,这种内存结构可能会被拆得很细,还会用到继承、多态,你想,这种结构如果直接映射为数据存储结构,会带来多大的性能问题?甚至是不可实现的,数据存储的时候首先想到的是性能的问题,这个目标与对象行为的解耦合设计目标多数情况是阻抗不匹配的,这种矛盾必须被隔离在不同的层,也就是隔离到领域层和DAO层,通过仓储层对它们进行结构转化,这就是仓储的意义所在。


既然聚合是不可拆分的一致性逻辑封装体,那么,仓储就必须以聚合为单位完整地加载整个聚合,应该避免单独加载被肢解的聚合数据,否则,就失去了聚合的完整性意义;


仓储接口的调用接口应当归属领域层,而实现放在仓储层,这种设计,叫做依赖倒置,什么叫“依赖倒置”?这个原则认为:接口的调用方依赖接口,但是他并不关心接口是如何实现的,所以,接口的定义就相当于是调用方提出的“需求”定义(不关心怎么被实现),既然需求的提出者就是需求的拥有着,那么接口的提出者就是接口的拥有者——领域层,所以,接口就应该和领域层在一起,依赖倒置的基本理念是:系统应该从不稳定的模块向稳定的模块依赖,而不是相反,现在,由于接口归属到了稳定的领域层,接口的实现——也就是不稳定的仓储层(可以随时改变实现方式),就反向依赖了稳定的领域层的接口,这就满足了依赖倒置的理念。


 由于领域对象是富血模型,需要在构造期依赖注入其他对象才能支持一些操作,但是,如果要对领域对象进行缓存,缓存的反序列化操作难以恢复依赖注入,所以,必须在仓储中把领域模型转化为贫血模型形式的PO后,交由ORM进行缓存或保存,这就解释了前面提到的问题——为什么不能直接使用ORM对领域对象进行管理和延迟加载,因为如果ORM操作的就是富血模型的PO,也就是领域对象(例如mybais就可以做到),那么ORM就无法同时提供可以缓存的贫血模型的PO了,为了从ORM得到贫血模型的PO,ORM就不能直接生成富血模型的PO,而应该先让ORM生成贫血模型的PO并进行缓存后,再让仓储用这个PO去装配成领域对象。



如果我们要把数据和逻辑行为绑定,就一定会用到富血模型的编程模式,富血模型是相对于贫血模型而言的,贫血模型指的是领域模型只有数据和get/Set方法,业务逻辑操作转移到外部,和数据分离,这其实是大多数我所见到的开发人员的代码风格,而富血模型根本上是采用了信息专家原则——让拥有数据的专家对象拥有操作这些数据的权力,使得无论是聚合根还是其内部对象都获得了封装一致性逻辑的能力,这种对象自治是一种保护性编程,使得代码更加的内聚和模块化,提升代码的健壮性。


如果采用贫血模型,所有的逻辑都被放在事务脚本中执行,就需要把事务脚本拆分到多个细粒度的事务脚本中来完成模块化——无论是拆分方法还是拆分服务对象,否则,这个事务脚本就会变得很臃肿,但是,如果所有细粒度对象自然而然地把属于自身逻辑的操作都纳入自己内部,就避免产生大量额外的细粒度事务脚本,使得高层的服务自然地变得简洁优雅;


再有,事务脚本之所以复杂,是因为有着太多的逻辑判断,这些需要进行逻辑判断的数据代表着不同的状态,现在,富血模型把状态数据和操作逻辑封装在一起,而且,它们还被封装为相同接口下的不同实现,对于同一个接口的不同实现的调用,还需要判断吗?你只要利用多态,自动调用不同的实现就可以操作不同的数据了吧?而且,这些被封装的数据的一致性还更有保障,这就是领域对象采用富血模型的好处。



DDD实现还有一个问题,大家知道聚合根是非常结构化的东西,这个结构化的东西带来的一个负面的影响:大家查询的需求是非常灵活的,但聚合根的结构是固定的,如果走聚合根,你要对获取的聚合内的结构化数据进行二次的非结构化过滤和转换,这就是不必要的复杂性。那我们为什么需要聚合根?聚合根的意义在于一致性逻辑的封装,其实只有在写入时才需要这种一致性保护,而不是读的时候。因此,聚合根只跟写有关,那读就完全没有必要用聚合根,所以应该绕过聚合根。因此就提出一个概念,就是我们数据的读和取要从两条路径走。这就叫做CQRS。



从前面的这些分析就会发现,由于依赖倒置,领域驱动开发的架构实际上已经从传统的层级的架构转变成为洋葱式的架构。领域核心,不会因为场景变化而变化,是被依赖,所以处于中间。第二层就是服务,封装与场景无关的可复用的调用,第三层是应用——和场景或用例对应,再外是输入输出层,显然,用户接口和数据库存储都是输入输出相关的,并且由于依赖倒置,让数据库从外部依赖内部核心,不再是被依赖的底层,这就是洋葱式架构。



下面我们看一个案例。这个商城从业务上来说拆分为这么几个大块,每个部分都是商城这个大领域当中的子域:结算、会员、访问控制、交易库存、财务。其中交易是整个系统存在的核心价值,如果没有交易,那么其他任何的业务其实都是没有意义的。所以说交易是核心的,叫做核心子域。而库存是为交易做支持的,所以它是一个支持域。而会员其实不论是哪一个部分都会用到,因此说这个子域也有一个专有的名词叫做通用子域。访问控制又是另外一种类似的子域,叫做协作子域,财务同样也是一个支持子域,这里面实际上已经进行了架构的切割。架构设计的时候我们要尽量避免模块与模块之间或者系统与系统之间产生依赖的回路。



当把这些子域开始切开以后,接下去我们开始细化。我们来看交易子域。作为买家可以干这些事,卖家可以干这些事,了解流程的时候,我会想到他有哪些概念存在。


当把这些子域开始切开以后,接下去我们开始细化。我们来看交易子域。作为买家可以干这些事,卖家可以干这些事,了解流程的时候,我会想到他有哪些概念存在。比如说,买家、卖家肯定有的。你可能会问买家和卖家不应该在会员模块里面定义和管理?这就是一个上下文划分的问题,在交易这个上下文才会存在买家和卖家,如果离开交易哪来买家和卖家。所以买家和卖家是在交易上下文当中才存在的,因此领域的概念是在上下文当中存在才有意义的。所以首先要把上下文明确出来。买家需要做一些跟商品有关的操作,商品要做上架等。这里面还有货物、SKU其实不在交易上下文当中,透明的部分统统都会被隔离到其他的上下文当中,比如说货物是库存的上下文。而出帐单、入帐单是在财务那边,当你把上下文切开来以后,会发现这样的分析结果会使得我们可以很单纯的处理跟这个上下文有关的东西。商品包含很多的SKU,又分很多类型,这些都是值对象,因为它的唯一性通过内容就可以来表示,不需要通过ID。订单又有订单条目以及订单的当前状态以及历史状态的追踪,还有订单的评价,买家卖家的评价。这些东西都是因为订单的存在而存在。



在分析的过程当中我们就会想聚合根是什么?红色圈起来的部分表示他们是一个聚合,因聚合根的生死而生死,我一旦要对聚合进行操作的时候,我会考虑把整个聚合所有的东西一次性载入到内存里,一致性的逻辑都封装在里面,健壮性就有保证。


通过这种方式,每个聚合都相对独立了,于是,我们可以以聚合为单位进行工作分解,大家可以很独立地进行开发——这完全不同于过去那种按功能模块进行分割的开发模式,因为不同功能内部可能会共享一个聚合,那种分工方式必然会把同一个聚合打散,导致逻辑可能的重复和不一致,这就是为什么软件会越做越烂的原因之一,它有一个声名狼藉的名称,叫做烟囱式任务分解!就因为早期懒得做聚合的分析设计,以为很省事,可以“快速开发”,结果,欠下了大量技术的债务——出来混,你迟早要还的,而且还是高利贷,记住!


那么聚合与聚合根之间是什么关系?聚合内的成员只可以引用外部聚合根,而且还仅仅是通过ID引用,并间接地从聚合根获取它内部数据的副本,或者把自己的数据副本传递给他,比如说sku是商品聚合里面的成员,订单项会用到sku,但不会去跟sku直接产生关系,只会通过商品来获取sku的拷贝。这使得说聚合根与聚合根之间是绝缘的,它们内部的一致性完全是被聚合根给封装起来了。



大家看,这里面每个目录是不是就对应到我们在前面这张图当中的业务模型。实际上可以理解成代码模块就是业务对象,代码的结构跟业务的结构竟然是一样的。因为你不需要进行转化,二者是同构的,使得你看代码结构就能直观地看懂了业务结构,多爽!由于他们之间没有转化,业务怎么变,内部代码就怎么变,这种应对业务变更的成本是最小的,我们的系统的可维护性就很强。



那么怎么实现富血模型?在Spring当中有一种注解,叫做“Configurable”,具体用法大家可以去查文档,这使得我们手工new一个对象的时候,它也会自动成为容器管理的对象,被依赖注入需要用到的其它对象,于是,它就可以拥有服务对象一样的能力,例如注入了仓储接口,就可以通过其它聚合根id存取其它聚合根的数据。



再来看服务层的时候,大家有没有觉得这个代码非常的简单?很简单。我们把这么多的细节代码都已经拆解到那些细粒度领域对象里面去,所以高层的代码就很简洁。



在仓储可以直接扮演工厂的角色,进行对象的装配;



我们还用到了Unit Of Work模式,在对领域模型进行集合操作时,我们可以把需要更新或删除的对象状态标志好,提交给仓储,仓储就会根据状态决定更新还是删除,这就使得领域层不用关心存储的细节。



再来看缓存:仓储的缓存必须要放到DAO里面做,因为仓储操作的是领域对象,而领域对象如果被缓存,是无法恢复依赖注入的服务对象的,所以说缓存必须要往下沉淀,放到DAO里面对PO进行操作。



这是关于DDD的其他的主题,实际上我们今天讲到的东西还只是领域对象整个开发的一个简单介绍。其实最有价值的四色原型分析法,有了四色原型分析法我们会提取到一些很有价值的东西,这些很有价值的东西又把它沉淀下来,成为分析模式。一个银行系统之所以在多个银行可以被公用,因为有很多通用的分析模式。还有柔性设计技巧,还有如何精炼模型,识别核心模型以及通用子另,还有DCI架构。


今天我跟大家分享的东西基本上到这里,最后请允许我介绍一下姚明众创。如果大家有兴趣的话,我非常欢迎大家到我们那边一起去探讨。也可以加入我们,我们的环境非常不错哦



我们现在非常需要这些人,大家如果对领域系统开发感兴趣,想在这方面获得成长机会的话,我非常欢迎大家以后跟我们对接。谢谢大家!


更多招聘信息请戳:http://www.yaominginfo.com/job.html?id=l5


快扫姚小明进行咨询



Question & Answer



现场提问



Q

王老师您好,我们原来在系统里面也用过领域系统,但是遇到的问题是它在落地的时候,特别是贫血模型和富血模型的时候,因为我们做Java开发养成了一习惯,就是通过自动代码分析程序。我们有好多理念,但是实现的时候遇到很多问题,有没有好的基础方向来支持

A

我个人认为CRED的代码自动生成的框架跟DDD的理念是背道而驰的,因为这种方向是基于应用的高层就是用户接口这一层,所有的操作就是一个CRED。事实上我们发现越是复杂的用户越是不可能是这样的。CRED是关于数据存储的操作,事实上大家都非常清楚说在一个系统当中你最复杂绝对不是关于数据,而是行为。我们做系统大家都清楚,最复杂的是行为,请问什么行为可以通过代码来实现。而DDD解决的问题不是关于数据如何存储,而是更好的封装我们的行为,封装我们的一致性。那是不是CRED就完全没有价值呢?其实也不是,有两个地方可以用到,第一如果你这个系统非常的简单,已经存化到只要CRED,就可以使用。还有一点,我可不可以把CRED工具用到和业务逻辑无关的部分。因为我们底下同样还是在操作,这部分操作完全跟那些业务逻辑没有关系,所以我们把这部分的代码生成隔离到这底下去。



   王立回答现场攻城狮的提问


Q

关于贫血的模型和富血的模型我听的不是很懂,因为像我们贫血模型的编程的话一般都是有一个属性加上get和set的方法,但是依然可以对业务相关的代码进行封装,所以跟富血模型有什么区别?

A

最关键的区别是你怎么看待封装。封装指的是将一致性有关的算法与数据绑定的,并且被封装在一个对象,这样对它的保护才会很好。假设我这个数据今天传给A做一次操作,明天再传给B操作,这样的一种碎片化的代码是散播在系统的各个方面。我们的数据在碎片化代码当中传播的话,它的一致性没有办法捆绑起来的。所以说数据和操作如果能够被聚合根打包才是一种最安全的方式。



Q

比如说一个订单兼一个具体的界面,这应该算查询吗?

A

订单的界面有什么操作吗?


Q

可能就是察看或者是修改一下地址之类的?

A

页面刷新的过程当中,一般会有发出命令的请求和一个返回数据的过程。这个发出命令请求有可能更改数据吗?没有就不用走聚合根,有就走聚合根。那返回的数据自然没有什么副作用,就走没有副作用的那条流程就可以。

Q

就是说基本上写入都是以聚合根的形式是吗?

A




Q

那这会对性能方面造成消耗吗?

A

我们DDD开发对于缓存是有一些强制性的要求,因为这些对象我们不希望每一次在操作的时候再去读一次数据库,只要说对象在缓存里面的话,性能损耗是很低。但是对内存的要求比较高。



由于时间关系,有的小伙伴还没来得及提问,但是在会后第一时间“包围”了王立继续探讨交流



部分攻城狮们合影留念

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值