使用依赖注入实现聚合根之间调用的逻辑悖论

DDD中如果有两个聚合根调用,如何解决?如果还是使用依赖注入,就会发生聚合根嵌套的可笑事情发生。

以代码为例子:有两个聚合根类AggregateRoot1 AggregateRoot2,AggregateRoot1的方法依赖AggregateRoot2实现,如果采取依赖注入,也就是Spring等框架做法如下:


class AggregateRoot1{
AggregateRoot2 aggregateRoot2;

public void dosth(){
....
aggregateRoot2.dosth();
...
}
}

AggregateRoot1的dosth方法依赖AggregateRoot2的dosth方法,不自觉就将AggregateRoot2使用依赖注入,但是这样就造成了AggregateRoot1聚合AggregateRoot2的现象,AggregateRoot2还是聚合根吗?

什么是聚合?


class A{
private B b;
public B getB(){
return b;
}
}

这段代码表达了A聚合了B,也就是说整体A包含了部分B,A和B关系是父与子,整体与部分的关系。

我们使用这种聚合关系表达A是B的父对象,对于DDD聚合,如果A不再被其他对象引用,类似二叉树的一个根节点,那么我们认为A就是聚合根。

但是,因为依赖注入,我们破坏了业务上的聚合根概念。

那么两个聚合根之间相互调用怎么办?
只有通过事件。

[该贴被banq于2013-04-12 17:18修改过]

呵呵!spring遇到DDD搞笑!两个思维模式,spring认为ioc能解决很多问题似的。
小秀一下寡人的框架JSDM 写aggre方式。


// 定义一个Aggre
function wrap(repos,services,publish){
function A(){}
A.prototype.fun = function(){
var repo = repos.B; //得到B的repository
repo.get("id001",function(err,b){
// 得到B的对象
......
publish(发布事件)
})
}
return A;
}


[该贴被brighthas于2013-04-12 19:08修改过]


class Order {
Customer customer;
}

CRM、SupplyChain中Order、Customer 可都是聚合根

2013-04-12 17:16 "@banq
"的内容
那么两个聚合根之间相互调用怎么办?
只有通过事件。 ...

正是基于聚合根之间只能使用异步事件或消息,可能导致数据不一致性,Evans才发表文章:在聚合根实现CAP定理 Acknowledging CAP at the Root -‐-‐ in the Domain Model

认为聚合根之间遵循CAP定律,实现最终一致性即可。以前的讨论贴:聚合与一致性和有界上下文

聚合根存在于有界上下文,在上下文场景诞生并进行测试,如果两个聚合根通过直接引用,如上面roo1和roo2,则可能破坏它们各自所在上下文场景。

[该贴被banq于2013-04-13 02:13修改过]

1.聚合根之间通过ID关联即可,一个聚合根的任何一个操作不应该依赖于其他聚合根的信息或操作;
2.聚合根之间只能是最终一致性;
3.聚合根之间异步通信用event-driven architecture来实现;
4.saga,或ProcessManager就是用来实现这种异步通信从而实现最终一致性的一种技术;
5.采用event sourcing+in memory的架构模式让这一切更完美;

发现jdon上目前讨论saga或processmanager的帖子很少,但实际上要实现最终一致性,它们不得不提。

这段话描述了我们应该ID关联,而不是对象引用:
Also when one Aggregate needs to reference another Aggregate—and this will almost always be true—you should point to the associated Aggregates by identity only, not by object reference. By using identity you will allow each referenced Aggregate to reside anywhere in the distributed infrastructure as best determined by the low-level partitioning software.

2013-04-13 22:31 "@tangxuehua
"的内容
Also when one Aggregate needs to reference another Aggregate—and this will almost always be true—you should point to the ...

这个的前提是做分布式系统,两个聚合根被隔离了,当然只能用ID,这个根聚合根直接引用并没矛盾!!!

2013-04-16 16:19 "@clonalman
"的内容
这个的前提是做分布式系统,两个聚合根被隔离了,当然只能用ID,这个根聚合根直接引用并没矛盾 ...

ID关联我是第一次听说,英文的identity 不一定是ID,而可能是一种表示标识的对象,如果直接用比如整数型ID,这个ID实际是一个值(值对象)。

在我们对象世界中,万物皆对象,如果一个字段想混入对象界,至少它带个值对象的头衔吧?

假设类似数据表外键这种ID可以使用,它和聚合根对象直接引用应该不矛盾。这两种在技术上尽管行得通,却违背领域语言,那么可能是一种crack黑客技术。

为什么我认为违背领域语言呢?聚合是显式,其背景隐式是bounded context,如同鱼和水的关系,两个聚合相当两个鱼缸,如果使用直接引用,相当于把两条鱼放在一个鱼缸中,水也合并在一起,bounded context混在一起就不是有界bounded了。

Evans还继续在这个PPT中论述,聚合是必须在bounded context中进行断言测试的,如果两个聚合直接引用,如何划定它们的测试用例呢?这是一种紧耦合带来的致命问题。

退一步讲,一个聚合包含另外一个聚合的ID值,那么这个聚合如果想调用另外一个聚合中的方法,还需要通过ID值将那个聚合代码加载到内存中,才能进行方法调用啊。注意方法调用才是关键,不能因为方法调用而将两个类耦合在一起。

http://www.jdon.com/45264
[该贴被banq于2013-04-16 17:06修改过]

2013-04-16 16:19 "@clonalman
"的内容
这个的前提是做分布式系统,两个聚合根被隔离了,当然只能用ID,这个根聚合根直接引用并没矛盾!!! ...

我只能说你没理解为什么推荐使用ID关联。
当然,我说的ID就像banq所说,是一种值对象,不一定是一个原子类型如int, guid.
你先看一下这篇文章再来回复吧:
http://dddcommunity.org/wp-content/uploads/files/pdf_articles/Vernon_2011_2.pdf

2013-04-16 23:03 "@tangxuehua
"的内容
我说的ID就像banq所说,是一种值对象 ...

很好,我总结这篇英文和事件的方法调用如下,假设BacklogItem和Product分别是两个不同bounded context下的聚合根,BacklogItem中实现Product的值对象,而不是直接引用聚合根:

从仓储Repository获得一个ProductVO :


public class ProductVO {
private long productId;
private long name;
private String description;
}

public class BacklogItem{
private ProductVO productVO;
void dosth(){
//向Product聚合根发事件消息实现调用
domainevents.send(new
ProductUpdatedEvent(productVO.getProductId));
}
}

延伸开来,经常有人提出一个误导问题:Java传递的值还是引用?
这个问题其实是应该在分析设计领域来回答,分析设计时,该使用值还是使用引用?除了值和引用之外难道没有另外一种实现关系的办法吗?No, 使用消息事件。作为底层语言都应该提供这三种方式实现。

这个问题实际应该属于Context maping上下文映射,通过服务这样一个无副作用的接口可以是实现不同上下文之间的翻译转换,事件和服务都属于一种契约合同DBC,事件同样也是和在不同上下文之间传递。


[该贴被banq于2013-04-17 10:00修改过]

这种问题最好避免,应当重构!重构思路如下:

0、一个bounded context之内,只能有一个root aggregate! 一山不容二虎!
1、真正的问题可能是两个bounded context之间如何协作? 一般是异步messaging的方式。

出现这种问题的诱因可能是:不小心把uses-a的关系,表达为has-a的关系。

UML中的dependency和association可认为是uses-a的关系。(uses-a的关系强度:association > dependency)
aggregation和composition可认为是has-a的关系。(has-a的关系强度:composition > aggregation)

如果是has-a的关系,用依赖注入的方式无可厚非,挺适合的,与现实贴合。(值的依赖以及延迟绑定)
如果是uses-a的关系,通过异步消息,进行松耦合比较好,与现实更贴合。(行为逻辑的依赖以及延迟绑定)

但是has-a和uses-a中的“a”在编译期就可以确定的,而不是运行时确定,就不必劳烦IoC和Messaging了。
(个人观点,因为代码看起来可能更直白和紧凑些,易于理解)

关于bounded context(业务场景)之间的关系的设计思路:
1)彼此之间,尽量独立,老死不相往来;
2)动态行为逻辑的依赖,使用异步messaging的方式;
3)动态值或类型的依赖,使用IoC的方式;(比如根据运行时的上下文,为业务场景提供不同的数据来源)
4)编译期确定行为或值的,可使用直接作为场景内部的一个方法或字段。

2013-04-17 19:16 "@jdon007
"的内容
如果是has-a的关系,用依赖注入的方式无可厚非,挺适合的,与现实贴合。(值的依赖以及延迟绑定)
如果是uses-a的关系,通过异步消息,进行松耦合比较好,与现实更贴合 ...

写得很好,如果结合DDD领域模型,has-a的聚合和组合主要是在聚合根中使用;而uses-a主要是在服务Service中使用。

问题来了,大量使用依赖注入IOC却是在Service中,比如一个Service代码需要依赖DAO,代码如下:


public class AServiceIml implements AService{

@Inject //将aDao实例注入
private ADao aDao;

public void save(A a){
aDao.save(a);
}

}

如此一个原则性错误,误用依赖好像很多年,老外业界也没有什么质疑,难道是我们错了?

1、数据源的读写
Dao, DaoImpl, Service, ServiceImpl,这种写法,时常是对interface的滥用,对IoC的滥用,大量接口的出现,并不意味着“针对接口编程”,甚至可能把本身应该couple的业务概念decouple掉,或反之。

不过Service和Dao的关系,我觉得应该是has-a的关系,Dao可以理解为Service内部的记忆机制。

已经不怎么用Dao了,更倾向于在需要存储的方法内部直接使用类似dbutils、spring jdbctemplate或自己写的对jdbc轻量级封装的库,易于书写SQL语句,直接读写数据源。

为什么使用同步或直接的方式呢?因为没有数据,业务场景一般是进行不下去的,一些纯写且不需要实时返回结果的场景,可以考虑用异步的方式,使用jdk并发包的Future、ExcutorService等类封装jdbc,比较容易实现。

2、服务之间、聚合根之间
如果S1与S2存在uses-a关系,两者之间的协作一般是用messaging的方式,这个我们已可以达成共识。

那么,S1中的老大聚合根A1, S2中的老大聚合根A2,它们之间怎么协作呢?服务已经界定了聚合根的权利边界了,它们之间的协作,也得通过服务之间的协作方式,即异步messaging的方式进行。

一直有个疑问, Aggregate是一些类似于pojo/poco的对象集合, 无继承和多态可言, 为什么要用DI?

是否将"uses-a"分为”notifies-a"(即发送通知事件不关心结果,通常为异步)和"relies on a"(即同步调用强依赖返回结果)更有意义。

当我们发现聚合根A需要大量地"relies-on-a"聚合根B时,是否可以认为需要重新考虑之前的bounded-context的划分是否合理--因为现在“两个水缸之间的鱼已经强烈需要在一条水缸里了”,两个聚合根之间已经形成了比较强的耦合关系。

@banq,你之前所发的


public class BacklogItem{
private ProductVO productVO;
void dosth(){
//向Product聚合根发事件消息实现调用
domainevents.send(new
ProductUpdatedEvent(productVO.getProductId));
}
}

中的 ProductUpdatedEvent(productVO.getProductId))
是否应该是BacklogDoSthEvent更为合理,由product来消费这条消息。
因为如果是”ProductUpdatedEvent",相当于backlogtiem需要明确知晓”我做了这件事以后需要更新product",那么逻辑上其实还是形成了对product的依赖。backlogitem是否应该只产出与自身相关的事件。

因此是否可认为合理场景下,聚合根之间的messaging大部分只会应用于异步通知的场景(即消息产生者通常并不关心消息消费者的处理结果)。

聚合根之间大量使用messaging进行同步交互的方式虽然可以实现bounded-context之间的隔离,但确实违背许多人传统的编程习惯。
DDD新手,目前还处于有些迷茫的状态,望各位大师指点。

[该贴被njzhxf于2013-04-21 10:42修改过]
[该贴被njzhxf于2013-04-21 11:00修改过]