作者:anders小明
2009年5月7日
需求背景
现在的样子
如PoEAA中提到的:事务脚本(Transaction Script)和表模型(Table Moduel)模式。
存在问题
事务脚本看到的是零散的数据,而表模型混合了下文要说领域模型和领域服务界限。
1. 两者都导致了分析和设计的割裂,领域模型只存在于分析中;数据间的内在关系无法通过代码体现;
2. 两者都无法有效的实现业务逻辑的具体的差异化和抽象的一致性;导致学习和维护成本的增加;
3. 两者都带来了测试上困难,增加了开发成本;
为何要面向对象?
1. 面向对象是自上而下的开发方法;这种方式对于迭代增进式的结构化过程来说成本是最低的;
2. 数据和业务逻辑的绑定性,采用面向对象容易操作和沟通(即实施成本低),同时有助于区分状态逻辑和任务逻辑;
3. 业务逻辑的差异性,同一个业务逻辑针对差异性数据有区分做法,采用面向对象容易维护;
什么是领域驱动设计
领域驱动设计的提出是由Eric Evans在其《领域驱动设计》一书提出。实质上是一种由内而外的设计方法,俗话说的先中间(模型和服务)后两边(界面,数据库以及集成)。
领域驱动设计的优势
传统的开发方式:基于数据库的设计开发。数据库提供的设计模型是表和字段两种粒度,这两种粒度有时并不合适于系统设计:
1. 模型的结构化能力
1.1. 同一组件下的设计优势,一个模型可以来自多张表的数据聚合而成,一张表可以聚合多个模型;一个逻辑是由几个固定字段或者非固定字段聚合;模型间的关联关系也是使用表无法展示的——外键的约束对于系统开发来说实在太有限了。而这些不论表还是字段粒度都无法支持的。
这里强调一下模型间的关联关 系,特别是和生命周期相关的聚合和组合关系。关系数据库中所保存的是系统分解后的表示,即关系被分解了而不是被表达了,外键对数据关系只起到一种约束作 用,它对于查询语句的构建并没有直接的影响。所有数据之间的关系都必须在查询的时候明确指定出来,即调用者必须拥有数据关系的知识,而不是数据本身拥有这 些知识.在如下的SQL语句中
“select * from a, b where a.fldA = b.fldB and a.fldC = 1 and b.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。一个领域模型包含了这些需要被持久化的业务数据,同时还包含了与之相关所有业务操作(即能操作所有属于本模型生命周期内的模型数据),并且有自己的继承体系。Martin Fowler认为有了这些就可以称为是一个领域模型,因此在其PoEAA中的ORM包含了一些不透明的持久化方案。我认为一个真正的领域模型需要一个透明持久化。
注: 领域模型在不同视图下导致不同的内容。比如一个代理人Agent对象,在Party的视图下只拥有基本属性,而在Sale channel视图下就保存了一些额外信息如:考核记录,优秀率等。
领域服务
领域服务包含的商业逻辑包含了两部分:流程逻辑。业务领域的流程逻辑(即业务流程)。指一系列的业务行为,包括维护一个或者多个领域模型。领域服务是一个Unit Of Work模式。
领域模型中逻辑仅包括自身生命周期关联数据的操作,相当于原子操作;只有领域服务则代表完整的服务操作边界,例如一个领域服务会包含对一个领域模型的三个调用;这一边界通常也是事务的控制边界;
Rod Johnson在《J2EE without EJB》第10章《持久化》里面比较清楚的论述了这个问题:
Workflow methods at the business facade level are still responsible for transaction demarcation, for retrieving persistent objects in the first place, and for operations that span multiple objects, but no longer for domain logic that really belongs to the domain model.
这段话明白无误的讲清楚了领 域模型应该包含什么逻辑,不应该包含什么逻辑。领域模型包含的逻辑,这里称之为“领域逻辑”,这些领域逻辑应该最小化的依赖于DAO,而我们讨论的那些需 要持久化操作参与的事务性逻辑,这里称之为“workflow methods”,这些“workflow methods”应该放在“business facade”,而不应该放在领域模型里面。
What logic to put into domain classes versus what to put into workflow controllers should be decided case by case. Operations that are reusable across multiple use case s should normally be part of domain classes. Single use case that are hardly reusable are candidates for controller methods; so are operations that span multiple domain objects.
最后Rod Johnson给出来一个区分的“business workflow logic”和“领域逻辑”的准则:视具体情况而定,紧密结合领域模型的,可重用度很高的操作可能是领域逻辑,应该放在领域模型里面;比较难重用的控制逻 辑方法,特别是可跨越多个领域模型的操作则放在business facade object里面。
业务规则
这里单列出来,很多时候业务规则是附属在领域服务中的,但在一些特定项目中,业务规则会被单独维护。
1. 产生一些控制信息,限制或者触发某些行为的执行(A rule is a declarative statement that applies logic or computation to information values);
2. 产生一些状态信息,提供给业务人员参考操作(A rule results either in the discovery of new information or a decision about taking action.)。
其它技术问题
应用职责角色分层,必然涉及到两种对象,一种是用于展示信息的结构——VO(边界外通过编码方式使用的),一种是DAO对象。这两种职责角色对象,严格的说不算是业务设计需要关心的。然而却和系统开发息息相关。而业务设计中的变化导致相关的工作量却是巨大的。
VO对象是为了集成而存在的;其意义是:1. 保护系统的信息边界,提供一种结构可以使其它系统或者组件通过编码方式获取系统内信息的方式;2. 保护系统的事务边界,领域对象技术上携带着持久化信息,通过VO得以屏蔽。常见的VO对象存在于Web层和Domain层。
因此,VO对象的存在只是为了集成而存在,其是否存在的取决于两个方面1. 集成的设计结构;2. 框架的两个能力——对象路径访问能力以及事务边界管理。
Domain层VO对象,通常是用于不同领域组件间的交互,但随着架构的改进,集成代码独立存在而不再嵌入到组件内部,组件的边界问题保护不复存在;更进一步的是,框架提供自动化的接口适配映射能力的增强。因而VO对象失去存在的意义。
Web层 VO对象,以SWF为例,早在SWF 1.x时代,框架就提供了丰富的对象路径访问能力,但其Web交互是典型的MVC2方式,事务边界在view的render前关闭,因而导致需要特定的 VO对象来避免持久化信息问题;而SWF 2.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实现类也是工作中开发维护成本较高的一部分,可以通过代码生成降低开发成本。已知的是JDBC 4.0规范和iBatis 3.0的实践。
领域服务的设计开发
业务服务是整个领域设计中另一个重要的元素;业务服务的如何设计,并无定论,但有原则和分类,最重要的是围绕着业务流程设计,而其基础建立于底层模型自身业务逻辑的原子操作。
按粒度划分:
1. 原子服务,业务服务的原子操作;在产品化设计中,该层次服务应该拥有扩展点和参数化能力;
2. 组合服务,封装业务服务的组合操作;在产品化设计中,拥有参数化,扩展点,事件和集成能力;
3. 还有一类业务服务设计是实现于工作流,该层次逻辑关注于系统集成上;在产品化设计中,该层次应该拥有事件;
实际上,原子服务和组合服务的粒度划分并不具备可操作性;只不过加以标识试图进一步的分析,并为产品化设计做基础;
按事务划分:
1. 事务服务,事务服务和持久化操作有关,提供事务边界;通常是聚合服务;
2. 计算服务,也算是read-only的事务服务;计算服务的粒度不一定;
由于服务是针对领域,因而事 务服务不关注于工作流的流程状态,只关心相关领域中长生命周期领域模型的生命周期;而计算服务更不关心流程相关,只验证输入合法性,做出计算,返回结果, 完全是无状态的;工作流则关心的是相关领域中的request对象的业务状态,对于同一业务对象的并发处理,应该通过业务来控制;
领域服务的运行模式
简单的说,业务处理将被细化成处理控制器和具体处理器。在这级别,处理对于请求的响应处理已知的有三种模式:
事件模式(Observer Pattern)、职责链模式(Chain of responsibility Pattern)以及数据流模式(Pipes and Filters Pattern)。这几个模式处理的各自不同的场景。其中数据流模式很适合需要处理大量数据的情况。
以下纯粹是个人观点, 不代表任何组织或社团.
现在流行的编程语言如Java和C#, 大多是面向对象的, 程序的各部分是通过方法调用连在一起, 其编程范式是命令编程, 即使支持其他范式也包装得很难理解和使用.
面向对象实际上只适合实现抽象数据类型, 让它去完成除此之外的任务确实是勉为其难, 即便能完成也给人不伦不类的感觉, 既不像面向对象编程, 也不像它的前任(过程编程). 这些语言中的对象与物质世界的物质(或对象)很不一致, 它使用方法调用的方式与其他对象进行相互作用, 而这与物质之间的(通过通信或媒介)相互作用是截然不同的, 因此用面向对象无法很确切地模拟现实世界(面向对象思想的初衷), 更不用说准确地为现实世界建立模型.
这几年流行的web服务和SOA虽使程序间交互更方便, 但它从本质上说还是使用”面向对象+命令编程+方法调用”的思路, 在编程方法论上并没有实质的进步.
用现在流行的语言实现的完成复杂功能的程序逻辑不清晰, 原因在于”命令编程+方法调用”的设计机制. 这使程序很难模块化, 副作用无所不在, 因此很难正确实现复杂功能.
现实世界的发展变化是通过事物间的相互作用实现的, 而这种相互作用用计算机科学的语言来说就是并发(concurrency). 软件的本质是什么? 我觉得:软件总是完成某种功能的,归根到底是对现实世界的事物间相互作用进行建模. 因此软件的组成部分间自然就是并发的关系, 而不是过程调用的关系. 用通信进程来对现实世界的事物间相互作用进行建模是比较合理的. 所以进程应该作为语言的基础成分, 是软件的基本组成部分, 而不是只为了提高效率才采用的.
为了使程序能准确地为现实世界建立模型, 从而正确性更高, 结构更合理, 模块化程度更高, 因此在几种编程思想或语言的基础上(见references), 我提出一种新的编程方法论: 面向进程+函数编程+进程间通信+逻辑编程+约束编程+其他合理的范式(命令编程除外)和一门编程语言ProcessLog (全称process logic).
ProcessLog只支持一种并发:通信进程. 它就是计算机科学家在上世纪70年代为了克服现在的Java中仍采用的那种并发方式的缺点而提出的. 它是经过充分研究得到的一种理想并发方式, 看了并发理论(concurrency)和进程代数(Process algebra), 就会明白这种并发方式可解决实际中的各种并发问题, 用它足够了.
这里的进程是进程代数的进程,不是过程,也不是Java中的线程. 看看jcsp或Hoare的CSP( http://www.usingcsp.com/)就明白了.推荐网址:
http://www.cs.kent.ac.uk/projects/ofa/jcsp/,
其中有两个ppt说得很明白:
"Process Oriented Design for Java: Concurrency for All",
"Communicating Processes, Components and Scaleable Systems".
ProcessLog的语法概要如下:
1 运算符
(1) ? 输入; c ? x 从输入端口c或通道c上接收输入值放到变量x中
(2) ! 输出; c ! v 把v的值从输出端口或通道c上输出
(3) -> 顺序进行的事件的先后关系
(4) | b : s 分支
(5) || 进程并行
(6) // 附属进程
(7) and, or, not 逻辑运算符
(8) 算术运算符和关系运算符 与Java中相同
2 程序的组成成分
(1) Unit 程序单元
(2) Process 进程
(3) Function 函数
(4) Predicate 谓词
(5) Channel 通道, 有两个端口: in 输入端口, out 输出端口
(6) OutPort 输出端口
(7) InPort 输入端口
3 数据结构
(1) List (函数编程中的List类型, 对List的操作函数与函数编程中相同);
(2) Tuple 元组, 同Clean.
(3) Set 集合
没有数组
4 进程的定义
Process p1 (OutPort pt1, InPort pt2 ){
pt2?x -> pt1! compute(x) -> p1
}
5 进程间相互作用
(1) 进程并行 process1( c1.out, c2.in)|| process2(c1.in, c2.out)
(2) 附属进程 (getE: getElements || getR: getReleasedVersion) // X.(in?method -> getE ! method ? elems -> getR ! em ? rem-> … ->X)
6 函数
[Function] compute(double x)=
| x<=0: x*x+3
| x>0: compute(x-5)* compute(x-3)
函数只能以事件的方式在进程中使用或在其他函数中使用, 不能独立使用.
7 谓词
/* 建图 */
Predicate createGraph(t, graph):-
addNode(t, null, ({},{}), graph1),
getDS(t, graph1.ns, tlist),
addList(tlist, t, graph1, graph).
/* 加节点 */
Predicate addNode(t, null, (ns, es), (ns1, es):-merge(ns, {t}, ns1).
Predicate addNode(t, upper, (ns, es), (ns1, es1)):-
merge(ns, {t}, ns1),
merge(es, {(upper, t)}, es1).
8 把谓词转换为函数
create(t)= graph
where createGraph(t, graph)
谓词不能独立使用也不能在进程中直接使用, 要在进程中使用需要先转换为函数.
9 程序单元: 包含进程和数据类型
Unit PmethodDAO;
interface
Tuple Method;
Process getLastVersion(OutPort pt1 , InPort pt2);
…
implementation
Method=(String id, String name, String version);
Process getLastVersion(OutPort pt1 , InPort pt2){
…
}
指导原则: 程序是由通过通道通信的进程组成的. 数据处理和算法用函数编程实现, 如果函数编程不适用于要处理的问题, 就使用逻辑编程或约束编程.
ProcessLog语言限制了编程的随意性, 要求只能用进程代数+函数编程+逻辑编程的方式编程, 不允许用Java或c#的命令方式编程.
ProcessLog 现在还没有在机器上实现. 我用该语言重写了实际项目中的一些复杂代码(原是Java代码), 证实用它写的程序确实简单清晰, 有类似数学的简洁美. "7 谓词"就是其中一部分代码.
我的想法是: 应先在纸面上规定它的语法与语义, 再通过使用它编写一些应用程序来发现它的不足,再进而改进它, 再实践, 再改进, 直到它基本完善再在机器上实现. 另外, 大家不要把语言分为中国人提出的还是外国人提出的, 科学无国界, 这里不存在狭隘的爱国主义. 我不是那种技术高手, 但我自信我是一个能将理论很好地联系实践的研究者.
希望有识之士和我一起共同发展这种编程方法论和这门语言.
[email protected]
References
1. CSP http://www.usingcsp.com/
2. JCSP http://www.cs.kent.ac.uk/projects/ofa/jcsp/
3. Clean http://clean.cs.ru.nl/
4. Prolog
5. Delphi
注: 转载时请注明作者.
说的东西,相当复杂。