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

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

想进一步了解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操作的详细信息。 另一方面,存储库是与DAO交互并为域模型提供“业务接口”的独立抽象。

存储库使用域的通用语言,与所有必需的DAO一起工作,并以域可以理解的语言为域模型提供数据访问服务。

DAO方法更细粒度且更接近数据库,而Repository方法更粗粒度且更接近域。 同样,一个存储库类可能注入了多个DAO。 存储库和DAO使域模型与处理数据访问和持久性详细信息脱钩。

域对象应仅取决于存储库接口。 这就是为什么注入存储库而不是DAO会导致域模型更加简洁的原因。 永远不要直接从客户端(服务和其他使用者类)调用DAO类。 客户端应始终调用域对象,而域对象又应调用DAO,以将数据持久保存到数据存储中。

管理域对象之间的依赖关系(例如,实体及其存储库之间的依赖关系)是开发人员经常遇到的经典问题。 解决此问题的常用设计解决方案是让Service或Facade类直接调用存储库,并且在调用该存储库时会将Entity对象返回给客户端。 这种设计最终导致了上述的Anemic Domain Model,其中外观类开始积累更多的业务逻辑,而域对象仅成为数据载体。 一个好的设计是使用DI和AOP技术将存储库和服务注入域对象。

示例应用程序在实现贷款处理域模型时遵循这些设计原则。

坚持不懈

持久性是基础结构方面,应该从中解耦域层。 JPA通过从类中隐藏持久性实现的细节来提供这种抽象。 它是注释驱动的,因此不需要XML映射文件。 但是同时,表名和列名已嵌入代码中,在某些情况下可能不是灵活的解决方案。

借助提供数据网格解决方案的网格计算产品(例如Oracle Coherence ,WebSphere Object Grid和GigaSpaces) ,开发人员在建模和设计业务域时甚至不需要考虑RDBMS。 数据库层以内存中的对象/数据网格的形式从域层中抽象出来。

快取

当我们谈论域层的状态(数据)时,我们必须谈论缓存方面。 经常访问的域数据(例如抵押贷款处理应用程序中的产品和利率)是缓存的理想选择。 缓存可以提高性能,并减少数据库服务器上的负载。 服务层是缓存域状态的理想选择。 诸如TopLinkHibernate之类的ORM框架也提供数据缓存。

贷款处理示例应用程序使用JBossCache框架来缓存产品和费率详细信息,以最大程度地减少数据库调用并提高应用程序性能。

交易管理

事务管理对于保持数据完整性以及整个提交或回滚UOW非常重要。 关于在应用程序体系结构层中应在何处管理事务一直存在争议。 还有跨实体事务(跨越同一UOW中的多个域对象)会影响应在何处管理事务的设计决策。

一些开发人员更喜欢在DAO类中管理事务,这是一个糟糕的设计。 这会导致事务控制的粒度太细,从而无法灵活地管理事务跨越多个域对象的用例。 服务类别应处理交易; 这样,即使事务跨多个域对象,服务类也可以管理事务,因为在大多数用例中,服务类都会处理控制流。

示例应用程序中的FundingServiceImpl类通过调用存储库来管理资金请求的事务并执行多个数据库操作,并在单个事务中提交或回滚所有数据库更改。

数据传输对象

在域对象模型在结构上与从业务服务接收和发送的消息不兼容的SOA环境中,DTO还是设计的重要组成部分。 消息通常以XML架构定义文档(XSD)进行定义和维护,这是从XSD编写(或代码生成)DTO对象并将其用于域和SOA服务层之间的数据(消息)传输目的的一种惯例。 在分布式应用程序中,将数据从一个或多个域对象映射到DTO将成为必不可少的事情,在分布式应用程序中,从性能和安全性的角度来看,通过有线发送域对象可能不切实际。

从DDD的角度来看,DTO还可以帮助维护服务层和UI层之间的分隔,其中在域和服务层中使用DO,在表示层中使用DTO。

推土机框架用于将一个或多个域对象组装为DTO对象。 它是双向的,在将域对象转换为DTO时,可以节省大量额外的代码和时间,反之亦然。 DO和DTO对象之间的2向映射有助于消除单独的DO-> DTO和DTO-> DO转换逻辑。 该框架还可以正确处理类型和数组转换。

该示例应用程序使用Dozer映射文件(XML)在请求进行资金处理时将FundingRequestDTO对象拆分为Loan,Borrower和FundingRequest Entity对象。 映射还负责在返回客户的途中将来自实体的资金响应数据聚合到单个DTO对象中。

DDD实施框架

Spring和Real Object Oriented( ROO ),Hibernate和Dozer等框架有助于设计和实现域模型。 其他支持DDD实现的框架是JMatterNaked ObjectsRuby On RailsGrails和Spring Modules XT Framework

Spring负责实例化和连接领域类,例如服务,工厂和存储库。 它还使用@Configurable注释将服务注入到实体中。 这个注释是特定于Spring的,因此实现注入的其他选择是使用诸如Hibernate Interceptor之类的东西。

ROO是一种基于“域优先,基础设施第二”的哲学的DDD实现框架。 开发该框架是为了减少Web应用程序开发中发现的模式的样板代码。 使用ROO时,我们定义域模型,然后框架(基于Maven原型)生成用于模型-视图-控制器(MVC),DTO,业务层外观和DAO层的代码。 它甚至生成用于单元和集成测试的存根。

ROO具有一些非常有用的实际实施模式。 例如,它区分状态管理字段,持久层使用字段级访问,而公共构造函数仅反映强制字段。

发展历程

没有实际的实施,模型是不好的。 实施阶段应包括尽可能使开发任务自动化。 为了查看可以自动执行哪些任务,让我们看一下涉及域模型的典型用例。 以下是用例中的步骤列表:

要求输入:

  • 客户端调用Facade类,将其作为XML文档发送数据(符合XSD); 门面类为UOW发起新交易。
  • 对传入的数据运行验证。 这些验证包括主要验证(基本/数据类型/字段级别检查)和业务验证。 如果有任何验证错误,请提出适当的例外。
  • 将描述翻译为代码(易于使用)。
  • 进行数据格式更改,使其对域模型友好。
  • 进行属性的任何分隔(例如将客户名称分为“客户实体”对象中的名字和姓氏属性)。
  • 将DTO数据分解为一个或多个域对象。
  • 保留域对象的状态。

回应:

  • 从数据存储区获取域对象的状态。
  • 如有必要,缓存状态。
  • 将域对象组装成应用程序友好的数据对象(DTO)。
  • 进行数据元素的任何合并或分离(例如将名字和姓氏合并为单个客户名属性)。
  • 将代码翻译成描述。
  • 进行必要的数据格式更改,以解决客户端数据使用需求。
  • 必要时缓存DTO状态
  • 当控制流退出时,事务提交(或在发生错误时回滚)。

下表显示了不同的对象,这些对象将数据从应用程序的一层传送到另一层。

表3.通过应用程序层的数据流

从对象 反对 构架
数据库表 冬眠
域委托 DTO 推土机
数据传输 DTO XML格式 杰克斯

如您所见,应用程序体系结构中很少有相同的数据以不同的形式(DO,DTO,XML等)流经的层。 这些保存数据的对象(Java或XML)以及其他类(例如DAO,DAOImpl和DAOTest)中的大多数本质上都是基础结构。 这些具有样板代码和结构的类和XML文件非常适合用于代码生成。

代码生成

ROO之类的框架还为新项目创建了标准且一致的项目模板(使用Maven插件)。 使用预生成的项目模板,我们可以在目录结构中实现源和测试类,配置文件的存储位置以及内部和外部(第三方)组件库的依赖关系的一致性。

当我们考虑开发典型的企业软件应用程序所需的无数类和配置文件时,可能会感到不知所措。 代码生成是解决此问题的最佳方法。 代码生成工具通常使用某种模板框架来定义模板或映射,代码生成器可以从模板或映射生成代码。 Eclipse Modeling Framework (EMF)有几个子项目,这些子项目有助于Web应用程序项目中所需的各种工件的代码生成。 像AndroMDA这样的模型驱动架构(MDA)工具使用EMF生成基于架构模型的代码。

在域层中编写委托类时,我已经看到开发人员手动编写这些类(大多数情况是从头开始编写第一个类,然后按照“复制并粘贴”模式创建其他域对象所需的委托类。这些类基本上是领域类的基础,它们是代码生成的良好候选者。代码生成选项是一个很好的长期解决方案,即使它需要一些初始投资(就代码和时间而言)来构建和测试代码生成器(引擎) )。

对于生成的测试类,一个不错的选择是为需要进行单元测试的主类中具有复杂业务逻辑的方法创建抽象方法。 这样,开发人员可以扩展生成的基本测试类并实现无法自动生成的自定义业务逻辑。 具有无法自动创建的测试逻辑的任何测试方法也是如此。

脚本语言是编写代码生成器的更好选择,因为它们开销较小,并且支持模板创建和自定义选项。 如果我们利用DDD项目中的代码生成功能,则只需要从头开始编写几个类。 必须从头开始创建的工件包括:

  • XSD
  • 域对象
  • 服务

一旦定义了XSD和Java类,就可以通过代码生成以下所有或大多数类和配置文件:

  • DAO接口和实现类
  • 工厂名称
  • 储存库
  • 域委托(如果需要)
  • 外观(包括EJB和WebService类)
  • DTO的
  • 上述类别的单元测试(包括测试类别和测试数据)
  • Spring配置文件

下表4列出了Web应用程序体系结构中的不同层,以及可以在该层中生成哪些工件(Java类或XML文件)。

表4:DDD实施项目中的代码生成

层/功能 模式 您编写的代码 生成的代码 构架
资料存取 DAO /存储库    DAO介面
DAO实现类,
DAOTest,
测试种子数据
Unitils,
数据库单元
域类 域测试   
坚持不懈 ORM 域类 ORM映射,
ORM映射测试
Hibernate
ORM单元
数据传输 DTO XSD DTO 杰克斯
DTO组装 组装工 制图 DO-DTO映射 推土机
代表 业务代表 将DO转换为DTO的代码     
正面    正面    远程服务
EJB,
网络服务
控制者 MVC 控制器映射文件 Struts/弹簧MVC   
介绍 MVC 查看配置文件 SpringMVC

委托层是唯一了解域对象和DTO的层。 其他层(例如持久层)应该不知道DTO。

重构

重构是在不更改应用程序功能或行为的情况下更改或重组应用程序代码。 重构可以与设计或代码相关。 完成设计重构以不断完善模型并重构代码以改进领域模型。

重构在DDD项目中起着重要的作用,因为它具有域建模的迭代和进化特性。 将重构任务集成到项目中的一种方法是在调用迭代完成之前将其添加到项目的每个迭代中。 理想情况下,重构应该在每个开发任务之前和之后进行。

重构应遵循严格的纪律。 结合使用重构,配置项和单元测试,以确保代码更改不会破坏任何功能,同时这些更改确实有助于预期的代码或性能改进。

自动化测试在重构应用程序代码中起着至关重要的作用。 如果没有良好的自动化开发人员测试和测试驱动开发 (TDD)做法,则重构将适得其反,因为没有自动的方法来验证作为重构工作一部分而进行的设计和代码更改不会改变行为或破坏程序。功能。

Eclipse之类的工具可帮助您以迭代方式实现域模型,并将重构作为开发工作的一部分。 Eclipse具有诸如将方法提取或移动到其他类或将方法下推到子类的功能。 还有一些用于Eclipse的代码分析插件,可以帮助管理代码依赖性和识别DDD反模式。 在进行项目的设计和代码审查时,我依靠诸如JDependClassycleMetrics之类的插件来评估域和应用程序中其他模块的质量。

克里斯·理查森(Chris Richardson)谈到了使用代码重构来使用Eclipse提供的重构功能将程序设计转换为面向对象设计。

单元测试/持续集成

我们之前讨论的目标之一是,域类应该是可单元测试的(在初始开发期间以及以后在重构现有代码时),而对容器或其他基础结构代码没有太多依赖。 TDD方法可帮助团队在项目早期发现任何设计问题,并验证代码是否与领域模型保持一致。 DDD是Test-First开发的理想选择,因为状态和行为包含在域类中,并且应该容易地单独进行测试。 重要的是测试域模型的状态和行为,而不是过多地关注数据访问或持久性的实现细节。

像JUnit或TestNG这样的单元测试框架是实现和管理域模型的好工具。 其他测试框架(如DBUnit和Unitils)也可以用于测试域层,尤其是将测试数据注入DAO类中。 这将最大程度地减少编写多余的代码以填充单元测试类中的测试数据。

模拟对象还有助于隔离地测试域对象。 但是重要的是不要在域层使用模拟对象而发疯。 如果还有其他简单的方法来测试域类,则应使用这些选项,而不要使用模拟对象。 例如,如果您可以在后端使用真实的DAO类(而不是模拟DAO实现)使用内存中的HSQL数据库而不是真实的数据库来测试Entity类; 这将使域层单元测试更快地运行,这是使用模拟对象的任何方式的主要思想。 这样,您将测试域对象之间的协作(交互)以及它们之间交换的状态(数据)。 对于模拟对象,我们将仅测试域对象之间的交互。

一旦完成开发任务,在开发阶段创建的所有单元测试和集成测试(无论是否使用TDD实践)都将成为自动化测试套件的一部分。 这些测试应在本地和更高版本的开发环境中频繁维护和执行,以查找新代码更改是否将任何错误引入域类。

埃里克·埃文斯(Eric Evans)在他的书中谈到了CI,他说CI的工作应始终在有界上下文中进行,并且应该包括人员和代码的同步。 诸如CruiseControlHudson之类的CI工具可用于设置自动构建和测试环境,以运行应用程序构建脚本(使用诸如Ant或Maven之类的构建工具创建)以从SCM存储库(如CVSSubversion等)中签出代码。 ),编译域类(以及应用程序中的其他类),如果没有构建错误,则自动运行所有测试(单元和集成)。 还可以设置CI工具来通知项目团队(通过电子邮件或RSS feed)是否存在构建或测试错误。

部署方式

域模型永远不会是静态的。 随着业务需求在项目生命周期中的发展以及新项目中提出的新需求,它们会发生变化。 另外,在开发和实施领域模型时,您会不断学习和改进,并且希望将新知识应用于现有模型。

打包和部署域类时,隔离是关键。 由于域层一方面在DAO层上具有依赖关系,另一方面在Service Facade层上具有依赖关系(请参见图2中的应用程序体系结构图),因此将域类打包和部署为一个或多个模块来管理这些依赖关系非常有意义。优雅地。

尽管设计模式(例如DI,AOP和工厂)在设计时将对象之间的耦合最小化,并使应用程序模块化,但OSGi (以前称为Open Services Gateway计划)在运行时解决了模块化问题。 OSGi正在成为打包和分发企业应用程序的标准机制。 它很好地处理了模块之间的依赖关系。 我们也可以将OSGi用于域模型版本控制。

我们可以将DAO类打包在一个OSGi捆绑包(DAO捆绑包)中,将服务外观类打包在另一个捆绑包(服务捆绑包)中,因此,由于OSGi,在修改DAO或Service实现或部署应用程序的不同版本时,无需重新启动应用程序。 如果我们必须支持某些域对象的现有版本和新版本以实现向后兼容,那么我们也可以部署同一域类的两个不同版本。

为了利用OSGi功能,必须在使用应用程序对象之前(即在客户端可以为其查找之前)向OSGi平台注册应用程序对象。 这意味着我们必须使用OSGi API进行注册,但是当使用OSGi容器启动和停止服务时,我们还必须处理失败情况。 Spring Dynamic Modules框架通过允许在应用程序中导出和导入任何类型的对象而无需更改任何代码,在此方面提供了帮助。

Spring DM还提供测试类以在容器外部运行OSGi集成测试。 例如,可以使用AbstractOsgiTests直接从IDE运行集成测试。 该设置由测试基础结构处理,因此我们不必为测试编写MANIFEST.MF文件,也​​无需进行任何打包或部署。 该框架支持当前可用的大多数OSGi实现( EquinoxKnopflerfishApache Felix )。

贷款处理应用程序使用OSGi,Spring DM和Equinox容器来管理模块级别的依存关系以及域和其他模块的部署。 LoanAppDeploymentTests显示了Spring DM测试模块的使用。

样例应用设计

下面列出了贷款处理示例应用程序中使用的域类:

实体:

  • 贷款
  • 借款人
  • 承保决定
  • 资金请求

值对象:

  • 产品费率

服务:

  • 资金服务

仓库:

  • 贷款库
  • 借款人资料库
  • 资金库

图3显示了示例应用程序的域模型图。

图3.分层的应用程序域模型(单击屏幕快照以打开完整尺寸的视图。)

本文讨论的大多数DDD设计概念和技术都适用于示例应用程序。 使用诸如DI,AOP,注释,域级别安全性和持久性之类的概念。 另外,我使用了几个开源框架来帮助DDD开发和实施任务。 下面列出了这些框架:

  • 弹簧
  • 推土机
  • Spring安全
  • JAXB(用于编组和解组数据的Spring-WS)
  • 弹簧测试(用于单元和集成测试)
  • 数据库单元
  • Spring动态模块

使用Equinox和Spring DM框架将示例应用程序中的域类部署为OSGi模块。 下表显示了示例应用程序的模块包装详细信息。

表5.打包和部署详细信息

部署工件名称 模块内容 Spring配置文件
客户端/控制器 loanapp-controller.jar 控制器,客户端委托类 LoanAppContext-Controller.xml
正面 loanapp-service.jar 外观(远程)服务,服务器代理类,XSD LoanAppContext-RemoteServices.xml
loanapp-domain.jar 领域类,DAO,通用DTO LoanAppContext-Domain.xml,LoanAppContext-Persistence.xml
构架 loanapp-framework.jar 框架,实用程序,监视(JMX)类,方面 LoanAppContext-Framework.xml,LoanAppContext-Monitoring.xml,LoanApp-Aspects.xml

结论

DDD是一个强大的概念,一旦团队在DDD中接受了培训,并开始应用“领域优先,基础设施第二”的理念,DDD将会改变建模者,架构师,开发人员和测试人员看待软件的方式。 用不同背景和专业领域的不同利益相关者(来自IT和业务部门)参与领域建模,设计和实施工作,引用Eric Evans的话,“重要的是不要模糊设计哲学(DDD)之间的界线以及帮助我​​们实现这一目标的技术工具箱(OOP,DI和AOP)”。

前沿前沿

本节介绍了一些影响DDD设计和开发的新兴方法。 其中一些概念仍在发展中,很有趣的是它们将如何影响DDD。

通过合同执行的体系结构规则和设计在域模型标准和实施最佳实践的治理和策略执行中起着重要作用。 Ramnivas 谈到了使用方面来强制仅通过工厂创建存储库对象的规则。 这是领域层中容易违反的设计规则。

领域特定语言(DSL)和业务自然语言(BNL)近年来受到越来越多的关注。 可以使用这些语言在域类中表示业务逻辑。 BNL功能强大,可以用来捕获业务规范,记录业务规则以及用作可执行代码。 它们也可以用于创建测试用例,以验证系统是否按预期工作。

行为驱动开发 (BDD)是最近讨论的另一个有趣的概念。 BDD通过提供跨越业务与技术之间鸿沟的通用词汇(泛在语言),帮助将开发重点集中在交付优先级,可验证的业务价值上。 通过使用专注于系统行为方面而不是测试方面的术语,BDD试图帮助开发人员将注意力集中在TDD最成功的真正价值上。 如果实践得当,BDD可以很好地补充DDD,因为BDD概念会对领域对象的发展产生积极影响。 所有域对象都应该封装状态和行为。

事件驱动架构 (EDA)是另一个可以在域驱动设计中发挥作用的领域。 例如,用于通知域对象实例中任何状态更改的事件模型将有助于处理域对象的状态更改时需要触发的事件后处理任务。 EDA有助于封装基于事件的逻辑,以免嵌入到核心域逻辑中。 Martin Fowler记录了有关域事件设计模式的信息。

资源资源

  • 域驱动设计,解决软件核心中的复杂性,埃里克·埃文斯(Eric Evans),艾迪生·韦斯利(Addison Wesley)
  • 应用域驱动的设计和模式,Jimmy Nilsson,Addison Wesley
  • 重构为模式,Joshua Kerievsky,Addison Wesley
  • 如果没有DI和AOP,能否充分实施DDD?

示例应用程序代码可以在此处下载。

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

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

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

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

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值