张逸说

出口成张,逸派胡言

0%

领域驱动设计基本概念答疑

本文内容来自我的知识星球「逸派胡言」对群友提出的有关领域驱动设计基本概念的回答。

实体与值对象

问题:DDD实现中领域对象区分实体(Entity)和值对象(Value Object)的目的(Why)是什么?或者换一种问法:领域对象区分实体(Entity)和值对象(Value Object)之后,带来的好处和收益是什么?

回答:从DDD的概念上讲,实体(Entity)与值对象(Value Object)的本质区别仅在于后者无需identity(唯一标识)。这其实就是带来的价值——就是你设计的对象不需要去跟踪和管理这个唯一标识。

这是概念划分上,值对象带来的价值。

再来说设计层面。通常情况下,我们建议将值对象设计成一个不变(Immutable)对象。当一个对象是不变的时,你就基本不需要担心并发带来的诸如同步、冲突等问题了,这既降低了编程的难度,又可以无需引入额外的同步锁影响程序的性能。

反而过来说,之所以可以将值对象设计成不变的,其根本原因还是在于我们无需跟踪和管理唯一标识。

在领域驱动设计中,我们提倡的实践是尽量定义值对象来替代基本类型,原因在于基本类型无法体现统一语言中的领域概念。此外,在多数语言中,我们无法对基本类型做封装,就意味着一个领域概念缺乏领域行为来支持。假设一个实体定义了许多属性,如果这些属性都是基本类型,就会导致与这些属性相关的领域行为都要放到实体中,导致实体的职责变得不够单一。

引入值对象后,情况就不同了,因为我们可以利用合理的职责分配,将这些职责(领域行为)按照内聚性分配到各个值对象中,这个领域模型就能变得协作良好。

当然,反过来说,之所以可以这样设计,还是在于值对象无需承担跟踪和管理唯一标识的职责。

这也是为何Eric要将实体和值对象分开的主要原因,也是值对象给我们带来的价值所在。

Repository与DAO

问题:Repository与DAO其实都是两种模式的名称。然而在领域驱动设计中,名称本身就是非常重要的。Dao即Data Access Object,即数据访问对象。从其命名上看,就应该属于数据访问层,即DDD中的基础设施层。

回答:在DDD中,所有的领域对象应该都属于领域层。那么,该如何访问这些领域对象呢?DDD希望解除领域层与基础设施层之间的关系,即将设计的注意力完全放在领域建模和领域设计上,思考领域逻辑的实现时,应尽可能地不要考虑领域对象的持久化(数据访问),于是就定义了Repository这个抽象。无论放在哪里(文件、DB或者内存),Repository都将其视为一个“资源库”的抽象。经过这么一层的抽象之后,获取领域对象,或者说管理领域对象生命周期的逻辑就应该属于领域层。

在实现上,你当然可以将这样的Repository接口命名为DAO,这本身没有问题,但名不正则言不顺,如果在领域层中夹杂了一个名为DAO的接口,仍然有“将基础设施混入领域层”的嫌疑。

所以,Repository是抽象,代表了对领域对象生命周期的管理,但并不等于是持久化,持久化只是Repository的其中一种实现。你可以假设一台服务器无比的强大,内存大且永远不会宕机,这时何须持久化呢?但无论怎么修改生命周期的具体管理方式,都不会影响到Repository的抽象。

领域服务与应用服务

问题:应用服务与领域服务的区别在哪?

回答:从分层架构上,应用服务属于应用层,领域服务属于领域层。

从职责上看,应用服务只是一个门面(Facade),它具体并不做领域服务的活儿,也就是不提供领域实现,也就是不包含业务逻辑。之所以要引入应用服务,有两个原因:

领域服务或其他领域对象的粒度太细(便于协作、扩展和重用),不利于客户端的调用,基于“最小知识原则”,还是让客户端少知道这些领域对象协作的知识为好。此时的应用服务更像是对领域对象的一种“编排”。

在调用领域对象去完成一个用例时,不可避免地要牵涉到一些属于“横切关注点”的内容,如事务、异常处理、授权认证等。这些横切关注点从职责上看,不属于领域层,放在领域服务中可能会导致对领域逻辑的污染,这些职责就像砌砖墙时需要的水泥。水泥自身不提供砖头的职责,但没有水泥,墙就没法砌起来。

具体对领域服务和应用服务的阐述,可以看我的这篇文章《一篇文章教你分辨应用服务和领域服务》。

请关注我的「知识星球」: