「领域驱动设计」领域驱动的设计和开发最佳实践(下)

简介: 「领域驱动设计」领域驱动的设计和开发最佳实践

数据访问对象

DAO和存储库在域驱动设计中也很重要。DAO是关系数据库和应用程序之间的契约。它封装了来自web应用程序的数据库CRUD操作的细节。另一方面,存储库是一个单独的抽象,它与dao交互,并向域模型提供“业务接口”。

存储库使用域的通用语言,使用所有必要的dao,并以域所理解的语言为域模型提供数据访问服务。

DAO方法是细粒度的,更接近于数据库,而存储库方法是粗粒度的,更接近于域。另外,一个存储库类可能注入了多个DAO。存储库和DAO使域模型与处理数据访问和持久性细节分离。

域对象应该仅依赖于存储库接口。这就是为什么注入存储库而不是DAO会产生一个更干净的域模型的原因。不应该直接从客户机(服务和其他使用者类)调用DAO类。客户机应该总是调用域对象,而域对象又应该调用DAO来将数据持久化到数据存储中。

管理域对象之间的依赖关系(例如,实体及其存储库之间的依赖关系)是开发人员经常遇到的一个经典问题。此问题的通常设计解决方案是让服务或Facade类直接调用存储库,当调用存储库时,存储库将向客户端返回实体对象。这种设计最终导致了前面提到的贫血域模型,其中facade类开始积累更多的业务逻辑,域对象成为纯粹的数据载体。一个好的设计是使用DI和AOP技术将存储库和服务注入域对象。

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

持久性

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

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

缓存

当我们讨论域层的状态(数据)时,我们必须讨论缓存的方面。频繁访问的域数据(如按揭贷款处理应用程序中的产品和利率)是很好的缓存候选者。缓存可以提高性能并减少数据库服务器上的负载。服务层是缓存域状态的理想选择。像TopLink和Hibernate这样的ORM框架也提供了数据缓存。

贷款处理示例应用程序使用JBossCache框架来缓存产品和费率细节,以最小化数据库调用并提高应用程序性能。

事务管理

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

有些开发人员喜欢在DAO类中管理事务,这是一个糟糕的设计。这导致了过于细粒度的事务控制,这没有提供管理事务跨多个域对象的用例的灵活性。服务类应该处理事务;这样,即使事务跨越多个域对象,服务类也可以管理事务,因为在大多数用例中,服务类处理控制流。

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

数据传输对象

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

从DDD的角度来看,DTO还有助于维护服务层和UI层之间的分离,其中DO用于域,服务层用于表示层,DTO用于表示层。

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

当请求进入资金处理时,样例应用程序使用Dozer映射文件(XML)将FundingRequestDTO对象分割为贷款、借款人和FundingRequest实体对象。该映射还负责将来自实体的资金响应数据聚合到返回客户端的单个DTO对象中。

DDD实施框架

Spring和Real Object Oriented (ROO)、Hibernate等框架有助于设计和实现域模型。其他支持DDD实现的框架有JMatter、Naked Objects、Ruby On Rails、Grails和Spring Modules XT框架。

Spring负责实例化和连接域类(如服务、工厂和存储库)。它还使用@ configurationannotation将服务注入实体。该注释是特定于Spring的,因此实现此注入的其他选项是使用诸如Hibernate拦截器之类的东西。

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

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

开发

没有实际的实现,模型是没有用的。实现阶段应该包括尽可能多地自动化开发任务。要查看哪些任务可以自动化,让我们来看一个涉及域模型的典型用例。以下是用例中的步骤列表:

请求:

  • 客户端调用Facade类,以XML文档的形式发送数据(与XSD兼容);Facade类为UOW启动一个新的事务。
  • 对输入的数据运行验证。这些验证包括主要的(基本的/数据类型/字段级别的检查)和业务验证。如果存在任何验证错误,则提出适当的异常。
  • 将描述翻译成代码(对域友好)。
  • 使数据格式更改对域模型友好。
  • 对属性进行任何分离(例如将客户名拆分为customer实体对象中的first和last name属性)。
  • 将DTO数据分解为一个或多个域对象。
  • 持久化域对象的状态。

响应:

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

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

表3. 数据流经应用程序层


正如您所看到的,在应用程序架构中有几个层,其中相同的数据以不同的形式(DO、DTO、XML等)流动。这些包含数据和其他类(如DAO、DAOImpl和DAOTest)的大多数对象(Java或XML)本质上都是基础结构。这些具有样板代码和结构的类和XML文件非常适合用于代码生成。

代码生成

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

当我们考虑到开发一个典型的企业软件应用程序需要大量的类和配置文件时,这可能会让人难以承受。代码生成是解决这个问题的最佳方法。代码生成工具通常使用某种模板框架来定义模板或映射,代码生成器可以从这些模板或映射生成代码。Eclipse Modeling Framework (EMF)有几个子项目,可以帮助生成web应用程序项目中所需的各种构件的代码。像AndroMDA这样的模型驱动架构(Model Driven Architecture, MDA)工具使用EMF根据架构模型生成代码。

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

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

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

  • XSD
  • 域对象
  • 服务

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

  • DAO接口和实现类
  • 工厂
  • 存储库
  • 域委托(如果需要)
  • Facade(包括EJB和web服务类)
  • DTO的
  • 以上类的单元测试(包括测试类和测试数据)
  • Spring配置文件

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

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


委托层是唯一同时具有领域对象和DTO知识的层。其他层,如持久层,应该不知道DTO的。

重构

重构是在不改变应用程序的功能或行为的情况下改变或重组应用程序代码。重构可以与设计或代码相关。进行设计重构是为了不断地细化模型并重构代码以改进域模型。

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

重构应该严格遵守规则。结合使用重构、CI和单元测试来确保代码更改不会破坏任何功能,同时这些更改确实有助于预期的代码或性能改进。

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

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

Chris Richardson谈到了使用Eclipse提供的重构特性,应用代码重构将过程设计转换为面向对象设计。

单元测试/持续集成

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

像JUnit或TestNG这样的单元测试框架是实现和管理域模型的好工具。其他测试框架,如DBUnit和Unitils,也可以用来测试域层,特别是将测试数据注入到DAO类中。这将最小化为在单元测试类中填充测试数据而编写的额外代码。

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

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

Eric Evans在他的书中谈到了CI,他说CI工作应该总是在有限的上下文中应用,它应该包括人和代码的同步。CI工具比如CruiseControl和哈德逊可以用来建立一个自动构建和测试环境中运行应用程序构建脚本(使用Ant或Maven这样的构建工具创建)检出代码从SCM存储库(如CVS, Subversion等),编译域类(以及其他类的应用程序),如果没有构建错误,然后自动运行所有的测试(单元测试和集成)。如果有任何构建或测试错误,也可以设置CI工具来通知项目团队(通过电子邮件或RSS提要)。

部署

域模型从不是静态的;它们随着项目生命周期中业务需求的演进和新项目中出现的新需求而变化。另外,在开发和实现域模型时,您需要不断地学习和改进,并希望将新知识应用到现有的模型中。

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

虽然DI、AOP和工厂等设计模式在设计时最小化了对象之间的耦合并使应用程序模块化,但OSGi(以前称为开放服务网关计划)在运行时解决了模块化问题。OSGi正在成为打包和分发企业应用程序的标准机制。它很好地处理了模块之间的依赖关系。我们还可以使用OSGi进行域模型版本控制。

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

为了利用OSGi的功能,应用程序对象在被使用之前必须在OSGi平台上注册(也就是说,在客户端对它们进行查找之前)。这意味着我们必须使用OSGi api来进行注册,但是我们还必须在服务启动和停止使用OSGi容器时处理故障场景。Spring Dynamic Modules框架通过允许在应用程序中导出和导入任何类型的对象而不需要修改任何代码,在这方面提供了帮助。

Spring DM还提供了在容器外运行OSGi集成测试的测试类。例如,AbstractOsgiTests可用于直接从IDE运行集成测试。设置由测试基础结构处理,因此我们不必编写清单。MF文件进行测试,或做任何打包或部署。该框架支持当前可用的大多数OSGi实现(Equinox、Knopflerfish和Apache Felix)。

贷款处理应用程序使用OSGi、Spring DM和Equinox容器来管理模块级依赖项以及域和其他模块的部署。LoanAppDeploymentTests展示了Spring DM测试模块的使用。

示例应用程序设计

贷款处理样本申请中使用的域类如下:

实体:

  • Loan
  • Borrower
  • UnderwritingDecision
  • FundingRequest

值对象:

  • ProductRate
  • State

服务:

  • FundingService

存储库:

  • LoanRepository
  • BorrowerRepository
  • FundingRepository

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


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

本文中讨论的大多数DDD设计概念和技术都应用于示例应用程序。使用了诸如DI、AOP、注释、域级安全性和持久性等概念。此外,我还使用了几个开源框架来帮助完成DDD开发和实现任务。这些框架如下:

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

样例应用程序中的域类使用Equinox和Spring DM框架部署为一个OSGi模块。下表显示了示例应用程序的模块打包细节。

表5所示。打包和部署细节


结论

DDD是一个强大的概念,它将改变建模人员、架构师、开发人员和测试人员在团队接受了DDD培训并开始应用“领域第一,基础设施第二”的理念之后看待软件的方式。不同利益相关者(从IT和业务单位)与不同背景和领域的专业知识参与域建模、设计和实现工作,引用Eric Evans,“重要的是不要模糊的哲学之间的线路设计(DDD)和技术工具框,帮助我们完成它(OOP, DI和AOP)”。

推进前沿

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

体系结构规则和契约实施设计在域模型标准和实现最佳实践的治理和策略实施中扮演重要角色。Ramnivas谈到了使用方面来执行只通过工厂创建存储库对象的规则;这是一个容易违反设计规则在领域层。

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

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

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

资源

  • 领域驱动设计,解决软件核心的复杂性,Eric Evans, Addison Wesley
  • 应用领域驱动的设计和模式,Jimmy Nilsson, Addison Wesley
  • 《重构到模式》,Joshua Kerievsky, Addison Wesley著
  • DDD可以在没有DI和AOP的情况下充分实现吗?
相关文章
|
6月前
|
设计模式 监控 算法
【领域驱动设计专题】一文带领你透视DDD领域驱动模型的本质和设计原理分析指南(通用语言体系)
【领域驱动设计专题】一文带领你透视DDD领域驱动模型的本质和设计原理分析指南(通用语言体系)
145 2
|
6月前
|
敏捷开发 监控 架构师
【领域驱动设计专题】一文带领你透视DDD领域驱动模型的本质和设计原理分析指南(构建领域知识)
【领域驱动设计专题】一文带领你透视DDD领域驱动模型的本质和设计原理分析指南(构建领域知识)
185 0
|
4月前
|
测试技术 数据处理
领域驱动使用问题之通过领域模型来驱动软件开发,如何解决
领域驱动使用问题之通过领域模型来驱动软件开发,如何解决
|
4月前
|
前端开发 开发者
领域驱动使用问题之在图研发平台中,领域驱动设计是如何帮助解决前端开发面临的问题的
领域驱动使用问题之在图研发平台中,领域驱动设计是如何帮助解决前端开发面临的问题的
|
5月前
|
Java 文件存储
设计优雅的接口:实现最佳实践与方法论
设计优雅的接口:实现最佳实践与方法论
|
存储 XML 缓存
「领域驱动设计」领域驱动的设计和开发最佳实践
「领域驱动设计」领域驱动的设计和开发最佳实践
|
存储 设计模式 缓存
「领域驱动设计」领域驱动的设计和开发最佳实践(上)
「领域驱动设计」领域驱动的设计和开发最佳实践
|
存储 设计模式 缓存
「首席架构看领域驱动设计」领域驱动的设计和开发最佳实践(上)
「首席架构看领域驱动设计」领域驱动的设计和开发最佳实践
|
存储 XML 缓存
「首席架构看领域驱动设计」领域驱动的设计和开发最佳实践(下)
「首席架构看领域驱动设计」领域驱动的设计和开发最佳实践