领域驱动设计模式设计与实践

想进一步了解DDD? 加入6月26日在纽约QCon举行的红帽开放创新实验室的专家那里,他们将使用Domain Driven Design的实践帮助您进行发现之旅,以帮助您的企业实施精益产品开发,共识和“左移”哲学。

领域驱动设计模式设计与实践_领域驱动的设计和开发实践

背景

域驱动设计(DDD)是关于将业务域概念映射到软件工件中的。 关于该主题的大多数著作和文章都基于Eric Evans的书“域驱动设计”,主要从概念和设计的角度涵盖了领域建模和设计方面。 这些著作讨论了DDD的主要元素,例如实体,值对象,服务等,或者谈论了诸如泛在语言 ,有限上下文和反腐败层之类的概念。

本文的目的是从实用的角度介绍域建模和设计,以探讨如何采用域模型并实际实现它。 我们将研究技术负责人和架构师可以在实施工作中使用的指南,最佳实践,框架和工具。 域驱动的设计和开发还受以下几个体系结构,设计和实现方面的影响:

  • 商业规则
  • 坚持不懈
  • 快取
  • 交易管理
  • 安全
  • 代码生成
  • 测试驱动开发
  • 重构

本文讨论了这些不同因素如何影响实施项目的整个生命周期,以及架构师在成功实现DDD实施过程中应寻求的内容。 我将从典型的域模型应该具有的特性列表开始,以及何时在企业中使用域模型(相对于完全不使用域模型或使用贫血的域模型)。

本文包括一个示例贷款处理应用程序,以演示此处讨论的设计方面和开发最佳实践如何在实际领域驱动的开发项目中使用。 该示例应用程序在实现贷款处理域模型时使用诸如Spring,Dozer,Spring Security,JAXB,Arid POJO和Spring Dynamic Modules之类的框架。 示例代码将使用Java,但是对于大多数开发人员而言,无论其语言背景如何,它都应该很容易理解。

介绍

域模型提供了一些好处,其中包括:

  • 它可以帮助团队在公司的业务和IT利益相关者之间创建一个通用模型,团队可以使用该模型来交流业务需求,数据实体和流程模型。
  • 该模型是模块化的,可扩展的,并且易于维护,因为该设计反映了业务模型。
  • 它提高了业务域对象的可重用性和可测试性。

另一方面,让我们看看当IT团队不遵循域模型方法来开发中型到大型企业软件应用程序时会发生什么。

不投资领域模型和开发工作就会导致具有“胖服务层”和“贫血领域模型”的应用程序体系结构,其中外观类(通常是无状态会话Bean)开始积累越来越多的业务逻辑,并且领域对象变成纯粹的数据带有吸气剂和吸气剂的载体。 这种方法还导致特定于领域的业务逻辑和规则分散(在某些情况下重复)在几个不同的外观类中。

在大多数情况下,贫血症域模型并不具有成本效益; 它们不会给公司带来与其他公司相比的竞争优势,因为在此体系结构中实施业务需求更改需要花费很长时间才能开发和部署到生产环境。

在介绍DDD实施项目中的不同体系结构和设计注意事项之前,让我们看一下富域模型的特征。

  • 域模型应专注于特定的业务运营域。 它应与业务模型,策略和业务流程保持一致。
  • 它应该与业务中的其他域以及应用程序体系结构中的其他层隔离。
  • 应该可以避免重复使用相同核心业务域元素的任何模型和实现。
  • 该模型应设计为与应用程序中的其他层松散耦合,这意味着不依赖于域层任一侧的层(即数据库层和立面层)。
  • 它应该是一个抽象且清晰分离的层,以便于维护,测试和版本控制。 域类应该在容器外部(以及在IDE内部)可以进行单元测试。
  • 应该使用没有任何技术或框架依赖性的POJO编程模型来设计它(我总是告诉与我公司合作的项目团队,我们用于软件开发的技术是Java)。
  • 域模型应该独立于持久性实现细节(尽管该技术确实对模型施加了一些约束)。
  • 它应该对任何基础架构框架具有最小的依赖性,因为它将比这些框架寿命更长,并且我们不希望在任何外部框架上进行任何紧密耦合。

为了在软件开发方面获得更好的投资回报(ROI),业务部门和IT部门的高级管理层必须致力于在业务领域建模及其实现方面的投资(时间,金钱和资源)。 让我们看一下实现域模型所需的其他一些因素。

  • 团队应定期与业务领域主题专家联系。
  • IT团队(建模人员,架构师和开发人员)应具备良好的建模和设计技能。
  • 分析师应具有良好的业务流程建模技能。
  • 架构师和开发人员应具有丰富的面向对象设计(OOD)和编程(OOP)经验。

域驱动设计在企业体系结构中的作用

域建模和DDD在企业体系结构(EA)中起着至关重要的作用。 由于EA的目标之一是使IT与业务部门保持一致,因此表示业务实体的域模型成为EA的核心部分。 这就是为什么应围绕领域模型设计和实现大多数EA组件(业务或基础设施)的原因。

域驱动设计和SOA

面向服务的体系结构(SOA)在最近的过去中获得越来越多的势头,以帮助团队根据业务流程构建软件组件和服务,并加快新产品的上市时间。 域驱动设计是SOA体系结构的关键元素,因为它有助于将业务逻辑和规则封装在域对象中。 域模型还提供了可以用来定义服务合同的语言和上下文。

如果尚不存在,那么SOA的工作应包括域模型的设计和实现。 如果我们过分强调SOA服务而忽略了域模型的重要性,那么最终将导致应用程序体系结构中的贫乏域模型和膨胀的服务。

一种理想的情况是,在开发领域中,DDD可以通过开发应用层和SOA组件来迭代实现,因为它们是领域模型元素的直接使用者。 使用丰富的域实现,通过为域对象提供外壳(代理),SOA设计将变得相对简单。 但是,如果我们过多地关注SOA层,而在后端没有合适的域模型,则业务服务将调用不完整的域模型,这可能会导致脆弱的SOA体系结构。

项目管理

域建模项目通常包括以下步骤:

  • 首先对业务流程进行建模和记录。
  • 选择一个候选业务流程,并与业务领域专家一起使用通用语言对其进行记录。
  • 标识候选业务流程所需的所有服务。 本质上,这些服务可以是原子的(单步)或协调的(有或没有工作流程的多步)。 它们也可以是业务(例如,包销或资金)或基础架构(例如,电子邮件或作业计划)。
  • 识别并记录上一步中确定的服务所使用的对象的状态和行为。

重要的是,首先将模型保持在高层次上,专注于业务领域的核心要素。

从项目管理的角度来看,实际的DDD实施项目包含与任何其他软件开发项目相同的阶段。 这些阶段包括:

  • 建模领域
  • 设计
  • 发展历程
  • 单元和集成测试
  • 根据设计和开发(模型概念的连续集成(CI))优化和重构域模型。
  • 使用更新的域模型(域实现的CI)重复上述步骤。

敏捷软件开发方法论非常适合这里,因为敏捷方法论着重于业务价值的传递,就像DDD着眼于使软件系统与业务模型保持一致一样。 同样,由于DDD具有迭代性质,因此诸如SCRUM或DSDM之类的敏捷方法是管理项目的更好框架。 使用SCRUM(用于项目管理)和XP(用于软件开发目的)方法是管理DDD实施项目的良好组合。

DDD迭代周期的此项目管理模型如下图1所示。

领域驱动设计模式设计与实践_领域驱动的设计和开发实践

图1. DDD迭代周期图(单击屏幕快照以打开完整尺寸的视图。)

域驱动的设计工作始于域建模结束的地方。 Ramnivas Laddad 建议有关如何实现域对象模型的以下步骤 他强调在域模型中要比在服务上更多地关注域对象。

  • 从域实体和域逻辑开始。
  • 最初从没有服务层开始,仅添加逻辑不属于任何域实体或值对象的服务。
  • 使用泛在语言, 按合同设计 (DbC),自动测试,CI和重构,以使实现与域模型尽可能紧密地保持一致。

从设计和实现的角度来看,典型的DDD框架应支持以下功能。

  • 它应该是基于POJO(如果您的公司是.NET shop,则为POCO)的框架。
  • 它应支持使用DDD概念设计和实现业务领域模型。
  • 它应该支持开箱即用的依赖注入(DI)和面向方面的编程(AOP)之类的概念。 (注意:这些概念将在本文后面详细解释)。
  • 与单元测试框架(例如JUnitTestNGUnitils等) 集成
  • 与其他Java / Java EE框架(如JPA,Hibernate,TopLink等)的良好集成。

样品申请

本文中使用的示例应用程序是房屋贷款处理系统,业务用例是批准房屋贷款(抵押)的资金请求。 当贷款申请提交给抵押贷款公司时,首先要进行“包销”流程,在此过程中,包销商会根据客户的收入详细信息,信用记录和其他因素批准或拒绝贷款请求。 如果贷款申请获得了包销小组的批准,则将在贷款批准流程中进行“结账和注资”步骤。

贷款处理系统中的资金模块使向借款人的资金分配过程自动化。 融资过程通常从抵押贷款人(通常是银行)开始,将贷款打包转给产权公司。 然后,产权公司审查贷款方案,并与财产的买卖双方安排日期以结清贷款。 借款人和卖方与产权公司的结业代理人会面,签署文件,以转让财产所有权。

建筑

典型的企业应用程序体系结构由以下四个概念层组成:

  • 用户界面 (表示层):负责向用户呈现信息和解释用户命令。
  • 应用程序层:此层协调应用程序活动。 它不包含任何业务逻辑。 它不保存业务对象的状态,但是可以保存应用程序任务的进度状态。
  • 域层:此层包含有关业务域的信息。 业务对象的状态保存在这里。 业务对象及其状态的持久性委托给基础结构层。
  • 基础结构层:该层充当所有其他层的支持库。 它提供层之间的通信,实现业务对象的持久性,包含用于用户界面层的支持库等。

让我们更详细地了解应用程序和域层。 应用层:

  • 负责应用程序中的UI屏幕之间的导航以及与其他系统的应用程序层的交互。
  • 还可以在将用户输入数据传输到应用程序的其他(较低)层之前,对用户输入数据执行基本(与业务无关)验证。
  • 不包含任何与业务或域相关的逻辑或数据访问逻辑。
  • 没有反映业务用例的任何状态,但是可以管理用户会话的状态或任务的进度。

域层:

  • 负责业务域的概念,有关业务用例和业务规则的信息。 域对象封装了业务实体的状态和行为。 贷款处理应用程序中的业务实体示例包括抵押,财产和借款人。
  • 如果用例跨越多个用户请求(例如,包括多个步骤的贷款注册过程:用户输入贷款明细,系统根据贷款参数返回产品和利率,),则还可以管理业务用例的状态(会话)用户选择特定的产品/利率组合,最后系统锁定该利率的贷款)。
  • 包含仅具有已定义操作行为的服务对象,该服务对象不属于任何域对象。 服务封装了业务域中不适合域对象本身的行为。
  • 是业务应用程序的核心,应与应用程序的其他层完全隔离。 而且,它不应该依赖于其他层(JSP / JSF, Struts ,EJB, HibernateXMLBeans等)中使用的应用程序框架。

下面的图2显示了应用程序中使用的不同体系结构层以及它们与DDD的关系。

领域驱动设计模式设计与实践_领域驱动的设计和开发实践

图2.分层的应用程序架构图(单击屏幕快照以打开完整尺寸的视图。)

以下设计方面被视为当前DDD实施方案的主要组成部分:

  • 面向对象编程( OOP
  • 依赖注入( DI
  • 面向方面的编程( AOP

OOP是域实现中最重要的元素。 应该利用普通Java类和接口通过利用诸如继承,封装和多态性之类的OOP概念来设计域对象。 大多数域元素都是具有状态(属性)和行为(作用于状态的方法或操作)的真实对象。 它们还对应于现实世界的概念,并且可以与OOP概念完全匹配。 DDD中的实体和值对象是OOP概念的经典示例,因为它们既具有状态又具有行为。

在典型的工作单元(UOW)中,域对象需要与其他对象进行协作,无论它们是服务,存储库还是工厂。 域对象还需要管理其他问题,例如本质上是跨领域的域状态更改跟踪,审核,缓存,事务管理(包括事务重试)。 这些是可重用的非域相关问题,通常会在包括域层在内的整个代码中分散和重复。 在域对象中嵌入此逻辑会导致与非域相关的代码使域层纠结和混乱。

在管理代码依赖关系而又没有对象之间的紧密耦合和隔离横切关注点时,仅OOP不能为域驱动的设计和开发提供出色的设计解决方案。 在这里,可以使用DI和AOP等设计概念来补充OOP,以最大程度地减少紧密耦合,增强模块化并更好地管理横切关注点。

依赖注入

DI是将配置和相关性代码移出域对象的一种好方法。 同样,域类对数据访问对象(DAO)类和服务类对域类的设计依赖性使得DI在DDD实现中“必不可少”。 通过将其他对象(例如存储库和服务)注入到域对象中,DI促进了更清洁和松耦合的设计。

在示例应用程序中,服务对象(FundingServiceImpl)使用DI注入实体对象(Loan,Borrower和FundingRequest)。 此外,实体通过DI引用存储库。 同样,其他Java EE资源(如DataSource ,Hibernate Session FactoryTransaction Manager)也注入到Service和Repository对象中。

面向方面的编程

AOP通过从域对象中删除诸如审计,域状态更改跟踪之类的横切关注点代码,有助于实现更好的设计(即,在域模型中减少混乱)。 它可用于将协作对象和服务注入域对象,尤其是未被容器实例化的对象(例如持久性对象)。 可以使用AOP的域层中的其他方面是缓存,事务管理和基于角色的安全性(授权)。

贷款处理应用程序使用自定义方面将数据缓存引入Service对象。 一次从数据库表中加载贷款产品和利率信息(客户端首先请求此信息),然后将其存储在对象缓存( JBossCache )中,以用于后续的产品和利率查询。 产品和费率数据经常被访问,但是不会定期更新,因此它是缓存数据而不是每次都访问后端数据库的理想选择。

DI和AOP概念在DDD中的作用是最近讨论线程中的主要主题。 讨论基于Ramnivas Laddad的演讲,他在声明中断言, 如果没有AOP和DI的帮助DDD是无法实现的 在演示中,Ramnivas讨论了使用AOP使域对象恢复智能行为的“细粒度DI”的概念。 他提到域对象需要访问其他细粒度的对象以提供丰富的行为,对此的解决方案是将服务,工厂或存储库注入域对象(通过使用Aspect在构造函数或setter调用时注入依赖项)。

克里斯·理查森(Chris Richardson)还讨论了如何使用DI,对象和方面通过减少耦合和增加模块化来改进应用程序设计。 克里斯谈到了“大胖服务”反模式,这是应用程序代码耦合,纠结和分散的结果,以及如何使用DI和AOP概念避免使用它。

注解

定义和管理方面和DI的最新趋势是使用注释。 注释有助于最大程度地减少实现诸如EJB或Web服务之类的远程服务所需的构件。 它们还简化了配置管理任务。 Spring 2.5Hibernate 3和其他框架充分利用了注释功能来在Java企业应用程序的不同层中配置组件。

我们应该利用注释来生成样板代码,从而在灵活性方面增加价值。 同时,应谨慎使用注释。 应该在不混淆或误导理解实际代码的地方使用它们。 使用注释的一个很好的例子是Hibernate ORM映射,它在类或属性名称旁边添加值以指定SQL表或列名称。 另一方面,诸如JDBC驱动程序配置(驱动程序名称,jdbc url,用户名和密码)之类的详细信息比使用批注更适合存储在XML文件中。 这是基于数据库处于相同上下文的假设。 如果在域模型和数据库表之间需要进行重大转换,则设计应考虑到这一点。

Java EE 5中提供的JPA注释像@实体@PersistenceUnit@PersistenceContext等添加持久性细节,以普通的Java类。 在域建模的上下文中,实体,存储库和服务是使用注释的很好的候选者。

@Configurable是Spring将存储库和服务注入域对象的方式。 Spring框架将“域对象DI”的概念扩展到@Configurable注释之外。 Ramnivas最近在博客中介绍了即将发布的Spring 2.5.2版本(从项目快照内部版本379开始可用)的最新改进 有三个新方面(AnnotationBeanConfigurerAspect,AbstractInterfaceDrivenDependencyInjectionAspect和AbstractDependencyInjectionAspect)可以为域对象DI提供简单而灵活的选项。 Ramnivas说,引入中间方面(AbstractInterfaceDrivenDependencyInjectionAspect)的主要原因是允许特定于域的注释和接口发挥作用。 Spring还提供其他注释,例如@Repository@ Service@Transactional,以帮助设计域类。

示例应用程序中使用的某些注释,实体对象(贷款,借款人和FundingRequest)使用@Entity注释。 这些对象还使用@Configurable注释连接存储库对象。 并且Service类使用@Transactional批注来使用事务行为装饰服务方法。

域模型和安全性

域层中的应用程序安全性可确保只有授权的客户端(人类用户或其他应用程序)才能调用域操作以及访问域状态。

Spring Security (Spring Portfolio中的一个子项目)在应用程序的表示层(基于URL)和域(方法级别)中都提供了细粒度的访问控制。 该框架使用Spring的Bean代理来拦截方法调用并应用安全约束。 它使用MethodSecurityInterceptor类为Java对象提供了基于角色的声明性安全性。 还存在用于域对象的访问控制列表(ACL)形式的实例级安全性,以在实例级控制用户访问。

使用Spring Security在域模型中管理授权需求的主要优势在于,该框架具有非侵入式架构,因此我们可以在域和安全性方面实现清晰的隔离。 同样,业务对象也不会因安全实施细节而混乱。 我们可以在一个地方编写通用的安全规则,并在需要实现它们的任何地方应用它们(使用AOP技术)。

在域和服务类中,授权在类方法调用级别进行管理。 例如,对于具有不超过100万美元的贷款,任何具有“包销商”角色的用户都可以调用“包销”域对象中的“贷款批准”方法,而在同一域对象中,对于贷款金额更大的贷款申请,可以使用“批准方法”只有具有“ Underwriting Supervisor”角色的用户才能调用超过100万美元。

下表总结了应用程序体系结构各层中各种应用程序安全问题。

表1.各种应用程序层中的安全问题

安全问题
客户端/控制器 身份验证,网页(URL)级别授权
正面 基于角色的授权
域实例级授权,ACL
数据库 数据库对象级别授权(存储过程,存储函数,触发器)

商业规则

业务规则是业务领域的重要组成部分。 它们定义了在特定业务流程场景中需要应用于域对象的数据验证和其他约束。 业务规则通常分为以下几类:

  • 资料验证
  • 数据转换
  • 商业决策
  • 流程路由(工作流程逻辑)

在DDD世界中,上下文非常重要。 上下文的特定性决定了域对象的协作以及其他运行时因素,例如要应用的业务规则等。验证和其他业务规则始终在特定的业务上下文中进行处理。 这意味着在不同的业务上下文中,同一域对象将必须处理不同的业务规则集。 例如,在通过贷款审批过程中的“包销”步骤之后,不能更改贷款域对象的某些属性(例如贷款金额和利率)。 但是,只要为特定的利率注册并锁定贷款,就可以更改相同的属性。

即使所有特定于域的业务规则都应该封装在域层中,但是某些应用程序设计还是将规则放在外观类中,这导致域类在业务规则逻辑方面变得“贫乏”。 在小型应用程序中,这可能是可接受的解决方案,但对于包含复杂业务规则的中型到大型企业应用程序,则不建议使用。 更好的设计选择是将规则所属的域对象放在域对象内。 如果业务规则逻辑跨越两个或多个Entity对象,则它应成为Service类的一部分。

同样,如果我们对应用程序不了解,最终设计业务规则将以代码中的多个switch语句的形式进行编码。 随着时间的流逝,随着规则变得越来越复杂,开发人员无需花时间重构代码即可将“ switch”语句移至更易于管理的设计中。 在类中对复杂的路由或决策规则逻辑进行硬编码会导致类中的方法更长,代码重复,最终导致僵化的应用程序设计,从长远来看,这将成为维护的噩梦。 一个好的设计是将所有规则(特别是随着业务战略的变化而经常变化的复杂规则)放入一个规则引擎(使用诸如JBoss RulesOpenRulesMandarax之类的规则框架)并从域类中调用它们。

验证规则通常以不同的语言(例如Javascript,XML,Java代码和其他脚本语言)实现。 但是由于业务规则的动态性质,诸如RubyGroovy领域特定语言 (DSL)之类的脚本语言是定义和管理这些规则的更好选择。 Struts(应用程序层),Spring(服务)和Hibernate(ORM)都有自己的验证模块,我们可以在其中将验证规则应用于传入或传出的数据对象。 在某些情况下,验证规则也可以作为方面(可以在此处链接AOP规则文章)进行管理,可以编织到应用程序的不同层中(例如,服务和控制器)。

在编写域类以管理业务规则时,请务必牢记单元测试方面。 规则逻辑中的任何更改都应易于隔离地进行单元测试。

该示例应用程序包括一个业务规则集,以验证贷款参数是否在允许的产品和利率规范之内。 规则以脚本语言(Groovy)定义,并应用于传递给FundingService对象的贷款数据。

设计

从设计的角度来看,域层应具有明确定义的边界,以避免该层因非核心域层问题(如特定于供应商的翻译,数据过滤,转换等)而损坏。域元素应设计为正确保留域状态和行为。 根据状态和行为,不同的域元素的结构也不同。 下表2显示了域元素及其包含的内容。

表2.具有状态和行为的域元素

域元素 状态/行为
实体,价值对象,汇总 状态与行为
数据传输对象 仅说明
服务,存储库 仅行为

既包含状态(数据)又包含行为(操作)的实体,值对象和集合,应具有明确定义的状态和行为。 同时,此行为不应超出对象边界的限制。 在用例上,实体应该根据其本地状态来完成大部分工作。 但是他们不应该知道太多无关的概念。

良好的设计习惯是仅包含封装域对象状态所需的属性的getter / setter。 在设计域对象时,仅为那些可以更改的字段提供设置方法。 此外,公共构造函数应仅包含必填字段,而不是包含域类中所有字段的构造函数。

在大多数用例中,我们实际上并不需要直接更改对象的状态。 因此,与其更改内部状态,不如创建具有更改后状态的新对象并返回该新对象。 在这些用例中就足够了,并且还降低了设计复杂度。

聚合类向调用者隐藏了协作类的用法。 它们可用于将复杂的,侵入的和与状态相关的要求封装在域类中。

支持DDD的设计模式

有几种设计模式有助于领域驱动的设计和开发。 以下是这些设计模式的列表:

  • 域对象(DO)
  • 数据传输对象(DTO)
  • DTO组装商
  • 存储库:存储库包含以域为中心的方法,并使用DAO与数据库进行交互。
  • 通用DAO
  • 时间模式:这些模式将时间维度添加到富域模型中。 基于Martin Fowler的“ 时间模式”的双时 框架提供了一种处理领域模型中的双时态问题的设计方法。 可以使用ORM产品(如Hibernate)来保留核心域对象及其比特时态属性。

DDD中使用的其他设计模式包括策略,外观和工厂。 吉米尼尔森(Jimmy Nilsson)讨论了工厂是他书中的一种领域模式。

DDD反模式

在最佳实践和设计模式的另一面,当实现域模型时,架构师和开发人员应注意一些DDD气味。 这些反模式的结果是,域层成为应用程序体系结构中最不重要的部分,而外观类在模型中扮演着更重要的角色。 以下是其中一些反模式:

  • 贫血域对象
  • 重复的DAO
  • 胖服务层:这是服务类最终拥有所有业务逻辑的地方。
  • Feature Envy:这是Martin Fowler在有关重构的书中提到的经典气味之一,其中一类中的方法对属于其他类的数据过于感兴趣。

数据访问对象

DAO和存储库在域驱动设计中也很重要。 DAO是关系数据库和应用程序之间的契约。 它封装了来自Web应用程序的数据库CRUD操作的详细信息。 On the other hand, a Repository is a separate abstraction that interacts with the DAOs and provides "business interfaces" to the domain model.

Repositories speak the Ubiquitous Language of the domain, work with all necessary DAOs and provide data access services to the domain model in a language the domain understands.

DAO methods are fine-grained and closer to the database while the Repository methods are more coarse-grained and closer to the domain. Also one Repository class may have multiple DAO's injected. Repositories and DAO's keep the domain model decoupled from dealing with the data access and persistence details.

The domain objects should depend only on Repository interfaces. This is the reason why injecting the Repository instead of a DAO results in a much cleaner domain model. DAO classes should never be called directly from the client (Services and other consumer classes). The clients should always call the domain objects which in turn should call the DAO's for persisting the data to the data store.

Managing the dependencies between domain objects (for example, the dependency between an Entity and its Repository) is a classic problem that developers often run into. The usual design solution to this problem is to have the Service or Facade class call a Repository directly and when invoked the Repository would return the Entity object to the client. This design eventually leads to the afore-mentioned Anemic Domain Model where facade classes start accumulating more business logic and domain objects become mere data carriers. A good design is to inject Repositories and Services into domain objects using DI & AOP techniques.

Sample application follows these design principles in implementing the loan processing domain model.

坚持不懈

Persistence is an infrastructural aspect from which the domain layer should be decoupled. JPA provides this abstraction by hiding the details of the persistence implementation from the classes. It is annotation driven so no XML mapping files are required. But at the same time, the table and column names are embedded in the code which may not be a flexible solution in some cases.

With grid computing products such as Oracle Coherence , WebSphere Object Grid, and GigaSpaces that offer data grid solutions, the developers don't even need to think about a RDBMS when they model and design the business domain. The database layer is abstracted from domain layer in the form of an in-memory Object/Data Grid.

快取

When we talk about the state (data) of the domain layer, we have to talk about the aspect of caching. Frequently accessed domain data (such as products and rates in a mortgage loan processing application) are good candidates for caching. Caching speeds up the performance and reduces the load on the database server. Service layer is ideal for caching the domain state. ORM frameworks like TopLink and Hibernate also provide data caching.

Loan processing sample application uses JBossCache framework to cache product and rate details to minimize the database calls and improve application performance.

Transaction Management

Transaction management is important to keep the data integrity and to commit or rollback the UOW as a whole. There has always been a debate about where the transactions should be managed in the application architecture layers. There are also the cross-entity transactions (that span multiple domain objects in the same UOW) that affect the design decision of where the transactions should be managed.

Some developers prefer managing the transactions in the DAO classes which is a poor design. This results in too fine-grained transaction control which doesn't give the flexibility of managing the use cases where the transactions span multiple domain objects. Service classes should handle transactions; this way even if the transaction spans multiple domain objects, the service class can manage the transaction since in most of the use cases the Service class handles the control flow.

FundingServiceImpl class in the sample application manages transactions for the funding request and executes multiple database operations by calling the Repositories and commits or rolls back all database changes in a single transactions.

Data Transfer Objects

DTO's are also an important part of the design in an SOA environment where the Domain object model structurally is not compatible with the messages that are received and sent from a business service. The messages are typically defined and maintained in as XML Schema Definition documents (XSD's) and it's a common practice to write (or code generate) DTO objects from the XSD's and use them for data (message) transfer purposes between domain and SOA service layers. Mapping the data from one or more domain objects to a DTO will become a necessary evil in distributed applications where sending the domain objects across the wire may not be practical from a performance and a security stand-point.

From a DDD perspective, DTO's also help maintain the separation between Service and UI layers where DO's are used in the domain and service layers and DTO's are used in the presentation layer.

Dozer framework is used for the assembly of one or more domain objects into a DTO object. It is bi-directional which saves a lot of extra code and time when converting domain objects into DTO's and vice-versa. The 2-way mapping between DO and DTO objects helps eliminate the separate DO -> DTO and DTO -> DO translation logic. The framework also correctly handles the type and array conversion.

The sample application uses Dozer mapping files (XML) to split the FundingRequestDTO object into Loan, Borrower, and FundingRequest Entity objects when the request comes in for fund processing. The mapping also takes care of the aggregation of the funding response data from the Entities into a single DTO object on the way back to the client.

DDD Implementation Frameworks

Frameworks like Spring and Real Object Oriented ( ROO ), Hibernate, and Dozer aid in designing and implementing a domain model. Other frameworks that support DDD implementation are JMatter , Naked Objects , Ruby On Rails , Grails , and Spring Modules XT Framework .

Spring takes care of instantiating and wiring together the domain classes such as Services, Factories, and Repositories. It also injects Services into Entities using @Configurable annotation. This annotation is Spring specific, so other options for achieving this injection is to use something like Hibernate Interceptor.

ROO is a DDD implementation framework built on "domain first and infrastructure second" philosophy. The framework was developed to reduce the boiler-plate coding of the patterns found in web application development. When using ROO, we define the domain model, and then the framework (based on Maven Archetypes) generates the code for Model-View-Controller (MVC), DTO's, Business Layer Facade and DAO layers. It even generates the stubs for unit and integration tests.

ROO has some very useful practical implementation patterns. For example, it distinguishes the state managed fields, the persistence layer uses field-level access, and the public constructors only reflect the mandatory fields.

发展历程

A model is no good without the actual implementation. The implementation phase should include automating the development tasks as much as possible. To see what tasks can be automated, let's look at a typical use case involving the domain model. Following is the list of steps in the use case:

Request In:

  • Client calls a Facade class sending data as an XML document (which is XSD compliant); Facade class initiates a new transaction for the UOW.
  • Run validations on the incoming data. These validations include the primary (basic/data type/field level checks) and business validations. If there are any validation errors, raise appropriate exceptions.
  • Translate the descriptions to codes (to be domain friendly).
  • Make the data formatting changes to be domain model friendly.
  • Make any separation of attributes (like splitting a customer name into first and last name attributes in a Customer Entity object).
  • Disassemble the DTO data into one or more domain objects.
  • Persist the state of domain objects.

Response Out:

  • Get the state of domain object(s) from datastore.
  • Cache the state if necessary.
  • Assemble the domain object(s) into application friendly data objects (DTO).
  • Make any merge or separation of data elements (such as combining first and last names into single customer name attribute).
  • Translate the codes into descriptions.
  • Make the data formatting changes necessary to address the client data usage requirements.
  • Cache the DTO state if necessary
  • Transaction commits (or rolls back if there was an error) as the control flow exits.

The following table shows different objects that carry the data from one layer to another in the application.

Table 3. Data flow through the application layers

From Object To Object 构架
DAO Database Table(s) DO Hibernate
Domain Delegate DO DTO Dozer
Data Transfer DTO XML格式 JAXB

As you can see there are few layers in the application architecture where same data flows through in different forms (DO, DTO, XML, etc). Most of these objects (Java or XML) that hold data as well as other classes like DAO, DAOImpl, and DAOTest are infrastructural in nature. These classes and XML files that have boiler-plate code and structure are great candidates for code generation.

代码生成

Frameworks like ROO also create a standard and consistent project template (using Maven plugin) for new projects. Using a pre-generated project template, we can achieve consistency in the directory structure on where to store the source and test classes, configuration files, and dependencies on internal and external (third-party) component libraries.

It could be overwhelming when we consider the myriad of classes and configuration files needed to develop a typical enterprise software application. Code generation is the best way to go about this problem. Code generation tools typically use some kind of template framework to define the templates or mappings from which a code generator can generate the code. Eclipse Modeling Framework (EMF) has several sub-projects that aid in the code generation of various artifacts required in a web application project. Model Driven Architecture (MDA) tools like AndroMDA use EMF to generate the code based on architecture models.

When it comes to writing the delegate classes in domain layer I have seen developers writing these classes manually (mostly writing the first one from scratch and then following "Copy And Paste" pattern to create the required delegate classes for other domain objects. Since most of these classes are basically facades to domain classes they are good candidates for code generation. Code generation option is a good long-term solution even though it involves some initial investment (in terms of coding and time) to build and test the code generator (engine).

For the test classes generated, a good option is to create abstract methods for the methods with complex business logic in the main class that need to be unit tested. This way, developers can extend the generated base test class and implement custom business logic which cannot be auto-generated. The same goes for any test method that has testing logic which cannot be auto-created.

Scripting languages are a better choice for writing code generators as they have less overhead and they support template creation and customization options. If we take advantage of code generation in a DDD project, we only need to write a few classes from scratch. The artifacts that must be created from scratch include:

  • XSD
  • Domain Object
  • 服务

Once we define the XSD and Java classes, we can code generate all or most of the following classes and configuration files:

  • DAO interface and implementation class
  • 工厂名称
  • 储存库
  • Domain Delegate (if needed)
  • Facade (including EJB and WebService classes)
  • DTO's
  • Unit Tests for the above classes (including test class and test data)
  • Spring configuration files

Table 4 below lists different layers in a web application architecture and what artifacts (Java classes or XML files) can be generated in that layer.

Table 4: Code generation in DDD implementation project

Layer/Function 模式 Code You Write Generated Code 构架
Data Access DAO/Repository    DAO Interface,
DAO Implementation class,
DAOTest,
Test Seed Data
Unitils,
DBUnit
Domain DO Domain class DomainTest   
坚持不懈 ORM Domain Class ORM Mappings,
ORM Mapping Test
Hibernate,
ORMUnit
Data Transfer DTO XSD DTO JAXB
DTO Assembly Assembler Mapping DO-DTO Mapping Dozer
Delegate Business Delegate Code to translate DO's to DTO's      
Facade    Facade    Remote Service,
EJB,
Web Service
控制者 MVC Controller Mapping Files Struts/Spring MVC   
Presentation MVC View configuration files Spring MVC

Delegate layer is the only layer that has knowledge of both Domain Objects and DTO's. Other layers such as Persistence layer should be unaware of DTO's.

重构

Refactoring is changing or restructuring the application code without changing the functionality or behavior of the application. Refactoring can be either design or code related. Design refactoring is done to continually refine the model and refactor the code to improve the domain model.

Refactoring plays an important role in the DDD project due to its iterative and evolutionary nature of domain modeling. One way to integrate refactoring tasks into the project is to add it in each iteration of the project before calling the iteration done. Ideally, refactoring should be done before and after every development task.

Refactoring should be done with strict discipline. Use the combination of refactoring, CI, and unit testing to make sure the code changes didn't break any functionality and at the same time the changes did help with the intended code or performance improvements.

Automated tests play a vital role in refactoring the application code. Without good automated developer tests and Test Driven Development (TDD) practices, refactoring can be counter-productive since there will be no automated way to verify that the design and code changes made as part of refactoring effort didn't change the behavior or break the functionality.

Tools like Eclipse help in implementing the domain model in an iterative way with refactoring as part of the development effort. Eclipse has features like extracting or moving a method to a different class or pushing down a method to a subclass. There are also several code analysis plugins for Eclipse that can help in managing the code dependencies and identifying the DDD anti-patterns. I rely on plugins like JDepend , Classycle , and Metrics when I do the design and code review of projects, to assess the quality of domain and other modules in the application.

Chris Richardson talked about applying code refactoring to transform a procedural design into an OO design using refactoring features provided by Eclipse.

Unit Testing/Continuous Integration

One of the goals we talked about earlier is that the domain classes should be unit testable (during the initial development as well as later when refactoring the existing code) without too many dependencies on the container or other infrastructure code. TDD approach helps the team in finding any design problems early in the project as well as verifying that the code is in alignment with domain model. DDD is ideal for Test-First development because the state and behavior are contained in domain classes and it should be easy to test them in isolation. It is important to test the state and behavior of domain model and not focus too much on the implementation details of data access or persistence.

Unit testing frameworks like JUnit or TestNG are great tools to implement and manage the domain model. Other testing frameworks like DBUnit and Unitils can also be used to test domain layer especially to inject test data into DAO classes. This will minimize writing extra code for populating test data in unit test classes.

Mock objects also help in testing the domain objects in isolation. But it's important to not go crazy with using mock objects in the domain layer. If there are other easy ways to test domain classes, you should use those options instead of using mock objects. For example, if you can test an Entity class using a real DAO class in the back-end (instead of a mock DAO implementation) with an in-memory HSQL database instead of the real database; it will make the domain layer unit tests run quicker which is the main idea behind using mock objects any way. This way, you will be testing the collaboration (interaction) between domain objects as well as the state (data) exchanged between them. With mock objects, we will only be testing the interaction between the domain objects.

All unit and integration tests created during the development phase (with or without using TDD practices) will become part of the automated test suite once the development tasks are done. These tests should be maintained and executed frequently in the local and higher development environments to find if the new code changes introduced any bugs into domain classes.

Eric Evans covers CI in his book saying that CI effort should always be applied within a Bounded Context and it should include the synchronization of people as well as code. CI tools like CruiseControl and Hudson can be used to set up an automatic build and test environment to run the application build script (created using a build tool such as Ant or Maven) to checkout the code from a SCM repository (like CVS , Subversion etc), compile the domain classes (as well as the other classes in the application) and if there are no build errors, then automatically run all the tests (unit and integration). CI tools can also be setup to notify the project teams (via e-mail or RSS feeds) if there are any build or test errors.

部署方式

Domain models are never static; they change as business requirements evolve during the lifecycle of the project and new requirements come up in the new projects. Also, as you develop and implement the domain model you constantly learn and improve, and you want to apply the new knowledge to the existing model.

Isolation is the key when packaging and deploying the domain classes. Since domain layer has dependencies on DAO layer on one side and Service Facade layer on the other (see the application architecture diagram in Figure 2), it makes lot of sense to package and deploy the domain classes as one or more modules to manage these dependencies elegantly.

While design patterns such as DI, AOP and Factories minimize the coupling between the objects at design time and make the application modular, OSGi (formerly known as Open Services Gateway initiative) addresses modularity at runtime. OSGi is becoming a standard mechanism to package and distribute the enterprise applications. It nicely handles the dependencies between the modules. We can also use OSGi for domain model versioning purposes.

We can package DAO classes in one OSGi bundle (the DAO bundle), the service facade classes in another bundle (service bundle), so when DAO or Service implementations are modified or a different version of the application is being deployed, thanks to OSGi, no application restarts are required. We can also deploy two different versions of the same domain class if we have to support the existing and new versions of certain domain objects for backward compatibility.

To take advantage of OSGi capabilities, the application objects have to be registered with OSGi platform before being consumed (ie before the client can do a lookup for them). This means that we have to use OSGi APIs to do the registration but we also have to deal with failure scenarios as services are started and stopped using OSGi container. Spring Dynamic Modules framework helps in this area by allowing any type of object to be exported and imported in the application without any code changes.

Spring DM also provides test classes to run OSGi integration tests outside the container. For example, AbstractOsgiTests can be used to run integration tests directly from the IDE. The setup is handled by the testing infrastructure so we don't have to write a MANIFEST.MF file for the test, or do any packaging or deployment. The framework supports most of the OSGi implementations currently available ( Equinox , Knopflerfish and Apache Felix ).

Loan processing application uses OSGi, Spring DM, and Equinox container to manage the module level dependencies and the deployment of domain and other modules. LoanAppDeploymentTests shows the use of Spring DM test module.

Sample Application Design

The domain classes used in the loan processing sample application are listed below:

Entities:

  • 贷款
  • Borrower
  • UnderwritingDecision
  • FundingRequest

Value Objects:

  • ProductRate

Services:

  • FundingService

Repositories:

  • LoanRepository
  • BorrowerRepository
  • FundingRepository

Figure 3 shows the domain model diagram for the sample application.

领域驱动设计模式设计与实践_领域驱动的设计和开发实践

Figure 3. Layered Application Domain Model (Click on the screen shot to open a full-size view.)

Most of the DDD design concepts and techniques discussed in this article are applied in the sample application. Concepts like DI, AOP, Annotations, Domain Level Security, and Persistence are used. Also, I used several open source frameworks to help in DDD development and implementation tasks. These frameworks are listed below:

  • 弹簧
  • Dozer
  • Spring Security
  • JAXB (Spring-WS for marshalling and unmarshalling the data)
  • Spring Testing (for unit and integration testing)
  • DBUnit
  • Spring Dynamic Modules

The domain classes in the sample application are deployed as an OSGi module using Equinox and Spring DM frameworks. The following table shows the module packaging details for the sample application.

Table 5. Packaging and Deployment Details

Deployment Artifact Name Module Contents Spring Configuration File(s)
Client/Controller loanapp-controller.jar Controller, Client Delegate classes LoanAppContext-Controller.xml
Facade loanapp-service.jar Facade (Remote) Service, Server Delegate classes, XSD's LoanAppContext-RemoteServices.xml
Domain loanapp-domain.jar Domain classes, DAO, Common DTO's LoanAppContext-Domain.xml, LoanAppContext-Persistence.xml
构架 loanapp-framework.jar Framework, Utility, Monitoring (JMX) classes, Aspects LoanAppContext-Framework.xml, LoanAppContext-Monitoring.xml, LoanApp-Aspects.xml

结论

DDD is a powerful concept that will change the way modelers, architects, developers, and testers will look at the software once the team is trained in DDD and start to apply "domain first and infrastructure second" philosophy. As different stakeholders (from IT and business units) with different backgrounds and areas of expertise are involved in the domain modeling, design and implementation effort, to quote Eric Evans, "it's important not to blur the lines between the philosophy of design (DDD) and the technical tool box that helps us fulfill it (OOP, DI, and AOP)".

Advancing Frontiers

This section covers some of the emerging approaches that impact the DDD design and development. Some of these concepts are still evolving and it will be interesting to see how they will affect DDD.

Architecture rules and Design by Contract enforcement plays an important role in the governance and policy enforcement of domain model standards and implementation best practices. Ramnivas talked about using the Aspects to enforce the rule of creating a Repository object only through Factories; this is an easy to violate design rule in domain layer.

Domain Specific Languages (DSL) and Business Natural Languages (BNL) are gaining more attention in the recent years. One can use these languages to represent business logic in the domain classes. BNL's are powerful in the sense that they can be used to capture business specifications, documenting the business rules, and as executable code as well. They can also be used to create test cases to verify the system works as expected.

Behavior Driven Development (BDD) is another interesting concept that has been discussed lately. BDD helps focus development on the delivery of prioritized, verifiable business value by providing a common vocabulary (Ubiquitous Language) that spans the divide between Business and Technology. By using terminology focused on the behavioral aspects of the system rather than testing, BDD attempts to help direct developers towards a focus on the real value to be found in TDD at its most successful. If practiced correctly, BDD can be a great complement to DDD where the development of domain objects is positively influenced by BDD concepts; after all domain objects are supposed to encapsulate state and behavior.

Event Driven Architecture (EDA) is another area that could play a role in domain driven design. For example, an event model to notify of any state change in the domain object instance would help in handling the post-event processing tasks that need to be triggered when the state of a domain object changes. EDA helps in encapsulating the event based logic from getting embedded in core domain logic. Martin Fowler documented about Domain Event design pattern.

资源资源

  • Domain-Driven Design, Tackling Complexity in the Heart of Software, Eric Evans, Addison Wesley
  • Applying Domain-Driven Design and Patterns, Jimmy Nilsson, Addison Wesley
  • Refactoring to Patterns, Joshua Kerievsky, Addison Wesley
  • Can DDD be Adequately Implemented Without DI and AOP?

The sample application code can be downloaded here .

您是建筑师还是有志成为?

由建筑师为建筑师撰写的一个月度通讯 ,掌握行业趋势。

领域驱动设计模式设计与实践_领域驱动的设计和开发实践

翻译自: https://www.infoq.com/articles/ddd-in-practice/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

领域驱动设计模式设计与实践

相关文章: