讨论:这样基于Domain Event的分层是否合理?

最近在对之前做过的一个项目进行二期修改。鉴于之前典型的贫血结构,以及Controller--->Service--->DAO模式让代码压力都集中在service层的情况。在参考了Banq写的几篇对象职责和Domain Event的文章后,我也试着捣鼓了一下新的分层模式。贴出来和大家讨论,欢迎拍砖!

【1】层次划分:

①控制层:数据映射、控制转向、业务调用
②业务层:从用户角度出发,看到的系统可以提供的功能接口
③实体层:包含了数据与行为的实体对象
④服务层:从程序内部角度出发,为了完成业务而划分出来的细粒度功能模块
⑤仓储层:对象的构建、缓存、持久化

上面我的说法可能不是很规范,因为DDD也没有仔细的研究,可能大家会对业务和服务层的直至有所疑惑:这里我的想法就是一个从用户角度出发的业务操作,对应到程序内部可能会被划分成多个细粒度的程序操作。

【2】协作关系:

①控制层与业务层:
※控制层提供业务层所需的原始,未经封装的数据
※控制层提供业务调用
※业务层返回给控制层业务出来结果,由控制层决定转向

②业务层与实体层:
※业务层在必要时(new,edit,delete等一系列命令操作),从仓储中加载对象
※业务层向实体层对象发出事件通知
※业务层接收实体的行为反馈

③实体与服务层:
※实体通过“服务注册”的方式,让实体具有“自我数据操纵”的能力
※实体接受到业务层的事件通知后,广播给注册的服务提供者
※服务层为需要提供服务的实体提供相应的操作功能
※实体层中包含了实体逻辑(可以自己处理而不需要依赖其他模块、层次)
※服务层中包含了服务逻辑(无法通过一个对象自身完成,涉及到其它对象)

④业务与服务层:
※当业务要求是查询要求,或者与特点对象无关时,业务层直接请求服务层
※服务层可以看成是对业务层请求的内部实现

⑤服务层与仓储层:
※仓储层的对象实体可以是:新建,缓存,从持久化介质中加载
※仓储层中包含了构建对象的Builder,否则构建和校验
※仓储层中包含了对象的缓存和缓存操作
※仓储层中包含了对持久层的访问

⑥实体与仓储层:
※仓储层构建的最终对象就是实体,仓储是实体的来源,也是实体最终的去向

下面分为两只情况来阐述协作流程:

【3】增删改请求的协作流程

控制层捕获请求,并决定由那个业务层对象处理
|---> 业务层需要构建/或者单个实体
|---> 从仓储中新建(insert)或者从缓存中加载(edit,delete),并返回实体,注册服务
|---> 向实体对象发出一个事件通知(save, update, delete)
|---> 实体对象遍历自身已注册的服务,广播该事件消息
|---> 服务层处理该事件,调用仓储层
|---> 仓储层执行持久化操作,并返回结果
|---> 接收事件处理结果,并返回给控制层
|---> 根据业务层结果,决定转向

【4】查询请求的协作流程

控制层捕获请求,并决定由那个业务层对象处理
|---> 业务层直接请求服务层(因为此时请求和单个实体无关)
|---> 服务层处理请求逻辑,并调用仓储层
|---> 仓储层可以从缓存中取,或者从持久化介质中取,并返回
|---> 接收查询结果,并返回给控制层
|---> 根据业务层结果,决定转向

【5】疑惑与担忧
①这种分层是否合理?因为我想让对象通过事件来消除和服务层的耦合?
②这种把命令、查询分开来对待的做法会不会令日后的逻辑变得分散而难以维护?
③在仓储的构建过程中,有可能需要调用服务层逻辑,会不会造成服务<--->仓储的双向依赖而耦合?

写了很多~~~也有劳大家费心看看。实在不想再回到贫血模型的日子啦
[该贴被pengpenglin于2010-03-23 16:37修改过]



不错。

2010年03月23日 16:10 "pengpenglin"的内容
向实体对象发出一个事件通知(save, update, delete)
|---> 实体对象遍历自身已注册的服务,广播该事件消息 ...

你的设计核心在这里,你是将服务注册大实体,当实体接受到事件消息时,向这些已经注册的服务广播这个消息。

这个思路应该比通常贫血模型要好多,但是还是有贫血模型之嫌疑,其实实体自己可以完成一些场景职责,实体不应该只做一个转发器或者什么事件都委托给服务去完成 。

如果说这个架构有什么缺点的话:就是没有对这个重要的细节进行控制和约束,面向过程的程序员可能还是喜欢走实体 --> 服务 -->仓储 -->数据库这个路子,尽管你增加了很多环节,但是对于他们来说,只是增加复杂性和障碍,没有彻底改变他们的编程思路。

>这种分层是否合理?因为我想让对象通过事件来消除和服务层的耦合?
通过事件消息是可以的,目前比较合理。当然,更好再结合一个场景混合器,也就是DCI架构,就是把服务层和实体一起注射到当前场景对象中,扮演场景角色完成功能。在异步架构思维:使用Akka实现领域建模中trait AccountRepository extends Actor是一个场景混合器,类似使用qi4j实现DCI架构的AssignmentsMixin。

>这种把命令、查询分开来对待的做法会不会令日后的逻辑变得分散而难以维护?
CQRS架构个人目前认为是一种妥协设计,你可以从命令和查询分开来考虑需求分类,但是具体实现还是尽量能互相借鉴重用。


>在仓储的构建过程中,有可能需要调用服务层逻辑,会不会造成服务<--->仓储的双向依赖而耦合?
要遵循上层依赖下层,不得逆向的原则,仓储是不能调用服务的,也没有理由调用服务,仓储只是对象转换或持久化操作,是一个转换层,不能封装其他业务逻辑,要做得简单,不要把业务从服务赶到仓储中去,因为这是数据库思维的程序员喜欢干的事情,他们总是有数据库恋母症,总是爱在离数据库最近的地方大写特写程序。


[该贴被banq于2010-03-23 18:00修改过]

在做一个系统的,我们主要考虑一下几个问题:

1 业务逻辑如何组织
2 业务逻辑如何封装
3 如何与持久层进行交互
4 事务并发如何控制

而这些考虑就造成了我们软件的一个层次架构,比如业务逻辑的封装层可以利用Facade,业务逻辑的组织,利用DDD,持久层通过XXX等,但是这仅仅都是一种技术上面的架构。一个真正复杂的系统,仅仅分层是不够的,关键还要靠我们把业务进行切分,只有业务首先切分了,模块和模块之间的耦合度减低了,这样把切分好的业务融入到分层的架构中,这样才能更加容易伸缩。如果仅仅分层而不对业务进行水平分割,那么系统的伸缩性还是不好的。

谢谢Banq的热心解答!

【1】首先你提到的实体不应该只做一个转发器,而可以完成一定的业务逻辑,其实我在上面的第3点中“实体与服务层”的关系有写到这个划分标准:就是看实体是否能够独立完成。(参见下面)不知这个划分标准是否合理?

③实体与服务层:
※实体通过“服务注册”的方式,让实体具有“自我数据操纵”的能力
※实体接受到业务层的事件通知后,广播给注册的服务提供者
※服务层为需要提供服务的实体提供相应的操作功能
※实体层中包含了实体逻辑(可以自己处理而不需要依赖其他模块、层次)
※服务层中包含了服务逻辑(无法通过一个对象自身完成,涉及到其它对象)

但是有一个很大的问题:业务逻辑总是会变的,而且变动频繁。如果我一开始把某个逻辑放在实体中,有一天它突然要依赖服务层了。那么会造成接口在实体和服务层中浮动,从而导致客户端调用到的地方有可能在修改后一夜间全部出错。这个有没有好的解决方法?

【2】其次你提到我的架构里面的缺点是没有对“这个重要的细节进行控制和约束”。这个我还不是很明白,能否再说明一下

【3】你提到的“从命令和查询分开来考虑需求分类,但实现时还是尽量借鉴重用”

是不是把他们独立成不同的Service,这样会造成类过多吧?而且都已经分开了如何进行重用呢?

【4】最后一个问题就是按照这种架构的话,事务的配置应该是在业务层还是服务层?感觉这时候应该是在业务层才比较合理吧。毕竟现在的服务层已经是细粒度的操作了,可能只是某项业务的一个环节而已

2010年03月25日 20:40 "pengpenglin"的内容
业务逻辑总是会变的,而且变动频繁。如果我一开始把某个逻辑放在实体中,有一天它突然要依赖服务层了。 ...

首先,你没有区分主次,实体的业务逻辑是主,服务层只是一个次要地位,服务层的意思是技术架构服务,就是为实体提供技术架构服务,而不是业务服务的,所以我们可以称之为领域服务,它区别于SOA的服务,他们和实体调用关系是: 控制层 -- > SOA服务 -->实体 -->领域服务

业务逻辑是一直在实体中,并且按照对象职责会被切分成很多接口,所需要的技术架构服务支持应该是被注射到实体中,或者通过Domain Events去驱动,这两种都是最松耦合的依赖处理设计,能够灵活应对变化,还有使用Trait或Mixin混合器。

>其次你提到我的架构里面的缺点是没有对“这个重要的细节进行控制和约束”。这个我还不是很明白,能否再说明一下

我指的控制和约束见上面问题。以Xmuzyu的这个帖子为例。要保证模型驱动组件,而不是组件来指挥模型,模型是智能机器人,一切由其指挥,而不是它被指挥。

>你提到的“从命令和查询分开来考虑需求分类,但实现时还是尽量借鉴重用”是不是把他们独立成不同的Service,这样会造成类过多吧?而且都已经分开了如何进行重用呢?

重用就是共用一个领域对象实体,查询和命令都共用内存中实体对象,这样不至于因为查询,在内存中制造大量临时对象,这也是flyweight模式。

>最后一个问题就是按照这种架构的话,事务的配置应该是在业务层还是服务层?

当然在业务层,不过要注意的是,已经不能使用传统的基于关系数据库ACID事务了,比如JTA事务或JDBC事务,只能使用并发锁或不变性等新并行计算技术来实现ACID(比如切割尽量细粒度,尽量使用不变性封装一些数据。)。总的原则是根据CAP定理来平衡。


[该贴被banq于2010-03-26 20:55修改过]

banq的讲解应该很精彩,但能否像lz那样把lZ这项目的层次重新列一下 否则光指出几个问题还不够清析。麻烦banq