java 关于java领域驱动设计引入

领域java领域驱动设计设计(DDD)的中惢内容是如何将业务领域概念映射到软件工件中大部分关于此主题的著作和文章都以Eric Evans的书《领域java领域驱动设计设计》为基础,主要从概念和设计的角度探讨领域建模和设计情况这些著作讨论实体、值对象、服务等DDD的主要内容,或者谈论、界定的上下文(Bounded Context)和防护层(Anti-Corruption Layer)這些的概念

本文旨在从实践的角度探讨领域建模和设计,涉及如何着手处理领域模型并实际地实现它我们将着眼于技术主管和架构师茬实现过程中能用到的指导方针、最佳实践、框架及工具。领域java领域驱动设计设计和开发也受一些架构、设计、实现方面的影响比如:

夲文讨论这些不同的因素在项目实施的整个生命周期中怎样对其产生影响,还有架构师在实现成功的DDD中应该去寻求什么我会先列出领域模型应该具备的典型特征,以及何时在企业中使用领域模型(相对于根本不使用领域模型或使用贫血的领域模型来说)。

文章包括一个貸款处理示例应用来演示如何将设计立场、以及这里讨论的开发最佳实践,应用在真实的领域java领域驱动设计开发项目之中示例应用用叻一些框架去实 现贷款 处理领域模型,比如Spring、Dozer、Spring Security、JAXB、Arid POJOs和Spring Dynamic Modules示例代码用Java编写,但对大多数开发人员来说不论语言背景如何,代码都是很容噫理解的

领域模型带来了一些好处,其中有:

  • 有助于团队创建一个业务部门与IT部门都能理解的通用模型并用该模型来沟通业务需求、數据实体、过程模型。
  • 模型是模块化、可扩展、易于维护的同时设计还反映了业务模型。
  • 提高了业务领域对象的可重用性和可测性

反過来,如果IT团队在开发大中型企业软件应用时不遵循领域模型方法我们看看会发生些什么。

不投放资源去建立和开发领域模型会导致應用架构出现“肥服务层”和“贫血的领域模型”,在这样的架构中外观类(通常是无状态会话Bean)开始 积聚越 来越多的业务逻辑,而领域对象则成为只有getter和setter方法的数据载体这种做法还会导致领域特定业务逻辑和规则散布于多个的外观类中(有些 情况下还会出现重复的逻輯)。

在大多数情况下贫血的领域模型没有成本效益;它们不会给公司带来超越其它公司的竞争优势,因为在这种架构里要实现业务需求变更开发并部署到生产环境中去要花费太长的时间。

在考虑DDD实现的项目中各种架构和设计因素之前让我们先看看富领域模型的特性:

  • 领域模型应该侧重于具体的业务操作领域。它应该结合业务模型、策略和业务流程
  • 它应该与业务中的其它领域,还有应用架构中的其咜层隔离开来
  • 它应该可重用,以避免相同的核心业务领域元素有任何重复的模型和实现
  • 模型应该设计得与应用中的其它层松耦合,这意味着领域层与上下两层(即数据库和外观类)都没有依赖关系
  • 它应当是一个抽象的、清晰划分的层次,以使维护、测试、版本处理更嫆易可在容器外(从IDE中)对领域类进行单元测试。
  • 它应该用POJO编程模型来设计没有任何技术或框架依赖性(我总是告诉公司里我工作的項目团队,我们软件开发用的技术是Java)
  • 领域模型应该独立于持久化实现的细节(尽管技术确实会对模型有一些限制)。
  • 它应该最小程度哋依赖于任何基础设施框架因为它将比这些框架更经久,我们也不希望与任何外部框架紧耦合

为了实现软件开发中更高的投资回报率(ROI),业务单位和IT的高级管理人员必须在业务领域建模及其实现的投资上(时间、金钱和资源)全力以赴让我们来看看实现领域模型需偠的其它因素。

  • 团队应该经常接近业务领域主题专家
  • IT团队(建模者、架构师和开发人员)应具备良好的建模、设计技能。
  • 分析师应该具囿良好的业务流程建模技能
  • 架构师和开发人员应该有丰富的面向对象设计(OOD)和编程(OOP)经验。

领域java领域驱动设计设计在企业架构中的莋用

领域建模和DDD在企业架构(EA)中发挥着重要的作用因为EA的目标之一就是结合IT和业务部门,业务实体的代表——领域模型就是EA的核心部汾这就是为什么大多数EA组件(业务或基础设施)应该围绕领域模型设计和实现的原因。

面向服务的体系架构(SOA)最近帮助团队构建基于業务流程的软件构件和服务、加速新产品上市时间的势头越来越强劲领域java领域驱动设计设计是SOA的一个关键因素,因为它有助于封装领域對象中的业务逻辑和规则领域模型也提供了定义服务契约使用的语言和上下文。

如果还没有领域模型SOA的实行就应该包括领域模型的设計和实现。如果我们太过强调SOA服务、忽略了领域模型的重要性那我们在应用架构中最终得到的就是一个贫血的领域模型和臃肿的服务。

悝想的情况是在开发应用层和SOA组件的同时,迭代地实现DDD因为应用层和SOA组件都是领域模型要素的直接消费者。使用丰富的领域实现通 過给领 域对象提供一个壳(代理),SOA设计将变得相对简单但如果我们太过于关注SOA层,在后端却没有一个像样的领域模型业务服务就会調用不完整的领域模 型,这可能会导致出现一个脆弱的SOA架构

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

  • 首先为业务流程建模并文档化。
  • 选择一个候选的业务流程与业务领域专家一起使用通用语言来文档化业务流程。
  • 识别候选业务流程需要的所有服务这些服务本质上可以是原子嘚(单步的)或组合好的(多步的,有无工作流皆可)它们也可以是业务(比如承保或资金)或基础设施(比如电子邮件或工作调度)。
  • 对上一步识别的服务所使用的对象确定并文档化其状态和行为。

一开始关注业务领域核心元素的时候就将模型保持在高水平是非常偅要的。

从项目管理的观点来看真实的DDD实现项目和其它软件开发项目所包含的阶段是一样的。这些阶段包括:

  • 基于设计和开发来完善、偅构领域模型(模型概念的持续集成(CI))
  • 使用更新的领域模型重复上述步骤(领域实现的CI)。

非常适合在这里使用敏捷软件开发方法學因为敏捷方法注重于交付商业价值,恰好DDD侧重于结合软件系统和业务模型此外,就DDD迭代的特性来 说SCRUM或DSDM这样的敏捷方法对项目管理來说也是更好的框架。结合使用SCRUM(适用于项目管理)和XP(适用于软件开发目标)方法对处理 DDD实现项目来说非常好

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

图为主营就是POCO)为基础的架构。

  • 应该支持使用DDD概念的业务领域模型的设计和实现
  • 应该支持像依赖注入(DI)和面向方向编程(AOP)这些概念的开箱即用。(注:稍后将在文章中详细解释这些概念)
  • 与单元测试框架整合,比如、、等

本文中使用的示例应用是┅个住房贷款处理系统,业务用例是批准住房贷款(抵押)的资金申请将贷款申请提交给抵押放贷公司的 时候,首先要通过承保过程承保人在这一过程中根据客户的收入详情、信用历史记录和其它因素来决定批准还是拒绝贷款请求。如果贷款申请获得承保组的批准 就進入贷款审批程序的结清和融资步骤。

贷款处理系统中的融资模块自动给贷款人支付资金通常,融资过程从抵押放贷公司(通常是银行)将贷款包递交给产权公司开始接着产权公司评估贷款包,并与房产买卖双方一起确定结清贷款的时间贷款人和卖方与结算中介在产權公司会面、签署书面协议,来转移房产产权

典型的企业应用架构由下面四个概念上的层组成:

  • 用户界面(表现层):负责给用户展示信息,并解释用户命令
  • 应用层:该层协调应用程序的活动。不包括任何业务逻辑不保存业务对象的状态,但能保存应用程序任务过程嘚状态
  • 领域层:这一层包括业务领域的信息。业务对象的状态在这里保存业务对象的持久化和它们的状态可能会委托给基础设施层。
  • 基础设施层:对其它层来说这一层是一个支持性的库。它提供层之间的信息传递实现业务对象的持久化,包含对用户界面层的支持性庫等

让我们更详细地看一下应用层和领域层。应用层:

  • 负责应用中UI屏幕之间的导航以及与其它系统应用层之间的交互。
  • 还能对用户输叺的数据进行基本(非业务相关)的验证然后再把数据传到应用的其它层(更底层)。
  • 不包含任何业务、领域相关的逻辑、或数据访问邏辑
  • 没有任何反映商业用例的状态,但却能处理用户会话或任务进展的状态
  • 负责业务领域的概念,业务用例和业务规则的相关信息領域对象封装了业务实体的状态和行为。贷款处理应用中的业务实体例子有抵押(Mortgage)、房产(Property)和贷款人(Borrower)
  • 如果用例跨越多个用户请求(比如贷款登记过程包含多个步骤:用户输入贷款详细信息,系统基于贷款特性返回产品和利率用户选择特定的产品/利率组合,最后系统会用这个利率锁定贷款)还可以管理业务用例的状态(会话)。
  • 包含服务对象这些服务对象只包含一个定义好的、不属于任何领域对象的可操作行为。服务封装了业务领域的状态而业务领域并不适用于领域对象本身。
  • 是商业应用的核心应该与应用的其它层隔离開来。而且它不应该依赖于其它层使用的应用框架(JSP/JSF、、EJB、、等)。

下面的图2显示了应用中使用的不同架构层次以及它们与DDD有怎样的關系。

图2. 多层应用架构图(点击查看大图)

下面的设计观点被认为是目前DDD实现诀窍的主要部分:

OOP是领域实现中最重要的基本原则应该利鼡像继承、封装和多态这样的OOP概念,使用Plain Java类和接口来设计领域对象大部分领域元素是既有状态(属性)又有行为(操作状态的方法或操莋)的真正对象。它们同时对应于真实世界的概念能很合 适地适用于OOP概念。DDD中的实体和值对象都是OOP概念的典型例子因为它们同时有状態和行为。

在典型的工作单元(UOW)中领域对象需要与其它的对象协作,无论这些对象是服务、资源库、还是工厂领域对象还需要处理其它那些本身就横切的关 注点, 比如领域状态变化跟踪、审计、缓存、事务管理(包括事务重试)这些都是可重用、非领域相关的关注點,通常很容易在包括领域层的整个代码中散布和重复在 领域对象中嵌入该逻辑会导致领域层和非领域相关的代码互相纠缠、产生混乱。

说到处理对象间之没有紧耦合的代码依赖关系和隔离横切关注点的时候OOP并不能独自为领域java领域驱动设计设计和开发提供极好的设计解決方案。在这是可以利用DI和AOP这样的设计概念对OOP进行补充以尽量减少紧耦合、提高模块化、更好地处理横切关注点。

DI能很有效地将配置和依赖代码从领域对象中移出此外,领域类对数据访问对象(DAO)类、服务类对领域类的设计依赖性使得DI成为DDD实现中“必须有”的内容通過将资源库和服务之类的其它对象注入到领域对象,DI有助于创建一个更清晰、松耦合的设计

EE资源也被注入到服务和资源库对象中。

通过從领域对象中移除横切关注点代码比如检查、领域状态变化跟踪等,AOP有助于实现一个更好的设计(即在领域模型中少一些乱七八糟的内嫆)可 利用 AOP把协同对象和服务注入领域对象,特别是那些容器没有实例化的对象(比如持久化对象)在可以利用AOP的领域层中,其它的方面有缓存、事务管理和基 于角色的安全(授权)

贷款处理应用利用自定义方面将数据缓存引入服务对象。贷款产品和利率信息从数据庫表中加载一次(客户端第一次请求这些信息时)然后存储到适用于后面产品和利率查找的对象缓存()中。产品和利率会被频繁访问但不会定期更新,所以缓存数据是一个很好的候选方案而不是每次都从后端的数据库获取。

在近期的讨论贴子里DDD中DI和AOP概念的作用是主要的话题。讨论以Ramnivas Laddad的演讲为基础Ramnivas在其演讲中主张, Ramnivas在这个演讲中讨论了“细粒度DI”的概念,这一概念利用AOP使领域对象恢复机敏性怹说领域对象需要访问其它细粒度的对象来提供丰富的 行为,该问题的解决方案是在领域对象中注入服务、工厂或资源库(通过在调用构慥或setter方法时期使用方面来注入依赖)

Chris Richardson也讨论了有关,通过减少耦合、提高模块化来改进应用设计Chris谈到了“超级大服务”反模式,这是應用代码耦合、混乱、分散的结果他还谈了如何利用DI和AOP的概念来避免这一反模式。

最近定义、处理方面和DI的趋势是使用注解对实现远程服务(比如EJB或Web Services)来说,注解有助于减少所需的工件它们还简化了配置管理任务。、以及其它框架都充分利用注解在Java企业应用的不同層中配置组件。

我们应该利用注解生成模板代码模板代码能在灵活性上增加价值。但同时应该谨慎使用注解注解应该用于不会引起混淆或误解实际代码的地方。使用注解 的一个 很好的例子是Hibernate ORM映射注解能直接用类或属性名给指定的SQL表或列名添加值。另一方面像JDBCjava领域驱動设计配置(java领域驱动设计类名、JDBC URL、用户名和密码)这样的详细信息则更适合于存放在XML文件中,而不是使用注解这基于数据库在同一个仩下文中这一假设。如果领域模型和数据库表之 间需要相当多的转换那就应该好好思考一下设计了。

提供JPA注解比如、、等,以此给简單的Java类添加持久化细节在领域建模上下文中,实体、资源库和服务都是使用注解的好地方

作用。Spring还提供了其它注解来帮助设计领域对潒比如、和。

示例应用中使用了部分注解实体对象(Loan、Borrower和FundingRequest)使用了@Entity注解;这些对象还使用@Configurable注解绑定资源库对象;服务类也使用@Transactional注解来鼡事务行为装饰服务方法。

领域层的应用安全确保只有授权的客户端(人类用户或其它应用)能调用领域操作访问领域状态。

(Spring Portfolio的一个孓项目)同时为应用的表现层(以URL为基础)和领域层(方法级)提供了细粒度的访问控制该框架使用Spring的Bean Proxy来拦截方法调用,运用安全约束它为使用类的Java对象提供了基于角色的声明式安全。它也有针对领域对象的访问控制列表(ACL's)形式的实例级别安全以控制实例级别的用戶访问。

在领域模型中使用Spring安全来处理授权需求的主要好处是框架有一个非侵入式的架构,我们可以完全隔离领域和安全方面此外,業务对象也不会和安全实现细节混成一团我们可以只在一个地方编写通用的安全规则,(使用AOP技术)在任何需要实现它们的地方运用它們

在领域和服务类中,授权在类方法调用级别进行处理举例来说,对于高达一百万美元的贷款承保领域对象中的“贷款审批”方法鈳以由任何具有“承保人 ”角色 的用户调用;而对于超过一百万美元的贷款申请来说,同一领域对象中的审批方法则只能由具有“核保主管”角色的用户调用

下表简要说明了应用架构每一层中应用的各种安全关注点。

表1. 各个应用层中的安全关注点

认证、Web页面(URL)界别授权
領域实例级别授权、ACL
DB对象级别授权(存储过程、存储函数、触发器)

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

  • 流程流向(工作流逻辑)

上下文在DDD世界中非瑺重要。上下文的特性决定了领域对象协作及其它运行时因素比如运用什么业务规则等。验证以及其它业务规则往往都是在一个特 定的業 务上下文中处理的这意味着,相同的领域对象在不同的业务上下文中将不得不处理不同的一组业务规则比如说,通过了贷款审批流程中的承保步骤后贷款领域 对象的一些属性(像贷款数额和利率)就不能再改变了。但在贷款刚刚登记并与特定利率关联的时候同样嘚属性是可以改变的。

尽管所有的领域特 定业务规则都应该封装在领域层但一些应用设计将规则放在了外观类中,这导致了领域类在业務规则逻辑方面变成了“贫血的”在小型应用中这可能是可接受的 解决方案,但不推荐将其用于包含复杂业务规则的中大型企业应用哽好的设计方案是把规则放在它们应该在的地方——领域对象中。如果一个业务规则 跨越两个或两个以上的实体对象那么该规则应该做為服务类的一部分。

此外如果我们不在应用中下苦功,往往把业务规则变成代码里的一串switch语句随着规则变得越来越复杂,开发人员不會愿意花费时间去重构代 码将 switch语句移到更易于管理的设计中。在类中硬编码复杂的流向或决策规则逻辑会导致类中出现更长的方法、代碼重复、最终僵化的应用设计长远来看,这 将成为维护的噩梦一个良好的设计是把所有的规则(特别是随着业务策略的变化而频繁改變的复杂规则)放到规则引擎(利用规则框架,比如、或)中去并从领域类中进行调用。

验证规则通常会用不同的语言实现比如Javascript、XML、Java玳码,还有其它脚本语言但由于业务规则的动态特性,、、(DSL) 这些脚本语言是定义、管理这些规则更好的选择Struts(应用层)、Spring(服务層)和Hibernate(ORM)都有其自己的验证模块,我 们可以在这些验证模块中对传入或传出的数据对象运用验证规则在一些情况下,验证规则还能被處理为方面它们可以组合到应用的不同层次中去(比如服务和控 制器)。

在编写领域类处理业务规则时紧记单元测试方面是非常重要嘚。规则逻辑中的任何变化都应该很容易、独立地单元可测

示例应用包括一个业务规则集来验证贷款特性是否都在允许的产品和利率规格内。规则在脚本语言中(Groovy)进行定义并用于传递给FundingService对象的贷款数据。

从设计的角度出发领域层应该有一个定义清晰的边界,以避免來自非核心领域层关注点的层的损坏比如特定供应商的说明、数据过滤、转换等。领域元素 应该设 计为正确地保存领域状态和行为不哃的领域元素会基于状态和行为进行不同的结构化。下面的表2展示了领域元素及其包含的内容

表2. 领域元素及其状态和行为

同时包含状态(数据)和行为(操作)的实体、值对象、聚合应该有定义清晰的状态和行为。同时该行为不应该超出对象边界的范围。实体应该在作鼡于本地状态的用例中完成大部分工作但它们不应该知道太多无关的概念。

对那些封装领域对象状态所需要的属性来说好的设计实践昰只包括这些属性的getter/setter方法。设计领域对象时只为那些能改变的属性提供setter方法。此外公有的构造函数应该只含有必需的属性,而不是包含领域类中所有的属性

在大部分用例中,我们并不是真的要去直接改变对象的状态所以,代替改变内部状态的做法是创建一个带有巳改变状态的新对象并返回该新对象。这种方法在这些用例中就足够了还能降低设计的复杂性。

聚合类对调用者隐藏了协作类的用法聚合类可用来封装领域类中复杂的、有侵入性的、状态依赖的需求。

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

  • 数据传输对象(DTO)
  • 资源库:资源库包含领域为中心的方法,并使用DAO与数据库交互
  • 时态模式(Temporal Patterns):这些模式给丰富的领域模型添加了时间维。基于Martin Fowler的为处理领域模型中的双时态问题提供了设计方法。核心的领域对象及其双时态属性能用ORM产品持久化比如Hibernate。

在DDD中应用的其它设计模式还包括策略模式、外观模式和工厂模式Jimmy Nilsson在他的里讨论了工厂模式,认为它是一种领域模式

在最佳实践和设計模式的反面,架构师和开发人员在实现领域模型时还应该提防一些DDD的坏气味由于这些反模式,领域层在应用架构中成为最不重要的部汾外观类反而在模型中承担了更重要的责任。下面是一些反模式:

  • 肥服务层:服务类在这里最终会包含所有的业务逻辑
  • 依恋情结(Feature Envy):这是Martin Fowler在他关于重构的中提到的典型的坏气味,在该反模式中一个类的方法对属于其它类的数据太过念念不忘。

DAO和资源库在领域java领域驱動设计设计中都很重要DAO是关系型数据库和应用之间的契约。它封装了Web应用中的数据库CRUD操作细节另一方面,资源库是一个独立的抽象咜与DAO进行交互,并提供到领域模型的“业务接口”

资源库使用领域的通用语言,处理所有必要的DAO并使用领域理解的语言提供对领域模型的数据访问服务。

DAO方法是细粒度的更接近数据库,而资源库方法的粒度粗一些而且更接近领域。此外一个资源库类中能注入多个DAO。资源库和DAO能防止解耦的领域模型去处理数据访问和持久化细节

领域对象应该只依赖于资源库接口。这就是为什么是注入资源库、而不昰DAO会产生一个更规则的领域模型的原因DAO类不能由客户端(服务和其它的消费者类)直接调用。客户端应该始终调用领域对象领域对象洅调用DAO将数据持久化到数据存储中。

处理领域对象之间的依赖关系(比如实体及其资源库之间的依赖关系)是开发人员经常遇到的典型问題解决这个问题通常的设计方案是让服务类或外观类直 接调用 资源库,在调用资源库的时候返回实体对象给客户端该设计最终导致前媔提到的贫血领域模型,其中外观类会开始堆积更多的业务逻辑而领域对象则成为单纯的 数据载体。好的设计是利用DI和AOP技术将资源库和垺务注入到领域对象中去

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

持久化是一个基础设施方面领域层应该与其解耦。JPA通过对类隐藏持久化实现的细节提供了这一抽象。它由注解推动所以不需要XML映射文件。但同时表名和列名嵌在代码中,在某些情況下可能并不是一个灵活的解决办法

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

在我们讨论领域层的状态(数据)时我们不得不谈到缓存问題。经常访问的领域数据(比如抵押贷款处理应用中的产品和利率)很值得缓存起来缓存能提高性能,减少数据库服务器的负载服务層很适合缓存领域状态。和这些ORM框架也提供数据缓存

贷款处理示例应用使用JBossCache框架来缓存产品和利率详情,以减少数据库调用、提高应用性能

对保持数据完整性、整体提交或回滚UOW(工作单元模式)来说,事务管理是很重要的应该在应用架构层的哪里处理事务一直存在争議。交叉实体的事务(在同一UOW中跨越多个领域对象)也影响在哪里处理事务这一设计决策

一些开发人员倾向于在DAO类中管理事务,这是一個欠佳的设计该设计导致过细粒度的事务控制,对那些事务跨越多个领域对象的用例来说这种事务控 制没有 灵活性。服务类应该处理倳务;即使事务跨越多个领域对象服务类也能处理事务,因为在大多数用例中是服务类在处理控制流。

示例应用中的FundingServiceImpl类处理资金申请嘚事务通过调用资源库执行多个数据库操作,并在单一事务中提交或回滚所有的数据库变化

领域对象模型在结构上与从业务服务接收戓发送的消息不兼容,在这样一种SOA环境中DTO就是设计中很重要的一部分。消息通常都在XML模式定义 文档 (XSD)中定义和维护从XSD编写(或代码苼成)DTO对象,并在领域和SOA服务层之间使用它们来传输数据(消息)是一种普遍的做法在分布式应用 中,将来自于一个或多个领域对象中嘚数据映射到DTO中会成为必然的弊端因为从性能和安全角度出发,跨越网络发送领域对象是不实际的

从DDD的角度来看,DTO还有利于维护服务層和UI层之间的缝隙其中DO用于领域层和服务层,DTO用于表现层

框架用于将一或多个领域对象组装为一个DTO对象。它是双向的将领域对象转換为DTO的时候,它会保存大量备用的代码和时限反之亦然。DO和DTO之间的双向映射有利于消除“DO到DTO”和“DTO到DO”各自的转换逻辑该框架还能正確处理类型和数组的转换。

示例应用在资金处理申请到来时利用Dozer映射文件(XML)将FundingRequestDTO对象划分成为Loan、Borrower、 FundingRequest实体对象。在返回给客户端时映射哃样负责将来自实体的资金响应数据聚合到单一的DTO对象中。

Spring负责实例化并将服务、工厂和资源库这些领域类联接在一起。它还使用@Configurable注解將服务注入实体该注解是Spring特有的,所以完成这一注入的其它选择是使用诸如Hibernate拦截器的东西

ROO是建立在观点“领域第一,基础设施第二”の上的DDD实现框架开发该框架是为了减少Web应用开发中模式的模板编码。利用ROO时我们定义领域模型,接着框架(基于 Archetypes)为模型-视图-控制器(MVC)、DTO、业务层外观和DAO层生成代码它也能为单元测试和集成测试生成stubs。 

ROO有几个非常实用的实现模式比如说,它区分处理属性的状态、使用属性级访问的持久层、只反映必需属性的公有构造函数

没有实际的实现,模型就没有用处实现阶段应该尽可能多地自动化完成开發任务。为了看看什么任务能自动完成让我们看看涉及领域模型的一个典型用例。下面是用例的步骤列表:

  • 客户端调用外观类以XML文档(XSD兼容的)的方式发送数据;外观类为UOW初始化一个新的事务。
  • 验证输入的数据验证包括基本验证(基本的/数据类型/属性级检查)和业务驗证。如果有任何的验证错误抛出适当的异常。
  • 将描述转换为代码(以成为简单的领域)
  • 改变数据格式,以成为简单的领域模型
  • 进荇所有的属性分割(比如,在客户实体对象中将客户姓名分成名字和姓)。
  • 把DTO拆分为一或多个领域对象
  • 持久化领域对象的状态。
  • 从数據存储中获取领域对象的状态
  • 将领域对象组装为对应用有利的数据对象(DTO)。
  • 进行所有的数据元素合并或分离(比如结合名字和姓组荿单一的客户姓名属性)。
  • 必要时改变数据格式以处理客户端数据使用的要求。
  • 如果有必要缓存DTO的状态。
  • 事务提交(如果有错误则回滾)退出控制流。

下表显示了应用中不同的对象这些对象将一个层的数据传到另一个层。

表3. 应用层间的数据流向

正如你所看到的相哃的数据以不同形式(DO、DTO、XML等)在应用架构中传递的层并不多。大部分持有数据的这些对象(Java或XML)还 有像 DAO、DAOImpl、DAOTest这些类实际上都是基础设施。这些有模板代码和结构的类、XML文件都很适合代码生成

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

典型的企业软件应用所需的种种类和配置文件时其数量之多令人望而生畏。代码生成是解决该问题的最好办法代码生成工具通瑺使用某类模板框架来定义模板,或是代码生成器能从中生成代码的映射(EMF)的几个子项目有助于Web应用项目需要的各种工件的代码生成。模型java领域驱动设计架构(MDA)工具比如AndroMDA,都利用EMF在架构模型的基础上生成代码

说到在领域层编写委托类,我看到开发人员手动编写这些类(大多是从无到有地写完第一个接着用“复制并粘贴”的模式来为其它的领域对象创建所需的委 托 类)。由于这些类大部分都是领域类的外观它们很适合代码生成。代码生成是长远的解决办法尽管建立并测试代码生成器(引擎)增加了初期的投入(代码量和 时间)。

对生成的测试类来说一个好的选择就是在需要进行单元测试的主类中,为带有复杂业务逻辑的方法创建抽象方法这样,开发人员能继承生成的测试基类然后实现不能自动生成的自定义业务逻辑。同样这个方法也适用于任何有不能自动创建测试逻辑的测试方法。

對编写代码生成器来说脚本语言是一个更好的选择,因为它们开销少还支持模板创建和自定义选项。如果我们在DDD项目中充分利用代码苼成我们只需要从无到有地编写少量的代码。必须从无到有进行创建的工件有:

一旦我们定义了XSD和Java类我们可以生成下列全部或大部分嘚类和配置文件:

  • 领域代理(如果有必要)
  • 上述类的单元测试(包括测试类和测试数据)

表4列出了Web应用架构中不同的层,以及那些层中能苼成什么工件(Java类或XML文件)

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

DO到DTO的转换代码

委托层是唯一同时理解领域对象和DTO的层。其它层例如持久层,不应該察觉到DTO

重构就是改变或调整应用代码,但不修改应用的功能或行为重构可以是设计相关的,也可以是代码相关的设计重构是为了鈈断完善模型、重构代码来提升领域模型。

由于重构的迭代性和领域建模不断演进的性质重构在DDD项目中发挥着重要作用。将重构任务集荿到项目中的方法之一是在项目的每次迭代中添加重构环节重构结束之后才算完成迭代。理想情况下每项开发任务之前和之后都应该進行重构。

进行重构应该有严格的规定结合使用重构、CI和单元测试,以确保代码变化不会破坏任何功能同时,代码的变化要有助于以後的代码和性能改进

自动化测试在重构应用代码中发挥着至关重要的作用。没有良好的自动化测试和(TDD)实践重构可能会产生反面的效果,因为没有自动化的方式去验证作为重构一部分的设计和代码并变化没有改变行为、或破坏功能

像这 样的工具有助于用迭代的方式囷作为开发一部分的重构来实现领域模型。Eclipse有一些功能比如把一个方法提取或移动到不同的类中,或将一个方法下推 到子类中也有几個Eclipse代码分析插件有助于处理代码依赖关系、识别DDD反模式。我做项目的设计和代码审查时都是依靠插件、和来评估应用中领域和其它模块嘚质量。

Chris Richardson谈到以使用Eclipse提供的重构功能将过程设计转变为一个OO设计。

我们刚才谈到的目标之一是领域类应该(在最初的开发阶段以及随後重构已有代码时)单元可测,而不过多依赖于容器或其它基础设施代码TDD方法有 助于团 队尽早地找出任何设计问题,并有助于验证代码與领域模型在保持一致DDD对测试先行开发来说是很理想的,因为状态和行为都包含在领域类中而且单独测试 它们应该是容易的。测试领域模型的状态和行为又不太过关注于数据访问或持久化的实现细节是很重要的。

单元测试框架比如JUnit或TestNG,都是实现和处理领域模型很棒嘚工具其它测试框架,像和Unitils也可用来测试领域层,尤其是把测试数据注入到DAO类中对在单元测试类中增加测试数据来说,这将大大减尐编写额外的代码

模拟对象(Mock objects)同样有利于单独测试领域对象。但是在领域层不要滥用模拟对象是很重要的如果有其他测试领域类的簡单方法,你应该使用这些方法来代替使用 模拟对象比如说,如果你能使用真实的后端DAO类(而不是模拟的DAO实现)和内存HSQL数据库(而不是嫃实的数据库)测试一个实体类能使领域层单 元测试运行得更快,而运行得更快正好是使用模拟对象潜在的主要想法这样,你将能测試领域对象之间的协作(交互)以及它们之间交换的状态(数据)。使用 模拟对象我们则只能测试领域对象之间的交互。

一旦开发任務完成所有在开发阶段创建的单元测试和集成测试(不管有没有使用TDD做法)都将成为自动化测试套件的一部分。这些测试用应该经常进荇维护并经常在本地或更高一级的开发环境中执行,以便找出新的代码变化是否在领域类中引入了Bug

Evans在他的中提到了CI,他说CI应该始终运鼡在界定的上下文中应该包括人和代码的同步。像和这些CI工具可用来建立一个自动化构建和测试的环境来运行应用构建脚本(使用或Maven這些构建工具创建)从SCM仓库中(像、等)检出代码,编译领域类(以及应用中的其它类)并在没有构建错误的情况下自动运行所有的测試(单元测试和集成测试)。CI工具还可以设置在有任何构建或测试错误时(通过E-mail或RSS Feeds)通知项目团队

领域模型绝对不会是静态的;在项目苼命周期中,它们会随着业务需求的演变、新项目中新需求的提出而发生变化此外,随着你开发和实现领域模型你能不断学习和提高,而且你也想在已有的模型中运用新的知识

打包、部署领域类的时候,隔离很关键因为领域层依赖于DAO层的一面,而服务外观层又依赖於DAO层的另一面(参见图2-应用架构图)所以这些领域类打包、部署为一或多个模块来处理依赖关系很有意义。

DI、AOP和工厂这些设计模式在设計阶段减少了对象之间的耦合并使应用模块化;(以前被称为开放服务网关规范)则在运行时处理模块化。OSGi正在成为打包、发布企业应鼡的标准机制它能很好地处理模块之间的依赖关系。我们还能用OSGi来进行领域模型的版本处理

我们可以把DAO类打包到一个OSGi的Bundle(DAO Bundle)中,把服務外观类打包到另一个Bundle(服务Bundle)中所以DAO或服务实现进行了修改,或是部署了应用的不同版本由于 OSGi,应用都不需要重启如果我们为了姠后兼容,必须支持某些领域对象已有的版本和新的版本那我们也可以部署相同领域类的两个不同版本。

为了利用OSGi的能力应用对象在消费之前(即在客户端能查找到它们之前),应该在OSGi平台中进行注册这意味着我们必须使用OSGi的API进行注册,我们还必须处理使用OSGi容器启动囷通知服务时的失败场景框架对该领域很有利,它允许在应用中导出或导入任何对象类型而不改变任何代码。

Spring DM还提供测试类以在容器外运行OSGi集成测试。比如说能从IDE中直接用运行集成测试。设置由测试基础设施来处理所以我们不需要为测试编写MANIFEST.MF文件,或者进行任何嘚打包或部署该框架支持大部分目前可用的OSGi实现(、和)。

在贷款处理示例应用中用到的领域类列举如下:

图3展示了示例应用的领域模型图

图3. 分层应用领域模型(点击查看大图)

在本文中讨论的大部分DDD设计概念和技术都在示例应用中进行了运用。像DI、AOP、注解、领域级别咹全、持久化这些概念都用到了另外,我还使用了几个开源框架来助力DDD开发和实现任务这些框架列举如下:

  • JAXB(用于封送处理和取消封送处理数据的Spring-WS)

示例应用中的领域类利用Equinox和Spring DM框架部署为OSGi模块。下表显示了示例应用的模块打包细节

表5. 打包、部署细节

外观(远程)服务,服务代理类XSD
领域类、DAO,通用的DTO
框架实用工具,监视(JMX)类方面

DDD是一个功能强大的概念,只要团队接受了DDD的培训并开始运用“领域第一,基础设施第二”的观点它就会改变建模者、架构师、开发人员和测 试人员 思考软件的方式。由于领域建模、设计和实现中会涉忣具有不同背景和专长领域的不同利益相关方(来自IT和业务单位)引用Eric Evans的说法,“不要弄混设计观点(DDD)和有助于我们完成它的技术工具箱(OOP、DI、AOP)之间的界限”

本节涵盖了一些新出现的、影响DDD设计和开发的方法。这些概念中的一些仍在不断发展观察它们将如何影响DDD吔很有意思。

在领域模型标准的治理、策略实施以及实现的最佳实践中,实施Architecture Rules和契约式设计起到了重要作用Ramnivas利用Aspects来强制仅通过工厂创建资源库对象;这是在设计领域层时经常被违背的规则。

领域特定语言(DSL)和业务自然语言(BNL)近几年来正得到越来越多的关注人们可鉯在领域类中使用这些语言表达业务逻辑。BNL可以用来保存 业务规 范记录业务规则,还能作为可执行代码从这种意义上来说,BNL是非常强夶的还能用它们创建测试用例,来验证系统是否如预期的那样运转

(BDD) 是最近被讨论的另一个有趣概念。通过提供跨越业务和技术之間鸿沟的通用词汇(通用语言)BDD有利于将开发集中在有优先次序、可验证的商业价值的发布 上。通过利用侧重于系统行为方面的术语洏不是单单着眼于测试,BDD引导开发人员将TDD背后的真正价值最大程度地发挥出来如果正确实践的话,BDD 可以成为DDD很好的补充BDD概念会对领域對象的开发产生积极的影响;毕竟领域对象就是对状态和行为的封装。

(EDA) 是能在领域java领域驱动设计设计中发挥作用的另一个领域比如說,在领域对象实例中通知任何状态变化的事件模型将有助于处理后事件(post-event)处理任务 在领域对象的状态改变时,后事件处理任务就需偠被触发EDA有利于封装基于事件的逻辑,将之嵌进领域逻辑的核心Martin Fowler评述了设计模式。

  • 领域java领域驱动设计设计:软件核心复杂性应对之道》Evans Eric著,Addison-Wesley出版社

示例应用的代码可以下载

}

犹记得刚刚参加工作时是地图廠商四维图新集团旗下的一家子公司,主要从事规划测绘相关软件研发的公司当时我的项目是为勘测设计院提供相对应的应用软件,对哋理信息和规划相关的图纸信息几乎已经专业水平。事实上规划设计大概和软件设计类似,有规划的设计、或无规划的设计造成的結果几乎是天壤之别。

我们或许很容易就能设想到一个毫无规划设计的城市纵横交错的路网、杂乱无章式的建筑布局、各种凌乱的棚户區设计,恰好象征着软件设计的无序性也恰好体现了软件企业在经费不足、组织缺乏管理、开发者能力不足、软件随时随地想改就改时嘚行业现状,只能说这样的软件是最能符合当时实际劳动生产力水平的产品

如图一所示,巴西棚户区层层叠叠、风格迥异、密密麻麻,如果作为一个外人贸然来到这样的地方大概很容易迷失期间、更不用说充斥在棚户区的各类毒品和黑社会。杂乱无章的建筑和街区僦像代码中错综复杂的调用链;而借助贫民区搞事的黑社会就像是代码中的异味或者bug,表面上看起来如此平静、与世无争、但是你永远也鈈知道啥时候会来一冷枪

不要以为离我们很远,我们其实轻易就能写出这样的软件工程项目不一定是“大泥球”系统,也有可能只是┅些看似简单的业务系统但内部代码逻辑,可能会复杂到令人窒息的程度也许那个时候有个别开发者也许会试图靠自己的能力来改变局面,但是往往也会碍于屎山太大难以下咽。

大概只有最顶级的规划设计师、耗费足够多的资源才能将这样的软件系统进行整改。然洏即便如此,如果以后没有持续维护的手段、更好的设计、仅靠老

或个别架构师、盲目相信将单体服务拆分成微服务几乎不太可能实現软件未来的可持续发展。

一个良好的软件产品的一生、或许其实是一家企业一生的真实写照

在特定组织架构下,缺乏技术基因的组织囿时候期待技术变革却会开启新的泥坑。而那些渴望靠技术改变一切的技术专家虽然拥有某些大厂微服务式架构、以及架构改造的经驗,他们也试图通过自己的努力为企业业务腾飞助力。而在他们过去的经验中往往相信组织遇到的问题,用微服务一定能解决问题嘫后大肆扩招,一年内从几个人的规模、扩招到数百人的规模将原来的系统从单体服务、改良成为微服务。但是靠单枪匹马根本无力拯救大势没有更好的业务拆分策略,就只能按照

的表名关系实现了最简单的拆分架构改造并非每次都会百试百灵,有时甚至连原来的需求都包不住毕竟只能看到用户界面层外观上的表面逻辑,而隐藏在业务中的那数十万行代码哪怕包含了企业最有价值的经验财富,也甴于代码过于混乱最终抛弃在源代码管理器中,堪称化神奇为腐朽

老系统改造也好、新系统开发也好,毫无疑问我们最容易相信的其实是老程序员经验,而程序员们掌控系统的方式就是靠数据库建模来java领域驱动设计软件开发的古老模式,而且几乎都是面向过程式的玳码这些代码的流程几乎一模一样,只需简单的按照步骤一步步套模式,轻易就能学会

1、查看用户界面,定义需要绑定到界面的模型和层级结构

2、设计数据库,不管什么类型的项目先根据客户提供的业务表单、将其转化成实体关系(ER图)、然后建立对应的代码模型。有可能使用专业软件设计ER图也有可能会使用Navicat软件设计ER图。

3、设计接口然后把数据拼凑成用户界面层所需的对象。

4、代码层次结构為传统的三层架构严格按照用户界面层、业务逻辑层、数据访问层进行设计,有时候会引入依赖注入框架实现不同层次间的解耦。

但昰有时候程序员不会严格区分需要编写的代码究竟是属于哪个层次应该囊括的内容。于是毫无疑问如果代码是为了实现用户界面上某些数据绑定操作,代码就往用户界面层写;或者代码是为了实现从数据库中抽取某些复杂数据、并构造成满足用户表现层逻辑的查询对象那么就可以看到数据访问层代码中那些臃肿的SQL语句或查询方法。

正如“罗马不是一天建成的”屎山也同样如此。这样的写法在代码刚剛编写之初并没有问题只是随着业务变化、时间的积累、程序员的水平、方法重构、新技术新组件的引入,代码将成为屎山

这时,高級程序员们的价值就在于他如何能够在屎山中快速找到bug、并解决问题的能力,这大概是一种不能复用、不可再生的能力因为永远有让囚看不懂的垃圾代码,而且每家企业都有自己的特点不同企业间往往不能循环利用。我一位朋友经常吐槽他感觉自己的价值就是守住公司那份拥有8年历史的古老代码,以便其他程序员在进行代码修改时不会引发莫名其妙的bug让系统无法运转。

在现代软件工程学的教科书Φ都会指出面向对象是解决软件复杂性的方法,但实际上掌握这种方法的开发者并不多由于开发者普遍缺乏抽象化思维,所以面向数據库、面向过程式的编程习惯能够成为业界主流并非时代的倒退,而仅仅只是在短期效率和长期维护性上被迫做出的艰难选择。

假设峩们设计出的符合三层架构的系统结构图简化后如下图所示:

这种数据库建模的开发流程中的输出成果:

1、会定义两种对象,分别是是媔向UI层的模型(DTO)和数据实体(Entity)在领域java领域驱动设计设计中,将这两种称为所谓贫血模型贫血模型,只有赋值器Set和取值器Get(在Java里面會使用POJO 这个名词来定义)。贫血模型是为了作为保存状态或传递对象而存在他并非按照实际用例场景对某类具体事务的抽象、也没有与對象相关的行为。

2、定义数据访问层来实现数据的持久化、或者从持久层实现数据的创建过程数据访问层存在的目的是为了构建上述贫血模型对象,这种访问机制被成为“事务脚本”事务脚本与对象行为割裂,而且容易导致异味产生

3、与用户行为相关的操作割裂的存放在不同层。有的可能放在用户界面层、有的可能放在数据访问层、有的可能放在业务逻辑层造成了领域知识的丢失。

4、用户界面层使鼡接口作为外观或者一种行为、开发者会使用自己独立的风格习惯来定义这种行为就容易造成术语和规则不统一,也会为后期产品的维護迭代造成问题

5、现在的软件设计,往往要求输出一份高保真的原型图、也会按照敏捷项目管理的流程对这份原型图建立持续更新的机淛确保原型图是需求的具体表达,但是产品语言并非统一语言也许产品语言具有业务含义,但是由于不能指导开发者进行接口、类、歭久层的设计造成了代码与需求的割裂。在张逸老师的《领域java领域驱动设计战术实践》提到他曾经使用dimension和metric两种不同的对象来定义一个维喥对象为代码造成了不必要的麻烦。我也曾经在一个项目遇到过产品术语未能澄清,导致开发中使用style和theme两种截然不同的定义来定义与“风格”相关术语为代码引入了不必要的纠结。

领域java领域驱动设计设计引入了以下概念但是我们无需在这篇文章中深刻理解这些概念嘚具体含义,我们只需知道有这个东西。当我们开始按照领域java领域驱动设计设计的方法设计一个系统时按照前人整理的领域java领域驱动設计的sample,往往就会将概念融汇贯通达到更好的理解效果。

1、统一语言:定义好产品原型需要建立统一语言。这是一种在内部和外部都能使用的规范化用语包括UML、适当的图、一致性的描述、以及专业术语和术语对应的英文描述。

2、实体:在领域中可以通过标识进行唯一徝定位的对象

3、值对象:在领域中,从其他领域或某个实体中分离出只包含某些特定属性的对象由于不具备唯一性特征,往往无需用於数据持久化

4、聚合、聚合根:将具有相关性的对象聚合在一起,并以聚合根的形式统一对外提供访问方法和属性字段成员

5、限界上丅文:领域包含核心领域、子域和通用子域,而限界上下文则是一个具体业务的流程每个限界上下文独立于其他限界上下文而存在,独竝演进、功能完备限界上下文的识别充满技术含量。

6、领域服务:包括仓储服务和工厂服务前者负责实现对象与数据库的操作过程、葑装了一系列数据库操作的方法;后者则侧重于对象的创建过程。个人认为从三层架构演进到领域java领域驱动设计架构过程中仓储服务是朂接近于数据访问层的逻辑,也是让大部分领域java领域驱动设计架构最终又回归到三层架构的一种通病从对数据访问层中抽出对象、行为、数据访问,是战术设计的关键步骤

领域java领域驱动设计设计引入了一堆新的架构形式,包括经典的四层架构、EDA(事件java领域驱动设计架构)、CQRS架构(命令查询职责分离)而由于Evans的原书没有过分讨论如何识别领域,后来又有许多大佬在他的基础上进行了完善提出了许多方法,包括名词、形容词、动词建模法、事件风暴、四色建模等方法限于篇幅,且听下回分解

领域java领域驱动设计设计,或许是解决这些問题的一剂良方但也或许是开启了暗黑世界的大门。

概念晦涩难懂、程序员们不愿意开始思维变革、技术上可能存在不预期的坑、都可能让新方法的实践陷入一滩烂泥还有许多人以为自己看懂了领域java领域驱动设计设计(包括笔者),在往项目中运用时总是有意无意的會被过程式代码的思维定式控制,让架构回退到三层架构

由于微服务架构的兴起,让复杂系统的开发维护成为大家普遍关心的问题使嘚Eric Evans于十五年前提出的这套理论,在今天绽放出了新的光芒当然领域java领域驱动设计设计仅仅只是众多面向对象编程的一种实践,通过领域java領域驱动设计设计将UML等方法灵活的运用其中通过打破原有数据库关系建模给代码造成的桎梏,让开发者能够真正的实现面向对象编程

嘫而思维模式的转换并非易事,从过程式代码中抽离出与对象有关的行为,远比理解这几个概念要复杂这需要大量经验的积累。

毋庸置疑数据库建模java领域驱动设计软件开发具有速度快、学习成本低的显著特点,在许多项目中能在短期内可以给开发者带来许多便利;洏应用领域java领域驱动设计设计,则可以在更长的维护周期内给软件维护带来实质性好处。

两种不同类型的开发模式根据企业实际出发進行选择,还只是开始但能真正运用好领域java领域驱动设计设计或者UML、面向对象设计这种软件工程的美学思维来改造我们的系统,让系统綻放出更加璀璨的光芒这才是软件设计的乐趣所在。

}

我要回帖

更多关于 java领域驱动设计 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信