暴露领域模型的不均匀性(中国IT读懂此文的不超过十个人)

暴露领域模型(Exposed Domain Model)的不均匀性――一个问题引发的思考
一个简单的问题
对象关系映射持久化引擎提供了弱类型的查询OQL,一个业务实体Person,现在要查找名字为**的人。模型层有两个可能做法:

1.传统的Service-DAO: getPersonByName(String name)
2.由上层直接使用持久化引擎
问题本身虽然简单,但是不同的选择会反映出很深刻的方法论的倾向性。本文对它们作一些辨析。

深度暴露
上述问题的第二种处理方法实际上将面向对象的持久化引擎直接暴露给表现层(或者是逻辑层的最上部),第一种方法是对持久化引擎作了简单的封装;问题在于,这种封装过分降低了通用性,基本上是与功能用例形成对应关系,其后果是导致大量的无任何抽象性可言的垂直逻辑单元;它们随着功能的增加线性膨胀。第一种方案直接将持久化引擎(实际上是持久化引擎的一个很薄的封装)暴露出来,而持久化引擎提供了对业务实体最直接的访问能力,是一种最深层次的暴露。功能用例直接接触深层模型。

面向对象查询语言
第一种方案使系统的上部直接面向弱类型的面向对象查询语言而不是具有明确的语义的服务接口,其合理性问题需要一个像样的解释。首先,面向对象查询语言无论在语法结构上与SQL看起来如何相似,从概念模型的角度,它与SQL都有本质的区别:OQL所面向的是业务实体及其关联关系所形成的领域模型的静态结构,SQL所面向的是关系表;业务实体及其关系集中了两个成果:第一,对象关系映射引擎在抽象的、通用的、领域无关的层面上、语法的层面上所做的转化;第二,实体及其关联关系所反映的抽象的领域模型。如另外一篇文章所述,这个模型实际上是完整的领域模型的静态方面。这个模型的产生集中体现了设计活动的基础形成果。现实的开发者经常需要完成一种项目改造,将一个直接面向关系表构造行为逻辑的系统改造为通过对象关系映射构造行为逻辑的系统;如果一个开发者做过这种种遗留系统的改造,应该对这两点有所体会。对象关系映射工具所提供的由表结构到类的映射工具所得到的类型系统基本无法以一种直观的方式体现业务对象的关系,改造者需要经过大量的调整,在业务对象和遗留系统数据库模式之间建立曲折复杂的而不是直接的映射关系之后,对象层才能够呈现出业务逻辑概念模型;这种调整将占去改造工作的一半以上的工作量,从而也蕴含了一半以上的设计努力;业务实体及其关系在很大程度上基本反映出领域模型。通过OQL所操作的正是这样一个领域静态模型。

对领域静态模型的操作由模型本身来完成的时候,这种操作就内化为模型的行为,这也是面向对象的一个基础的概念:封装,使信息及其之上的逻辑合一。如同任何方法论一样,面向对象的封装概念同样有自己得适用条件,从而也会因为系统构造者的习惯、风格、价值观等因素的差异引发分歧;在客观上,封装的必要性问题也是一个连续的谱系,这个谱系的坐标轴就是:所封装的逻辑的强弱。逻辑越强,蕴含了相当的原子操作的组合操作时,封装是必要的,如果是不具有任何组合性质的读、写的原子操作,封装的必要性就会成为可选的问题从而引起不同的意见,不同的处理方式;这些处理方式的正确与否在抽象层面上难以界定,因而就需要根据所实施的环境和条件加以考量做出现实的判断。关于作为信息结构载体的Bean的成员是否应该直接暴露为公有变量还是应该通过一个方法来访问的争论,在抽象的层面上,与所谓暴露模型的问题一样,都是关于封装的争论。然而,Bean的问题的具体条件和后果都与暴露模型有极大不同;如果不考虑Bean工具类的因素,前者基本上会局限于代码风格这样比较狭隘的范畴。暴露模型则不同,对封装问题不同的处理所产生的设计后果的差异是非常巨大的,具体地说:过强的封装会严重降低模型的通用价值,从而产生组合爆炸般的封装代码。基于这一点,对于简单的、基本是原子级的对模型的操作,直接深入模型的深层,理论上具有根本的合理性。直接通过面向对象操作语言操作模型,在相当多的情况下都是正确的选择,它避免了大量的与数量极多并且变动不居的功能用例的平行的狭隘的结构组件的出现,使模型保持较高的抽象性、通用性、同一性。

模型的高级行为:组合逻辑
上一小节的分析实际上已经论证了组合逻辑作为模型的一个固有构成的必然性。这些逻辑如果由功能层基于领域静态模型来实现;换句话说,由功能层直接操作静态模型来实现组合逻辑,则会从实质上,而不仅仅是形式上违背封装原则,其具体表现是在不合适的地方,比如JSP页面或者Servlet中出现复杂的难于维护的QL语句;有时是较长的代码;使稀薄的控制层变厚重;这种现象也就是《POJO IN ACTION》中所描述的暴露模型的弱点(Also, the lack of a façade increases the chance that changes to the business tier could affect the presentation tier. There is also the risk of business logic creeping into the presentation tier.)。严格来说,这是教条主义的弱点,是对模型不分条件、不分部位、不加区分地暴露,是暴露过渡,是对封装的反动过度,矫枉过正;是方法实施者、系统设计者本人把握能力的问题。这是方法论之所以不能够代替设计者个人创造力、软件开发之所以成为Art的根源。

不均匀暴露
以上两个小节可以直接导出一个结论:模型在各个部分的暴露与不暴露、暴露到什么程度,是不均匀的,因为一个系统中不同的部分是否需要组合逻辑、逻辑组合的程度是不均匀的,正确地、辩证地处理这种不均匀性,自然会导致与之相适应的暴露程度。从这个意义上说其实也无所谓暴露模型或者封装模型,因为我们在一个现实系统的开发中无法彻底摈弃或者实施任何一种方法,设计者所要做的无非是权衡、折中,在正确的时间、正确的地点作正确的事情。因此,从现在开始,当我们在积极的意义上来谈论所谓暴露模型,所指的是这样一个不均匀的暴露模型

暴露模型中功能用例有时直探模型的核心,有时访问模型的表层,呈现出跨层访问的结构特点。

另外几个相关问题
领域实体及其关系通过QL语言暴露给功能层的前提是:这些实体及其关系形成了对概念模型的完备的再现;否则,如果实体集合需要经过复杂的装配才能够形成概念上的领域模型,则这种暴露就将导致功能层复杂的QL语句;这时,系统构造者面临两个选择:或者调整实体对象及其关系使其足够直观地再现概念模型,或者通过封装掩盖一个不良的静态模型。

适度封装(或者暴露)的指导原则可以被推广到其他方面,比如在处理应用逻辑与一个通用的、细粒度的API的关系上。但是,具体情况不同,在做法上差别极大,本文不作深入探讨。

暴露模型与开发流程
最后我们略谈一下暴露模型与开发流程方法论的关系。谈到开发流程方法论,在这里主要是指以重构为核心方法的敏捷开发和传统的事前设计为重点的方法。首先一点是,暴露模型与两类开发流程方法论之间没有绝对的关系;其次,由上文所述能够看出,暴露的不均匀性是一个需要极大创造性的指导原则,而不是设计教条,而一切创造性活动在本质上都更加倾向于一个进化的过程,因此,从这个意义上,暴露模型与敏捷开发、重构活动有天然的和谐关系;事实上,分布于模型的各个部位上的不均匀程度与模型本身的其他方面一样,伴随着开发的历史而形成自己变化发展的历史。

[该贴被GUANPEI于2008-07-06 06:06修改过]

这需要这个模型与系统存在内在的耦合,spring之Mock可解决一些技术问题。如果我想把某些作业开放成服务供别人用,似乎不太合适。

>模型层有两个可能做法:

>1.传统的Service-DAO: getPersonByName(String name)
>2.由上层直接使用持久化引擎

Evans DDD中好像没有说模型层是这两种解决方式,所谓模型层就是围绕模型为核心展开,我一直主张:讨论设计领域的问题如果不从设计源头“分析建模”这个宏观高度来出发,就有可能发生为设计而设计。如果作者象Evans DDD那样举些例子来说明自己观点,所谓大道至简,相信你的思想会在中国IT普及。我们崇尚普及,中国软件设计太需要提高了。
[该贴被banq于2008-07-07 10:05修改过]

我并没有把问题描述清楚,因为这本来是写给公司开发团队的一篇东西,大家对我的想法都很了解,所以开头就是一提。基本是这个意思:功能层(大部分情况也就使表现层)是否为了一个极其简单的功能(getByName)都要做一个封装,而这个封装不具有任何逻辑?如果直接访问持久化引擎的一个简单封装,是不是更简洁,允不允许这样做?如下:
PersistenceFacade facade = ...//
List ps = facade.query("from Person where name = 'zhangsan'");
...
其中PersistenceFacade是一个O/R映射的简单封装

getPersonByName如何实现调用,需要视具体业务情况而定,如果getPersonByName是作为唤醒冬眠的领域对象一个方法,那么它和getPersonByID是等价的,同属于Evans DDD中从仓储Repository获取领域对象方法,可以使用工厂等方式解决。

如果属于一般查询,返回是一个集合,那么就要从过滤规则这方面去考虑。

感觉离题了

暴露领域模型(Exposed Domain Model)的不均匀性是设计师应当极力避免的,应当为所有的业务逻辑提供一个统一的接口,这是为了消除紧耦合付出的代价,所以设计师应当欣喜得接受它,而不用为此自寻烦恼。
软件开发的最高目标是为秩序、消除混乱,而不是为了执行效率,我们只有付出这样的代价;

想起了我有次在中国银行的例子:
1、我要用存折取款,于是我在叫号机那拿到了一个号;
2、我前面有两个人;
3、一个人取款,一分钟搞定;
4、一个人可能想开基金或者股票户之类,与柜员进行了长达一个小时的交谈;
5、等我明白情况,已经等了数十分钟,就算快气死,也只能继续等下去。。。。

当然,我们可以把存取款(业务简单)与开户(业务复杂)这样的事分开,但这样付出接口多样、客户(使用接口的程序员)难于理解、约定复杂的代价;

对于多人合作主、生命期长的项目,层次最好是均一的、宁厚勿薄的,符合约定的,这样虽然略显古板、笨拙,但从长期来看,却是最有利的。


另外,提到OQL,根据我的经验,我认为领域模型应当是最重的,在设计领域模型时,使实现业务逻辑的OQL越简单越好;
复杂的OQL是混乱之源;业务操作通过领域模型之间的接口实现,而与OQL无关;过多使用OQL,不可避免地使OQL成为业务操作语言,薄化并弱化了业务层,使业务层成为组装、调用OQL的参数传递层;
我觉得OQL实际上是不应当出现的东东,作为ORM,CRUD就行了,为何要OQL?!象iBatis学习,做好自己的本份就行了!

我们要通过需求所要求的业务操作来设计领域模型。需求变更,所影响最大的是领域模型,因此,尽量薄化、容器化、配置化领域模型之外的层次是最合适的。

我觉得,在系统中,表现层与持久层应当是属于“容器”的一部分,领域模型只需要通过某种“语言”(XML或者标注)描述如何表现自己,自己能做哪些业务操作,如何持久化自己,然后把它布署到某种容器中就行了。


[该贴被mentat于2008-07-07 16:14修改过]
[该贴被mentat于2008-07-07 16:18修改过]

客户:我想看看上个月我订了什么货,但是我不知道怎么做。
action:你把“上个月”这条件给我,我告诉你。
客户:我已经给你了。
action:这需要同我们的订单系统打交道,我只负责把你的请求转交给service处理。
service:没问题,我知道这个系统为客户提供的一切功能。
domain:不行,根据规定,有些订单我们不能显示给客户。
service:那是你(domain)的事,我不想知道系统内部的规则,我也不需要向客户提供这些过滤操作,你只要把结果给我就好了。
domain:真糟糕,我不知道数据库是什么东西,但我要的那些订单都在那里面。幸运的是我有个朋友叫dao(repository),它对数据库很了解,喂,你替我把订单找出来吧。
dao:我已经给你了。
domain:现在我要确认哪些不能显示给客户。我似乎已经筛掉了那些“不能见”的订单,现在我的任务是把剩下来的订单交给service。
service:我会马上把这些结果交给action。
action:我已经显示给客户了。
客户:我总算看到结果了。
1:订单做为输入和输出存在于整个流程当中。
2:dao不负责处理筛选。
3:domain负责筛选,没问题的话,它总是能描述所有的业务操作,尽管有时候(测试时)这些操作作用的对象是虚拟的。
4:service提供更“面向客户”的接口。
5:action的责任仅仅是转发和过滤(非业务性过滤,如不准输入空白,不准输入负数)
6:如果开放成服务,应该开放service那一层。
以上是个人的一些理解,望大家指正。

"暴露领域模型(Exposed Domain Model)的不均匀性是设计师应当极力避免的,应当为所有的业务逻辑提供一个统一的接口,这是为了消除紧耦合付出的代价,所以设计师应当欣喜得接受它,而不用为此自寻烦恼"
暴露领域模型(Exposed Domain Model)的不均匀性是设计师无法回避的,设计师的职责是如何以有序的方式处理这种不均匀性,提供结构平衡,层次明晰的接口。拙见。

“2:dao不负责处理筛选。 ”
这一点实际中难于做到,为方便(分页、排序、分组...),一般把在dao都根据domain的传入的参数进行了筛选;
按说不应当这样,但没有办法;

我认为一个重量级的对象的是必要的,我想可以通过多接口的方法来实现(实际中没有做过,一直这样想着),对于一个用户模型User:
public class User implements UserPojo,UserService{}

UserPojo提供getter/setter方法接口
UserService提供isLegalUser()之类的业务接口

在三层间穿行的就是一个User;
但在表现层,arrow紧缩成UserPojo,对应FormBean进行DTO之类操作;
而在Service层,narrow紧缩成UserService,进行业务操作;
在DAO层,narrow成UserPojo进行持久化操作;

反正三层都在一个JVM中,User大一点应当不要紧吧!
我想这样可以解决部分领域模型不均匀的问题吧?均匀与否只在于你在哪个层采用哪个接口!


[该贴被mentat于2008-07-08 12:15修改过]

在Jimmy Nilsson的Applying Domain-Driven Design and Patterns: With Examples in C# and .NET这本书中

I think there are few things to note regarding how I use layering now compared to my old style. First of all, the Application layer isn't mandatory; it is only there if it really adds value. Since my Domain layer is so much richer than before, it's often not interesting with the Application layer.
Another difference is that instead of all calls going down, the Infrastructure layer might "know" about the Domain layer and might create instances there when reconstituting from persistence. The point is that the Domain Model should be oblivious of the infrastructure.

不要动不动就用这种标题,好像中国就你一个NB的。
[该贴被bmrxntfj于2008-07-09 13:05修改过]

楼上的楼上的那个用装饰器来弄的吧?如果能不继承(实现接口和继承抽象类的统称),我觉得最好不继承,用组合进一个实例的办法来弄更合适合。不过实现pojo的接口其实没多大意思,无非是一些setter/getter,更改的时候还写一些乏味的setter/getter,实在不舒服,我觉得继承这个类是可以的,而把另一个Manager用来接口。
就楼主的那个问题:getPersonByName,如果我就是要看看叫这个名字的人(比如freebox)究竟都是哪些人,这里面的service和domain就拥有了同样的接口,但是有时候操作的目的不只是看看而已,比如这样:我要看看叫freebox的人都有哪些,但是其中有一个吉林的家伙很讨厌,被系统认为是“坏用户”,系统不想让别人看到他以免其他人学坏,这里就需要做过滤操作,domain的接口不仅仅是getPersonByName,可能是getGoodPersonByName,但过滤这一细节不必交给客户,客户没有这样的要求,这里的service和domain就不具备相同的接口,并且过滤规则可以变化,也许吉林的freebox在10天之后变成好用户了,另一个变成坏用户,这些都是domain的事。
[该贴被freebox于2008-07-09 18:18修改过]

不好意思,忘记了多种对象协作完成的业务操作,那只能用Facade了,做一个Service来提供业务接口
那这样还不如都用Facade,组合优于继承嘛
郁闷
提供setter/getter还是有一点好处的,可以在需要的地方把1换成男,把0换成女,把String换成Date

其实不均匀性也好理解与解决,在于要合理地划分出业务层与领域层,领域层与表现层语法与语义的差别要靠业务层来进行填补,差别有大有小,填充料自然有薄有厚,不过我觉得条件允许的情况下,还是提供一个平滑的业务层比较好,不要让其他层的使用者过分惊讶

I think there are few things to note regarding how I use layering now compared to my old style. First of all, the Application layer isn't mandatory; it is only there if it really adds value. Since my Domain layer is so much richer than before, it's often not interesting with the Application layer.

这段与正在讨论的话题有关
Another difference is that instead of all calls going down, the Infrastructure layer might "know" about the Domain layer and might create instances there when reconstituting from persistence. The point is that the Domain Model should be oblivious of the infrastructure.

这段似乎没有关系

@GUANPEI
我顺带着就拷过来了。

另外我认为有写地方得注意下:

一、Infrastructure做为ddd层次的最底层,绝对不能违反分层的基本原则,为了在Infrastructure中使用
Domain中的对象而让Infrastructure去引用Domain.解决办法:元数据映射
在此指出:Jimmy Nilsson的Applying Domain-Driven Design and Patterns: With Examples in C# and .NET这本书中
“Another difference is that instead of all calls going down, the Infrastructure layer might "know" about the Domain layer and might create instances there when reconstituting from persistence. The point is that the Domain Model should be oblivious of the infrastructure."
我认为这样不妥,明显违反分层原则。
既然这样的话,传统的Dao模式就有点麻烦了。
领域实体到了Infrastructure就被打散成基本元素(这里没有使用元数据映射)。Infrastructure跟其他层也是同样的问题。

三、作为第二条的推论,一些基类、接口比如:IEntity,应该放在Infrastructure中,可以看下ddd书中关于分层的那张图,Domain有继承Infrastructure的箭头.
这里.NET Domain-Driven Design with C#: Problem-Design-Solution书的作者Tim也讨论了这点。
http://www.codeplex.com/dddpds/Thread/View.aspx?ThreadId=30532

四、Application不是Domain的外观,所以通常情况可以直接使用Domain,而不是再在Appliction中再封装,这也不符合ddd的分层。可以看下ddd书中关于分层的那张图,UI是直接可以调用Domain的.
Jimmy Nilsson的Applying Domain-Driven Design and Patterns: With Examples in C# and .NET这本书中
"I think there are few things to note regarding how I use layering now compared to my old style. First of all, the Application layer isn't mandatory; it is only there if it really adds value. Since my Domain layer is so much richer than before, it's often not interesting with the Application layer."

五、仓储应该是internal访问,客户应该使用Service.不应使用Repository.
[该贴被bmrxntfj于2008-07-10 23:55修改过]