浅谈领域驱动设计

原文http://www.blogjava.net/AndersLin/archive/2009/05/07/269496.html

浅谈领域驱动设计

作者:anders小明

2009年5月7日

需求背景

现在的样子

如PoEAA中提到的:事务脚本(TransactionScript)和表模型(TableModuel)模式。

存在问题

事务脚本看到的是零散的数据,而表模型混合了下文要说领域模型和领域服务界限。

1.两者都导致了分析和设计的割裂,领域模型只存在于分析中;数据间的内在关系无法通过代码体现;

2.两者都无法有效的实现业务逻辑的具体的差异化和抽象的一致性;导致学习和维护成本的增加;

3.两者都带来了测试上困难,增加了开发成本;

为何要面向对象?

1.面向对象是自上而下的开发方法;这种方式对于迭代增进式的结构化过程来说成本是最低的;

2.数据和业务逻辑的绑定性,采用面向对象容易操作和沟通(即实施成本低),同时有助于区分状态逻辑和任务逻辑;

3.业务逻辑的差异性,同一个业务逻辑针对差异性数据有区分做法,采用面向对象容易维护;

什么是领域驱动设计

领域驱动设计的提出是由EricEvans在其《领域驱动设计》一书提出。实质上是一种由内而外的设计方法,俗话说的先中间(模型和服务)后两边(界面,数据库以及集成)。

领域驱动设计的优势

传统的开发方式:基于数据库的设计开发。数据库提供的设计模型是表和字段两种粒度,这两种粒度有时并不合适于系统设计:

1.模型的结构化能力

1.1.同一组件下的设计优势,一个模型可以来自多张表的数据聚合而成,一张表可以聚合多个模型;一个逻辑是由几个固定字段或者非固定字段聚合;模型间的关联关系也是使用表无法展示的——外键的约束对于系统开发来说实在太有限了。而这些不论表还是字段粒度都无法支持的。

这里强调一下模型间的关联关系,特别是和生命周期相关的聚合和组合关系。关系数据库中所保存的是系统分解后的表示,即关系被分解了而不是被表达了,外键对数据关系只起到一种约束作用,它对于查询语句的构建并没有直接的影响。所有数据之间的关系都必须在查询的时候明确指定出来,即调用者必须拥有数据关系的知识,而不是数据本身拥有这些知识.在如下的SQL语句中

“select*froma,bwherea.fldA=b.fldBanda.fldC=1andb.fldD=2”

其中,a.fldA=b.fldB可以称为关联条件,而a.fldC=1可以称作是坐标条件。

SQL的复杂性很大程度上来源于我们频繁的需要在各处指定完全一样的关联条件而无法把它们抽象成可复用的组分.在ORM所提供的对象空间中,对象之间的两两关联只要指定一次,就可以在增删改查等各种操作过程中起到作用,特别是在对象查询语句中,可以通过两两关联自动推导出多实体之间的关联关系,虽然推导出的结果未必是最优化的.

1.2.采用模型方式容易解决项目的集成问题(两个组件访问同一张表的情况)

2.架构的结构化能力

事务脚本直接访问到表。换句话说,其架构只有服务+数据库表,这样的架构下数据库表可以说就是我们的模型。

这里看看在逻辑设计上,领域模型驱动设计对于架构影响

A.服务,模型和模型仓库(Repository),模型的重建和关联交由模型仓库完成,单个数据逻辑交给模型处理(支持泛化);

B.测试的好处;

3.分析和设计的统一

沟通的问题,客户关注于其业务,分析的模型接近于客户需求,设计也采用模型的方式,避免分析和设计的割裂。有助于开发人员间,开发人员和业务人员以及客户间的沟通

4.其它好处

4.1.采用领域模型可以屏蔽持久化信息。持久化设计的表设计是和DBA相关的,DBA对于表设计有权力的。采用模型可以有效隔离各自工作;

4.2.模型可以通过很多的手段透明的解决性能问题,而采用表做模型导致容易导致性能问题,当然不是没有办法解决,一种是通过DataSet的方式,但这样的切换成本较高。

领域驱动设计的可能

上下文环境

领域模型存在于系统的各个地方,不过在不同地方有不同的映射实现。在通常的开发过程中,该映射存在于文档和开发人员的脑海中的。当要向客户展示时,就面临一个映射的关系。比如要允许客户可以在线编辑页面呈现的显示元素,在规则定义里使用对象系统时。

基础平台

随着ORM框架的发展,如hibernate,可以提供继承多态等能力,能够支持关联关系,特别是面向对象设计中生命周期相关的聚合关系,使得基于领域模型开发在技术上具备了可行性。

当前不足

不过ORM在集合上的处理不尽如人意。受限于JDK的集合,不能像DLinq那样提供集合的过滤和集合函数操作。使得一些设计,不得不屈从于性能问题,把领域模型的关联关系人为断开。

Ferrum项目或许会是一个可行的方案。

领域驱动设计的技术分析

概述

整体结构

领域分两个含义:领域模型,领域服务,业务规则和Repository;其中模型的持久化,重建和关联交由Repository完成;而单数据逻辑(依赖自身数据信息以及关联数据)则归于模型(支持泛化);服务则关注于任务处理,包括了多个模型处理,以及其它服务的调用。

如果把一个系统看作是一个机械组件的话,那么领域模型就相当于人的骨架;而流程逻辑相当于骨架上的肌肉;那么控制逻辑就相当于肌肉中的神经。

领域模型

概念上,一个领域模型和普通的符合面向对象原则的对象有声明区别:领域模型是业务意义上,承载了业务数据(可以认为所有领域模型是有状态对象),从本质上说它直接来源于现实世界,没有技术层次上的考虑,“符合面向对象原则的对象”是用面向对象方法分析得到的,是基于计算机领域技术的(这样的对象可以是无状态的);但反过来,符合面向对象的对象不一定反映业务领域的模型。

技术上,领域模型是指那些包含需要被透明持久化的属性,以及相关业务逻辑的POJO。一个领域模型包含了这些需要被持久化的业务数据,同时还包含了与之相关所有业务操作(即能操作所有属于本模型生命周期内的模型数据),并且有自己的继承体系。MartinFowler认为有了这些就可以称为是一个领域模型,因此在其PoEAA中的ORM包含了一些不透明的持久化方案。我认为一个真正的领域模型需要一个透明持久化。

注:领域模型在不同视图下导致不同的内容。比如一个代理人Agent对象,在Party的视图下只拥有基本属性,而在Salechannel视图下就保存了一些额外信息如:考核记录,优秀率等。

领域服务

领域服务包含的商业逻辑包含了两部分:流程逻辑。业务领域的流程逻辑(即业务流程)。指一系列的业务行为,包括维护一个或者多个领域模型。领域服务是一个UnitOfWork模式。

领域模型中逻辑仅包括自身生命周期关联数据的操作,相当于原子操作;只有领域服务则代表完整的服务操作边界,例如一个领域服务会包含对一个领域模型的三个调用;这一边界通常也是事务的控制边界;

RodJohnson在《J2EEwithoutEJB》第10章《持久化》里面比较清楚的论述了这个问题:

Workflowmethodsatthebusinessfacadelevelarestillresponsiblefortransactiondemarcation,forretrievingpersistentobjectsinthefirstplace,andforoperationsthatspanmultipleobjects,butnolongerfordomainlogicthatreallybelongstothedomainmodel.

这段话明白无误的讲清楚了领域模型应该包含什么逻辑,不应该包含什么逻辑。领域模型包含的逻辑,这里称之为“领域逻辑”,这些领域逻辑应该最小化的依赖于DAO,而我们讨论的那些需要持久化操作参与的事务性逻辑,这里称之为“workflowmethods”,这些“workflowmethods”应该放在“businessfacade”,而不应该放在领域模型里面。

Whatlogictoputintodomainclassesversuswhattoputintoworkflowcontrollersshouldbedecidedcasebycase.Operationsthatarereusableacrossmultipleusecasesshouldnormallybepartofdomainclasses.Singleusecasethatarehardlyreusablearecandidatesforcontrollermethods;soareoperationsthatspanmultipledomainobjects.

最后RodJohnson给出来一个区分的“businessworkflowlogic”和“领域逻辑”的准则:视具体情况而定,紧密结合领域模型的,可重用度很高的操作可能是领域逻辑,应该放在领域模型里面;比较难重用的控制逻辑方法,特别是可跨越多个领域模型的操作则放在businessfacadeobject里面。

业务规则

这里单列出来,很多时候业务规则是附属在领域服务中的,但在一些特定项目中,业务规则会被单独维护。

1.产生一些控制信息,限制或者触发某些行为的执行(Aruleisadeclarativestatementthatapplieslogicorcomputationtoinformationvalues);

2.产生一些状态信息,提供给业务人员参考操作(Aruleresultseitherinthediscoveryofnewinformationoradecisionabouttakingaction.)。

其它技术问题

应用职责角色分层,必然涉及到两种对象,一种是用于展示信息的结构——VO(边界外通过编码方式使用的),一种是DAO对象。这两种职责角色对象,严格的说不算是业务设计需要关心的。然而却和系统开发息息相关。而业务设计中的变化导致相关的工作量却是巨大的。

VO对象是为了集成而存在的;其意义是:1.保护系统的信息边界,提供一种结构可以使其它系统或者组件通过编码方式获取系统内信息的方式;2.保护系统的事务边界,领域对象技术上携带着持久化信息,通过VO得以屏蔽。常见的VO对象存在于Web层和Domain层。

因此,VO对象的存在只是为了集成而存在,其是否存在的取决于两个方面1.集成的设计结构;2.框架的两个能力——对象路径访问能力以及事务边界管理。

Domain层VO对象,通常是用于不同领域组件间的交互,但随着架构的改进,集成代码独立存在而不再嵌入到组件内部,组件的边界问题保护不复存在;更进一步的是,框架提供自动化的接口适配映射能力的增强。因而VO对象失去存在的意义。

Web层VO对象,以SWF为例,早在SWF1.x时代,框架就提供了丰富的对象路径访问能力,但其Web交互是典型的MVC2方式,事务边界在view的render前关闭,因而导致需要特定的VO对象来避免持久化信息问题;而SWF2.x时代,view的render是在事务边界内,VO不再需要。

针对维护Dao对象而产生成本的一种解决方法是:代码生成。生成的策略分为两种环境,开发环境和生产环境。开发环境实时动态生成,可以采用动态代理机制;而生产环境要求性能,采用预编译生成能力。

另外Web层或者UI层,不需要额外的VO对象的另一个理由是:通常Web层的独立维护的成本大于其复用的价值。在开发中,Web层需要的信息都来自Domain层,这样容易出现Web层的VO对象和Domain对象结构一致,虽然这样便于学习和简化开发,但导致维护成本的提高,每当页面需要一个新的属性就需要改变太多的类,同时Domain对象设计容易限制于Web层,而实践中Web层的变化更多于Domain层。正确的路径是很好的维护Domain层,同时不维护Web层。

设计开发

领域模型的四种类型

领域模型可以分为四种类型:

0.全局常量对象

1.长生命周期业务对象(类似保单对象);

2.交易过程的事务对象,几乎没有生命周期的;

3.业务请求对象和业务反馈对象。这类对象以前没有识别的,通常和VO混在一起;但是在IAA中以及电信业的模型是这类对象是独立存在,并被持久化的;业务请求对象建立在增量更新上很有用。当然他们也是几乎没有生命周期的。

并非所有的业务系统都拥有这四类对象!相当一部分的业务系统,并没有显著的长生命周期对象,因而没有明确的增量变更操作类型及其规则,业务操作是直接更新业务对象,也就没有业务请求以及业务反馈对象;同时此类业务系统的事务对象也通常不存在;

这里要额外补充说明的是:

对于业务请求,每个业务请求必需记录下业务时间;对于业务处理,每个业务处理还可能保留一定的人工干预控制信息,也将同生命周期的输入数据一起记录;对于生命周期,每个生命周期状态的变化都可能会有独立的数据需要记录。

领域模型的级别

不论是那种类型对象,都拥有一个属性,对象等级;对于保险系统来说,保单对象,产品对象以及组织对象是一级对象,而险种和角色等都是二级对象,其生命周期附属于一级对象;这点对于设计Repository以及服务粒度都有影响。

领域模型的动静之分

在系统运行期间,被频繁建立和更新的称为“动态”,而在较长的一段时间内保持稳定的称为“静态”。

通常而言,“动态”的领域模型群通常代表了系统的核心业务对象。而“静态”的领域模型则在业务上代表了系统的依存关系。

领域模型的分析过程

1.设计一个贫血的领域模型,包含所有需要被持久化的数据,除了用例显示要求的和隐式要求的(根据业务分析出需要的辅助数据,主要是特定的控制信息),以及与其它领域模型的关联关系;

2.考虑是否支持多态

3.分析潜在的性能问题,根据需要人为断开存在的关联关系;

4.列出相关的逻辑,利用面向对象设计原则,决定哪些逻辑属于该领域,并加入该对象模型;

5.分析是否实现该对象的依赖关系注入。

业务请求对象的虚实之道

业务请求的概念,与HTTP请求是不同的。为避免误解,特意加上业务一词修饰。所谓虚实是指是否将业务请求概念实例化。不做实例化的理由时处理简单;实例化则有助于处理业务事务控制以及应用账目模式。一个业务操作上的业务请求可能包括多个请求对象,与核心业务对象对应,例如:在线订单,就包括了购买物品及其数量和折扣,支付协议和发货协议等。

对于没有实例化业务请求的情况下,在实际业务操作时,对每一个HTTP表单的操作都需要一个物理的事务来支持。这样做的问题是,由于没有记录业务请求,直接操作业务对象,在做业务日志时只能记录操作前以及操作后的信息(既“减肥前,减肥后”);同时跨越多个物理事务,要支持查询到一次业务请求所有操作的信息,需要新建一个日志索引或者类似的手段,在业务开始时获取注册一个索引,所有日志操作中引用这个索引,在业务结束后关闭该索引。虽然如此,也带来的是业务上做回退(undo)以及重做(redo)操作的不便。

但是如果实例化业务请求,就很容易处理这两样操作。建立一个业务请求,同时记录所涉及的请求数据。这样做的好处是:可以很容易的记录一些额外的信息;同时可以很容易的支持审批操作(既俗话说的“管帐的不管钱,管钱的不管帐”)。

另外,业务请求对象附加好处是,由于某个领域模块的增量操作通常从一个根对象(即一级对象)开始,所依赖的过滤条件可以从业务请求中加以识别并通过框架提前加载,而领域服务对象的方法接受传递对象而不再关心对象的加载工作;同时也可以通过框架处理基本数据复制工作,这样程序只关心关联对象的操作。

业务请求对象比较适合明确增量变化的业务系统,通常这样业务系统在处理变更时,规则和关联处理很多;对于全量数据,采用业务请求对象得不偿失,最终两个对象设计会趋于一致;对于不确定性变化系统,如果应用业务请求对象会导致捕获变化数据的困难,应该考虑采用更好的业务组织方式(如SpringSide的设计实现,但SS没有考虑批改的流程和日志)。

不过目前大部分的系统都没有处理业务请求实例化,不是所有的业务操作需要审批,另外实例化的麻烦是,已经处理了一个日志对象,再处理一个业务请求对象总是让人多少心里有点不爽;

一个可能的持久化方式是:记录对象路径及其对应的值;通过自动化的系统扫描,获取对象路径结构,以Key和Value的形式记录在数据库中,可以避免复杂的持久化处理;

Repository(Dao)的设计开发

Repository是一种特殊的Service,不做任务处理;而是提供模型的持久化,重建和查询工作。由于企业应用大都通过数据库实现持久化,因而Repository和传统的Dao间的集成设计就非常重要。

已知的有三种设计方式:

方式

描述

优缺点

1.两个接口两个实现类

Repository和Dao各自独立接口,而通过Repository实现类转发请求给Dao实现类

这种方式虽然正统,但是维护成本太高;一次更新最多要改四处地方

2.两个接口一个实现类

Repository和Dao各自独立接口,一个实现类同时实现两个接口

这种方式就大大简化维护成本;一次更新最多只改一个接口和一个实现类

3.两个接口一个实现类

与方式2不同是,Dao接口继承Repository接口,一个实现类实现Dao接口

这种方式的维护成本和方式2差不多,但是当接口方法在这个两个接口间流动时,可以通过开发工具完成

另外Dao实现类也是工作中开发维护成本较高的一部分,可以通过代码生成降低开发成本。已知的是JDBC4.0规范和iBatis3.0的实践。

领域服务的设计开发

业务服务是整个领域设计中另一个重要的元素;业务服务的如何设计,并无定论,但有原则和分类,最重要的是围绕着业务流程设计,而其基础建立于底层模型自身业务逻辑的原子操作。

按粒度划分:

1.原子服务,业务服务的原子操作;在产品化设计中,该层次服务应该拥有扩展点和参数化能力;

2.组合服务,封装业务服务的组合操作;在产品化设计中,拥有参数化,扩展点,事件和集成能力;

3.还有一类业务服务设计是实现于工作流,该层次逻辑关注于系统集成上;在产品化设计中,该层次应该拥有事件;

实际上,原子服务和组合服务的粒度划分并不具备可操作性;只不过加以标识试图进一步的分析,并为产品化设计做基础;

按事务划分:

1.事务服务,事务服务和持久化操作有关,提供事务边界;通常是聚合服务;

2.计算服务,也算是read-only的事务服务;计算服务的粒度不一定;

由于服务是针对领域,因而事务服务不关注于工作流的流程状态,只关心相关领域中长生命周期领域模型的生命周期;而计算服务更不关心流程相关,只验证输入合法性,做出计算,返回结果,完全是无状态的;工作流则关心的是相关领域中的request对象的业务状态,对于同一业务对象的并发处理,应该通过业务来控制;

领域服务的运行模式

简单的说,业务处理将被细化成处理控制器和具体处理器。在这级别,处理对于请求的响应处理已知的有三种模式:

事件模式(ObserverPattern)、职责链模式(ChainofresponsibilityPattern)以及数据流模式(PipesandFiltersPattern)。这几个模式处理的各自不同的场景。其中数据流模式很适合需要处理大量数据的情况。

相关推荐