在本章,我们简单解释每种元素的职责:
2.2.1.- 领域实体
实体表示领域对象,主要由对象的标识和连续性定义,并不只是由组成对象的属性定义。
实体一般和主要的业务/领域对象有一个直接的关系,例如客户,雇员,订单等等。因此,把这些实体保存到数据很正常,虽然这完全取决于每一个具体应用。虽然不是强制的,但是“连续性”这方面通常存储到密切相关的数据库。连续性意味着实体应该可以在应用的执行周期"生存“。每次重新启动应用时,应该可以在内存中重建这些实体。 为了区别实体,标识的概念就是必须的,尤其是两个实体都着相同的值/数据时。标识是应用的根本方面。应用中错误的标识可以导致数据损坏问题或程序错误。很多时候在真实的领域或应用领域模型是由它们的标识定义而不是属性定义的。一个好的实体的例子是人。实体的属性,例如住址,财务数据甚至姓名都可以改变;然而,同一个人的实体的标识将保持不变。因此,一个实体的基本概念是一个持续抽象的生命,可以变化不同的状态和情形,但总是有相同的标识。 一些对象并不主要是由它们的属性定义;这些对象表示特定生命周期的标识,经常有不同的表现形式。一个实体应该可以区分别的实体,即使当它们有相同的描述性属性时。关于领域驱动设计,根据Eric Evans的定义,”一个由它的标识定义的对象叫做实体“。实体在领域模型中是非常重要的,应该仔细的辨别和设计。在一些应用中可能是实体的在另一些应用中可能不是。例如,在一些系统中地址可能没有标识。然而在其它系统中例如电力公司,客户的地址就非常重要,因此地址必须有一个标识。这种情况下,地址应该看作一个领域实体。在其它情况,例如电子商务应用,地址可能就是一个属性。在后一种情况下,地址不是特别重要,应该作为值对象(稍后会解释)。一个实体可以有很多类型,可以是一个人,汽车,银行交易等,但重要的一点是,是不是实体取决于特定的领域模型。一个特定的对象可能在领域模型中不是一个实体。同样地,不是所有领域模型中的对象都是实体。例如,在银行交易的情景中,在同一天的两笔存入账款被视作两个不同的银行交易,所以它们有标识,一般是实体。即使, 两个实体的属性都一样,他们也应该当作不同的实体。
关于设计和实现,这些实体是离线的对象,用来在层间获取,传输实体数据。这些对象代表了真实世界的业务实体,例如产品或订单。另一方面,应用中的实体内部用来在内存中作为数据结构。此外,如果这些实体依赖特定的数据持久化技术,那么这些类就应该放在数据持久化层中。另一方面,如果我们按照面向领域设计的模式使用POCO,他们是不和任何技术相关的纯粹的类。因此,这些实体应该在领域层中,由于他们是领域实体,独立于任何基础结构技术。
PI原则(持久化透明),POCO和STE
这个概念,推荐使用POCO来实现领域实体,可能是根据面向领域架构当实现实体时最要考虑的。原则完全支持领域层的所有组件都必须是完全对基于的数据持久化层的技术透明的。 持久化透明是一个建议隐藏对象的持久化细节的设计原则。”透明“表明行为被封装,这是一个面向对象的设计宗旨。这种实现实体的方法对很多设计尤其重要。在那些设计中(例如DDD),把这些元素和数据访问技术隔离是至关重要的。换句话说,实体不从任何基类继承,不实现任何和技术相关的接口,这样的实体在.NET叫做POCO。 相反,继承特定基类或和技术相关接口的对象称为”指令性类"。选择哪个不是小事,必须被认真考虑。另一方面,使用POCO给我们对选择持久化模型很大的自由空间。另一方面,它带来了关于“透明的程度”的限制和过载,持久化引擎有这些实体和它们的关系。POCO类在开始时的实现成本较大,除非使用ORM帮助映射甚至生成代码。 自跟踪实体STE (Self Tracking Entities)的概念比较宽松。就是说,由实体定义的数据类并不完全”纯粹“,而是需要实现一个或更多的接口。在这种情况,并不完全符合持久化透明原则,重要的是这个接口需要在我们的掌控。换句话说,这个接口不能是任何外部基础结构技术的一部分。否则,我们的实体会变成”指令性类"。
无论如何,实体是贯穿整个架构或至少在应用服务器层的对象。后者的情况是当使用DTO进行远程通信的情况,这样领域模型的内部实体不会到表现层或任何超越内部层的服务。使用DTO或者实体在分发服务层分析,因为这是和多层应用和分布式开发相关的。 最后,我们必须考虑到当处理远程通信时类的序列化需求。把实体从一层传到另一层需要实体序列化;必须支持特定的序列化机制,例如XML或二进制格式。需要确认选定的实体类型支持序列化。另一个选项是,在分发服务层和应用层使用转换DTO的聚合。实体逻辑包含在实体内部实体对象自己具备一定的逻辑。例如,在银行账号实体中可以有当金钱增加或支付的业务逻辑。自运算字段的逻辑可以是另一个例子。当然,也可有有没有自身逻辑的实体,但这种情况只会发生在确实没有内部逻辑时,因为如果所有的实体都没有逻辑,那我们将陷入 Martin Fowler提出的”贫血领域模型“的反模式。参照贫血领域模型:http://www.martinfowler.com/bliki/AnemicDomainModel.html
"贫血领域模型“发生在当只有数据实体例如类,属于实体的领域逻辑混在了较高层的类中。需要注意的是
在一般情况下,领域服务不应包含任何内部实体的逻辑。 如果服务(领域服务)拥有100%的实体逻辑,属于不同实体域逻辑的混合可能是危险的。这是“事务脚本" 实现的特征,和”领域模型“或”面向领域“正相反。“事务脚本"的方法具有”贫血模型“,因为所有的逻辑都在服务中,领域实体对象是什么?只是承载数据的架构。客户端代码请求领域提供数据,然后客户端代码根据这些数据作出决定。 事务脚本和业务服务层的方法容易上手,但当应用变得更复杂时会导致一堆问题。
我们实现应用领域驱动设计模式是为了消除这些事务脚本和业务服务。事务脚本只是负责找到合适的领域的入口点,然而告诉领域类做一些工作。 结果是业务逻辑可以在领域中更深的表达。你可以方便的找到正确的抽象,可以让你的代码容易理解,可以建有单一职责的类。这也意味着,对象的创建和持久化会抽象和封装,不必知道数据访问就可以调用客户端代码或脚本。这是因为领域实体应该是持久化透明,不应该有任何数据访问的引用。这些工作都不需要在事务脚本中进行,移动到了领域中去做。
此外,使用/调用仓储库的相关逻辑应该是在应用层的服务里。除非我们确实需要,它不应该在领域服务中,一个对象(实体)不需要知道是如何保存自己,就像一个引擎提供引擎的能力,但不制造本身,或是一本书不知道怎么把自己放在书架上。这就是为什么我们不应该从实体内部调用仓储库。 我们应该包含在领域层的逻辑应该是我们可以和领域/业务专家讨论的。这种情况,我们通常不和他们讨论任何有关仓储库或事务的事。关于使用领域服务的仓储库,当然有例外。那就是当我们需要根据领域逻辑的状态来获取数据。这种情况,我们需要从领域服务调用仓储库。但这通常用户查询数据。所有的事务,工作单元管理等等,应该放在应用层的服务。
Rule Nº: D8. 基于标识识别实体
- "领域驱动设计”书中的实体模式 by Eric Evans.
- 实体设计模式
- http://www.codeproject.com/KB/architecture/entitydesignpattern.aspx
Rule Nº: D9. 面向领域架构的实体必须是POCO或STE(自跟踪实体)。
- "领域驱动设计”书中的实体模式 by Eric Evans.
- 实体设计模式
- http://www.codeproject.com/KB/architecture/entitydesignpattern.aspx
下面的图展示了特定应用中的类,强调的是有时可以作为实体,有时可以作为实体中的值对象:
Rule Nº: D10. 需要时识别和实现值对象模式
- Martin Fowler的“值对象”模式。《企业应用架构模式》中提到:“一个小型简单的对象,像是钱或者时间区间,它们不是基于标识符的。”
至于性能,由于值对象不可更改的性质,使我们可以执行一定的“技巧”。这在系统中有数千个值对象的实例而可能有值是相同的巧合。它们不可变的性质,使我们能够重用它们;它们会是“可互换”的对象,由于它们的值是相同的而它们没有标识符。这种优化对有的软件运行缓慢而有的有良好性能起重要作用。当然,所有这些推荐要根据应用的环境和部署环境。共享对象有时提供更好的性能但在特定的上下文中可能减少可伸缩性,因为访问共享的重用对象的中心点可能导致通信的瓶颈。
一个模型可以有任意数量的对象,大多数对象会和其它有关联。我们会有不同种类的关联。大多数对象间的关系必须要在代码和数据库中体现。例如,雇员和公司间的一对一关系会体现在对象间的引用,也会在数据库两张表间有关联。关于一对多关系情况就更复杂。但是也有很多关系对于领域模型是没有用的。简言之,当模型有很多复杂的关系时很难保证模型变化的一致性。
因此,我们要考虑的是尽量简化中的实体模型的关系。这就是聚合模型的所在。一个聚合是一组相关的被视为整体的对象。聚合由一个边界把内部和外部的对象分开。每个聚合有一个根对象(聚合根实体),从外部访问只能通过这个对象。根实体对象有组成聚合所有对象的引用,但是外部对象只能引用根对象实体。如果在集合内有其它的实体或值对象,这些实体对象的标识符是本地的,只有在属于聚合时才有意义。如果独立时没有意义。
这种单点的聚合访问准确地确保数据的完整性。只有聚合根才能使用仓储库直接查询,其它的只能通过相关的聚合访问。聚合的外部没有访问或修改聚合内的数据,只有通过根,这意味着一个非常重要的控制水平。如果根实体被删除,聚合内部的其它对象也被删除。
如果聚合对象需要保存到数据库,应该只能通过根实体访问。聚合内的二级对象必须通过关联获取。这意味着只有聚合的根实体可以和仓储库有关联。同样的情况发生在一个较高的层的服务。我们可以有服务直接和聚合根实体相关,但是不能和聚合的二级对象相关。
然而一个聚合的内部对象,允许有其它聚合根实体的引用。
Rule Nº: D11. 在需要的情景识别和实现聚合模式来简化模型对象间的关系
- 《领域驱动设计》的聚合模式 - Eric Evans.