回复: 领域驱动设计和开发实战


架构

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

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

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

  • 负责应用中UI屏幕之间的导航,以及与其它系统应用层之间的交互。
  • 还能对用户输入的数据进行基本(非业务相关)的验证,然后再把数据传到应用的其它层(更底层)。
  • 不包含任何业务、领域相关的逻辑、或数据访问逻辑。
  • 没有任何反映商业用例的状态,但却能处理用户会话或任务进展的状态。

领域层:
  • 负责业务领域的概念,业务用例和业务规则的相关信息。领域对象封装了业务实体的状态和行为。贷款处理应用中的业务实体例子有抵押(Mortgage)、房产(Property)和贷款人(Borrower)。
  • 如果用例跨越多个用户请求(比如贷款登记过程包含多个步骤:用户输入贷款详细信息,系统基于贷款特性返回产品和利率,用户选择特定的产品/利率组合,最后系统会用这个利率锁定贷款),还可以管理业务用例的状态(会话)。
  • 包含服务对象,这些服务对象只包含一个定义好的、不属于任何领域对象的可操作行为。服务封装了业务领域的状态,而业务领域并不适用于领域对象本身。
  • 是商业应用的核心,应该与应用的其它层隔离开来。而且,它不应该依赖于其它层使用的应用框架(JSP/JSF、Struts、EJB、HibernateXMLBeans等)。

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

附件: DDDImplementationCycleDiagram_lg.gif
图2. 多层应用架构图

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

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

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

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

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

依赖注入

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

在示例应用中,服务对象(FundingServiceImpl)利用DI注入实体对象(Loan、Borrower和FundingRequest)。实体也通过DI引用资源库。同样的,像数据源、Hibernate会话工厂事务管理器这些其它的Java EE资源也被注入到服务和资源库对象中。

面向方面编程

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

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

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

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

注解

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

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

Java EE 5提供JPA注解,比如@Entity@PersistenceUnit@PersistenceContext等,以此给简单的Java类添加持久化细节。在领域建模上下文中,实体、资源库和服务都是使用注解的好地方。
@Configurable是 Spring将资源库和服务注入领域对象的方式。Spring框架在@Configurable注解之上扩展了“领域对象依赖注入”思想。 Ramnivas最近在博客中谈论了即将发布的Spring 2.5.2版本(从项目的Snapshot Build 379开始可用)的最新改进。 有三个新的方面(AnnotationBeanConfigurerAspect、 AbstractInterfaceDrivenDependencyInjectionAspect和 AbstractDependencyInjectionAspect)为领域对象依赖注入提供了简单、更灵活的选择。Ramnivas说,引入中间的方 面(AbstractInterfaceDrivenDependencyInjectionAspect),其主要原因是要让领域特定的注解和接口发挥 作用。Spring还提供了其它注解来帮助设计领域对象,比如@Repository@Service@Transactional

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

领域模型和安全

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

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

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

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

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

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

安全关注点
客户端/控制器认证、Web页面(URL)界别授权
外观基于角色的授权
领域领域实例级别授权、ACL
数据库DB对象级别授权(存储过程、存储函数、触发器)

业务规则

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

  • 数据验证
  • 数据转换
  • 商业决策
  • 流程流向(工作流逻辑)

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

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

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

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

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

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