今天终于在一个大型项目中运用了DDD

13-01-16 gameboyLV
先说说之前几次DDD项目失败的案例,其实也不能算是失败,只是没有领会DDD的思想。

之前的DDD是建立在数据层之上的,首先是每张数据表对应一个数据实体,每个数据实体由泛型的DAO管理,DAO又被数据上下文继承以实现事务,这就构成了数据层,业务代码是写在DataContext中。

数据层:DataEntity <- DataMapper<DataEntity> <- DataContext

在这样的数据层之上构建了“貌似”DDD的领域层,首先由领域对象继承数据上下文以实现序列化和脏读校验等功能,聚合根又继承领域对象以实现级联更新、延迟加载和管理领域对象之间的关联,最后由实现了LINQ查询的仓储对象管理聚合根,这就是当时的领域层。

领域层:DataContext <- Entity <-RootEntity <- Repository

这套架构陆陆续续用了1年左右,在运用的过程中发现DDD除了增加项目的复杂程度之外,没有带来任何好处,这是为什么呢?

直到前段时间我才发现,构建在数据层之上的领域层完全就是一个错误,领域层应当处于整个项目中的最底层,而且完全不用继承IEntity,IRepository之类的接口,正是这些接口绑架了领域模型。

==================分割线==================

经过重新设计的领域层今天终于完工了,处于最底层的仍旧是数据实体,原来的泛型DAO和DataContext全部都移动到了横切层,以类似于切面的方式向领域层提供服务。

数据实体之上是值对象,值对象是完全按照领域需求构建的。数据实体和值对象之间还有一层薄薄的数据转换服务,用于将面向数据库的数据实体转换为面向领域的值对象。

                    

数据层:DataEntity <-> Data Transformation Services <-> ValueObject

数据层之上是领域层,一个领域对象内持有若干个值对象,领域对象负载维护自生的状态,和领域对象处于同一层次的是领域服务,领域服务更倾向与处理业务逻辑,在两者之上有仓储对象,仅负责查询。

领域层:Entity / Service <- Repository

构建在数据层和领域层之外的是横切层,横切层提供轻量级的数据服务,例如事务管理、对象缓存,单例模式的DataMapper。这些服务都是以using的方式提供,即:只能在小范围的函数块内使用,使用过后自动销毁。

横切层:EventManager / CacheManager / DataManager 等。

==================分割线==================

现在的领域层已经处于整个项目的最底层了,ValueObject完全是按照领域的对象的需求构建的,使用起来得心应手。

今天在一个大项目中实践后发现,完全可以不参考UI设计,不参考原型设计,只按照需求说明书设计领域结构,设计完成之后的领域模型居然很神奇的符合UI和原型的需求。

这样的开发流程似乎也很符合TDD的理念,接口先行、之后是测试、再后来是功能、最后才是UI。

20
lswweb
2013-01-17 09:20
最好上个图,比较清晰明了一些。

gameboyLV
2013-01-17 10:16


原来的数据层现在分散到了3个部分:

1、删除重量级的ORM功能,例如级联更新,延迟加载、连表查询等

2、基本的单表持久化移动到横切层,注意最下面一条虚线,现在数据持久化是通过数据实体发消息给数据存储实现的。

3、缓存管理、事务管理移动到横切层,现在直接缓存领域对象,而不是缓存数据实体

也就是说,现在已经没有传统意义上的数据层了,数据存储都是通过消息机制将更改DataEntity的操作传递到持久化服务,持久化服务只有非常简单的CUD功能,可以连接到Sql,NoSql甚至XML文件。

[该贴被gameboyLV于2013-01-17 10:25修改过]

lshoo
2013-01-17 20:18
学习........

flyzb
2013-01-17 21:58
感觉图有些问题,领域服务应该是领域模型对上层(应用层)的调用接口,仓储层应该是领域模型与数据库的交互接口,所以刚好画反了。

gameboyLV
2013-01-18 09:31
这正是这张图想表达的内容,仓储层并不需要和数据库进行交互。

我们习惯性的思维,仓储需要接受一个请求,然后查询数据库获得数据实体,再将数据实体转换为领域对象。人总是有惰性的,有时候为了省事,数据实体就直接参与业务逻辑、甚至是返回给UI了。

在这张图上,领域层、应用层、UI层都是访问不到数据实体的,对领域对象进行的任意操作都会经由数据转换服务(防腐层)转换为若干条DataEntity.Change消息,数据存储(持久化服务)复杂侦听这些消息,并连接数据库。

也就是说领域层只需要修改数据,而不需要关心持久化的问题。

clonalman
2013-01-18 12:26
2013-01-16 20:25 "@gameboyLV"的内容
直到前段时间我才发现,构建在数据层之上的领域层完全就是一个错误,领域层应当处于整个项目中的最底层,而且完全不用继承IEntity,IRepository之类的接口,正是这些接口绑架了领域模型。 ...

恭喜恭喜啊,这种机会不多的。

别忘了把架构信息也踢处领域,那就彻底解放了。

领域不一定是最低层,但其它各层都是围绕着它转的

flyzb
2013-01-19 09:36
2013-01-18 12:26 "@clonalman"的内容
直到前段时间我才发现,构建在数据层之上的领域层完全就是一个错误,领域层应当处于整个项目中的最底层,而且完全不用继承IEntity,IRepository之类的接口,正是这些接口绑架了领域模型 ...

    我完全反对这样的做法。因为领域模型主要是一个逻辑模型的体现,而仓储层本质上是帮助领域模型屏蔽了数据库。

  如果你认为这样做才是真正解放了领域模型,那么请问当数据库从关系型变成nosql时,你的领域模型是否还能保持不变?

gameboyLV
2013-01-19 11:28
当然可以,在我的值对象和数据实体之间有一层数据转换服务(防腐层),当数据实体变成bson,datatable,或二进制文件时,只需要在防腐层中添加对应的转换器即可,领域模型可保持不变。

甚至可以一套领域模型同时使用NOSQL和SQL两种存储机制,当防腐层检测到SQL持久化服务当机时自动切换到NOSQL的持久化服务。

flyzb
2013-01-19 14:36
从图上看,个人总体感觉"文不对题",有些混乱。

既然是“仓储”,那就是与数据库打交道的,这个定义是很清晰的,但"仓储“在本图中的位置竟然在领域对象之上,完全不符合DDD的定义。

既然是”领域服务“,那就是领域层对外的被调用接口,也就是领域的逻辑外观,但'领域服务"在本图中是被“仓储”调用而与领域对象无关。

banq
2013-01-19 16:35
非常不错,恭贺一下,创新精神可嘉。具体细节需要详细了解和讨论。

gameboyLV
2013-01-19 16:56
2013-01-19 14:36 "@flyzb"的内容
既然是”领域服务“,那就是领域层对外的被调用接口,也就是领域的逻辑外观,但'领域服务"在本图中是被“仓储”调用而与领域对象无关。 ...

Eric Evans的《领域驱动设计》一书中68页有这么一段对领域服务的介绍:

“一些领域概念不适合被建模为对象,如果勉强地把这些重要的领域功能划分为Entity或Value Object的职责,那么不是歪曲了基于模型的对象的定义,就是人为地增加了一些无意义的对象。”

很明显领域服务并不是领域的边界,而是与领域对象处于同一层次。场景上下文才是领域的边界。同样是这本书的236页有这么一段:

“明确地定义模型所应用的上下文。根据团队的组织、软件系统的各部分的用法以及物理表现来设置模型的边界。在这些边界中严格保持模型的一致性,而不要受到边界之外问题的干扰和混淆。Bounded Context明确地限定了模型的应用范围。”

[该贴被gameboyLV于2013-01-19 16:58修改过]

flyzb
2013-01-19 22:03
DDD中的”上下文”实际上指的领域模型的一致性和完整性,而不是指功能边界,从14章的图14-3和图14-4的那个例子更可以明确地看出来。

另外,你引用的那段”领域服务“的话只是说明领域服务的逻辑与领域对象和值对象的不同。你应该好好领会一下”服务“的基本含义:

服务是指为他人做事,并使他人从中受益的一种有偿或无偿的活动。不以实物形式而以提供劳动的形式满足他人某种特殊需要。

对于”领域服务“,就是领域模型为应用层做事的。

[该贴被flyzb于2013-01-19 22:11修改过]

gameboyLV
2013-01-20 09:53
2013-01-19 22:03 "@flyzb"的内容
对于”领域服务“,就是领域模型为应用层做事的。 ...

应该是为领域层做事的才对,14-3那个图预订Context是实线,实线表示某种物理存在的事务,预订Context应该就是一套与其他Context进行通讯的API。

14-3这里例子似乎有歧义,你说RoutingService对外服务也许,对内服务也行,但是。。。另外一个例子就没有歧义了:

Jimmy Nilsson《领域驱动设计与模式实战》182页

//Order
public void Accept()
{
  if(_status != OrderStatus.NewOrder)
    throw new ApplicationException("You can only call Accept() for NewOrder orders");
    
    _status = OrderStatus.Accepted;
    _orderNumber = _orderNumberService.GetNextOrderNumber();
}
<p>

这段代码正好与Eric Evans的观点相互印证:

“一些领域概念不适合被建模为对象,如果勉强地把这些重要的领域功能划分为Entity或Value Object的职责,那么不是歪曲了基于模型的对象的定义,就是人为地增加了一些无意义的对象。”

因为OrderNumberService实际上是一个锁机制,所以不适合放在Order对象中,我就不信OrderNumberService还能为应用层做事?脱离了Order的OrderNumberService没有任何意义。

banq
2013-01-20 11:20
我认为两位对服务的看法都有道理,Eric Evans还认为服务是一个无副作用的函数,输入参数和输出结果能够确定,符合契约设计DBC:Design by Contact,因此易于测试,采取断言方式进行测试,而测试的输入参数和输出结果正好是DDD设计的两个领域模型。

楼主这个系统其实有两个服务,一个显式画出来,服务于实体模型;还有一种是隐式的,也就是API接口,实际可能就是一种服务,或者场景A出也可能是@flyzb 认为的服务。

以上只是个人见解。

猜你喜欢
2Go 1 2 下一页