使用qi4j实现DCI架构

我曾经DCI架构是什么?在一文中提到Qi4j框架实现DCI架构比较好,dzone今天就有一篇文章专门谈Implementing DCI in Qi4j

DCI是一种新的构建面向对象应用的方法途径,DCI: Data数据, Context场景, Interaction交互。该文谈了如何使用DCI构建一个REST API。

Roles
DCI核心思想是:对象不是由单个类如POJO组成的,而是使用Roles角色来组合组装功能,再具体一点,当我们在这里说“对象Object”,是指DDD中的实体,而不是值对象或其他。在带有保存数据的企业架构中,实体将被划分为很多Roles,每个Roles有各自的职责目标,这点可以使用Qi4j的Mixin完成(AOP),或者Scala的Traits,否则很难以实现。

Data
DCI的数据代表实体对象中的数据,数据是进行私有Mixin(Private mixin)混合的,不可以被实体对象以外对象访问,Private mixin是一个重要概念,可以在不使用private语法的情况下,让状态成为对象的私有属性。

Context
Context掌握了在一个交互场景中角色与一个特定对象实例的关系映射。COntext可以使用POJO或Qi4j的TransientComposites 实现,后者功能很强大,体现了面向组合的架构特点。

Interactions
交互是作为场景实现的方法,寻找场景中相关的角色,然后激活调用其领域方法,重要一点是,交互的方法应该对应于用户界面表现层的Action方法,也就是MVC中控制器的方法。如果你改变了你的界面,就改变相应的场景和交互。

该文结合了作者项目,演示了DCI的使用情况,这个项目只有三个简单领域模型: Project, User, 和 Task,Task是一种可分配的角色,而Project和 User 是一种分配者的角色,User处于一种被分配的角色,这样,我们可以有一个简单场景叫InboxContext,有一个方法(或者叫交互):assign(Assignable):分配(可分配)。

Project有一个带有Task的InBox,那是我们要分配给用户的Task。但是它既可以被一个User拥有,也可以被一个特定Project拥有,这样就需要角色职责分配,更多职责发现和切分见:对象的责任与职责

切分的目标是达成这样:"assignment"不和这样场景绑定:"查询一个用户的tasks列表, 他们其中一个能够分配给这个用户",而应该是: "查询可分配者的分配Assignments列表, 选择其中一个分配给分配者"。这两者区别就是,与具体用户解耦,从对象职责行为这个新角度来考虑场景,交互和场景与具体类解耦,只和角色有关系,我们就可以在角色这个层次来思考设计。

这种方式可以让程序员侧重角色这个抽象层面来考虑,然后着重了解职责“分配”是如何工作的,而不必着眼于用户,因为用户有用户名和密码 有个人简历等等很多细节资料;如果我们以后使用Calculation替代Task,使用ClusterNode替代User,这样我们就不必修改这个系统,可以复用它了,唯一修改的就是对象和角色的映射,如果熟悉RBAC基于角色权限设计原理的话,应该对这种从角色和职责角度考虑方式的认同。

该文还使用代码表达了如何实现交互,如下代码:


InteractionContext map = new InteractionContext();
RootContext context = assembler.objectBuilderFactory().newObjectBuilder( RootContext.class ).use(stack ).newInstance();
InboxContext inboxContext = context.user( userId ).inbox();

为当前场景创造了RootContext,这是所有场景的根场景,从根场景能够创建子场景InboxContext ,其中userId用来寻找特定的User,如果使用REST实现,这个userId就使用URL实现,比如:"/administrator/inbox" .

为了实现前面"查询可分配者的分配Assignments列表, 选择其中一个分配给分配者",代码如下:


class AssignmentsMixin
implements Assignments
{
@This //自动注入Data
AssignmentsData data;

//选择其中一个分配给分配者
public void assignTo( Assignable assignable, Assignee assignee )
{
assignable.assignTo( assignee );
data.assignments().add( assignable );
}

//查询可分配者的分配Assignments列表
public Iterable<Assignable> assignments()
{
return data.assignments();
}
}

我们如果实现将任务Task分配给某个用户,就使用如下代码:

inboxContext.assignTo( task );

更多代码可见原文有下载。

最后,该文总结了这样做的优点:

以上代码可能多了些,但是能够完成代码的可读性, 代码的可维护性, 易于改变拓展性,如果你使用普通POJO方法,把所有职责放在一个类中,好像很简单了,一旦这个软件项目发展到一定程度,就难以拓展维护。

此外还有优点是可以重用复用,能够避免贫血模型,在目前所谓主流架构Spring或EJB之中,你为了避免将所有行为方法放入一个大类中,将数据放在实体中,将行为分开放到服务Service中(见请问一下这样分层对不对),这实际破坏了封装,就是MF指责的失血模型,问题摆在那里,但是一直没有得到解决, DCI架构解决了这个问题。


[该贴被banq于2010-03-15 15:56修改过]
[该贴被banq于2010-03-15 17:45修改过]

和xmuzyu讨论了以Jive Jdon案例说明对象职责和SOLID原则应用一文中的“阅读次数”到底应不应该属于帖子这个对象的属性,其实这个问题存在很多案例中,“阅读次数”可以说不是帖子的固有属性,帖子这个对象离开“阅读次数”这个属性不是不能存在,探究“阅读次数”这个属性和固有属性比如帖子的名称等是有区别的,属于一种场景属性,也就是说只有在阅读这个场景下才会发生的属性。

DCI架构本质:DCI: 对象的Data数据, 对象使用的Context场景, 对象的Interaction交互行为,我们知道,对象有数据属性和方法行为,以前我们是封装在一个对象中,为什么要封装在一个对象中?因为这个对象在某个需求用例场景中被使用时需要这些属性和方法行为,注意了,这里面有一个关键点,就是对象被使用,以前我们进行面向对象设计,是遵循一种静态原则,因为这个对象被使用需要这些属性和行为,所以,我们在编码时将这些属性和行为写在这个类中。

这个逻辑过程是不对的,那是因为过去程序语言平台落后,导致了我们这种思维逻辑,现在是的思维逻辑是:对象被使用时需要的属性和行为不必一定要在编写代码时写入,而是在运行时再注入或MiXIN混合进去。

这就是DCI架构的本质。

我们还是以“阅读次数”这个案例分析,在以Jive Jdon案例说明对象职责和SOLID原则应用一文我们讨论焦点集中在“阅读次数”到底应不应该属于帖子这个对象,其实这个角度有问题了,如果按照DCI架构和对象角色职责这个架构来考虑,应该这样:

我们除去具体事物如用户和帖子,而是从角色来分析这个场景,就像上贴中存在“分配者”和“被分配者”一样,这里存在两个角色“阅读者”和“被阅读者”,而场景Context是和“阅读者”有关的一个对象,那么一个帖子被阅读的建模描述如下:
第一步:根据用户创建一个阅读场景对象:
ReadContext readcontext = RootContext.create(userId)
第二步:由阅读场景对象来执行交互行为阅读:
readcontext.view(readed);

其中readed被阅读者就是帖子。

所以,按照DCI架构来说,阅读这个行为应该属于Context场景这个行为,只有在这个场景下才会发生阅读这个行为。

如果不按照DCI架构来分析,我们会倾向把阅读这个行为放到“帖子”这个对象中,有人担心,以后再有“顶”这个行为,那么顶的结果数据也要放到“帖子”这个对象中,这里有一个误区,“帖子”不是一个类,不是说所有场景属性结果都放如帖子这个一个类中,帖子只是一个对象群中根实体,我们可以根据不同场景创建不同实体类或值对象,都从属于“帖子”这个根实体,组成一个边界和子领域。

当然,如果有Qi4j或AOP的Mixin来支持,我们也可以使用楼上这种DCI架构方式。

但是个人认为,上面AssignmentsMixin类实际是和业务无关的器具技术类,如果语言本身提供AssignmentsMixin这个混合机制,就不再需要了。

另外,DCI架构中对于我们普通的POJO技术,也就是没有Mixin支持的环境中,最大的借鉴就是引入Context这个场景对象,D和I以前都有,就是对象的数据和方法,通过Context这个对象引入,使的我们的软件更加贴近需求分析中用例场景,四色原型可以说是DCI架构的前言。

Context这个对象其实和角色动作职责有关,如果你不是管理者,你就不可能进入管理这个场景,角色是场景的前置条件,交互动作是场景的必然结果,很符合DBC设计原则。

这应该是面向对象设计领域新的革命思维。


[该贴被banq于2010-03-17 13:13修改过]
[该贴被banq于2010-03-17 13:21修改过]

"DCI架构本质:DCI: 对象的Data数据, 对象使用的Context场景, 对象的Interaction交互行为,我们知道,对象有数据属性和方法行为,以前我们是封装在一个对象中,为什么要封装在一个对象中?因为这个对象在某个需求用例场景中被使用时需要这些属性和方法行为,注意了,这里面有一个关键点,就是对象被使用,以前我们进行面向对象设计,是遵循一种静态原则,因为这个对象被使用需要这些属性和行为,所以,我们在编码时将这些属性和行为写在这个类中。"

这个印证了我们的认识也随着否定之否定规律在变化,囧,原来以前一直是以静态的观点看待事物(类。。)。

2010年03月17日 13:12 "banq"的内容
通过Context这个对象引入,使的我们的软件更加贴近需求分析中用例场景,四色原型可以说是DCI架构的前言。

DCI架构的核心是Context,场景是角色参与具体业务活动的表现,从用例图可以看出,如下图:


这张图是典型的需求用例图,这是通往软件的第一步,通常我们从用例图到软件建模,有一个转换,那是因为过去软件技术不行,只能做妥协转换,而根据DCI架构,我们可以在软件中直接实现用例需求。

DCI架构的场景有时很象SOA中的服务,服务确实为对象被使用提供了场景,所以,很多场景是在提供服务时发生的。但是必须注意到,交互行为属于场景这个范围内,你不能忽视场景这个边界,而直接将交互行为方法写到服务中,这就造成了服务类过大,回到面向过程编程了。见这个有关Spring下的分层讨论


2010年03月17日 13:12 "banq"的内容
和xmuzyu讨论了以Jive Jdon案例说明对象职责和SOLID原则应用一文中的“阅读次数”到底应不应该属于帖子这个对象的属性.....

四色原形讲究某一个人,事,地点(PPT),以某一种角色,在某个时间段完成了某一个行为或者职责,那么这个时候,应该是对象因为具有了某个角色,进入了某个场景,而进入某个场景以后,对象在这个场景下就会出现对应的行为,而行为必然要设计到一些属性,而这部分属性就是“场景属性”,因此对象设计的时候,有两部分属性,一部分是固有的,另外一部分是“场景的属性”。但是目前因为语言的原因,还不能在语言级别支持DCI,如果能在语言级别实现的话,那么就真正实现了“编码是分离,运行时结合的思想”,说到这里,我想到了,EJB也是编程时分离,部署时粘合,运行时候真正统一“,而同时我们领域模型对象和架构也是”设计时分离,实现的时候粘合,运行的时候模型和架构完整的结合“,这些思想好像都是一样的。如何能将一些细粒度的,复用性强的对象通过mixin方式组合,形成一个功能丰富而且符合领域实质的对象,应该是我们所追求的。
[该贴被admin于2010-03-18 08:03修改过]

2010年03月17日 23:34 "xmuzyu"的内容
EJB也是编程时分离,部署时粘合,运行时候真正统一“,而同时我们领域模型对象和架构也是”设计时分离,实现的时候粘合,运行的时候模型和架构完整的结合“,这些思想好像都是一样的

不只是模型和架构在运行时结合,就是模型本身也是运行时拼装的,DCI架构主要是谈后者,因为前者已经在业界成熟多年,比如EJB,不是什么新闻了。

EJB这种架构分离得还不彻底,比如你使用实体Bean或JPA表达领域模型,只能按照我前面讲的静态思维一对一地写死实体类。现在所谓JavaEE包括Java语言都不能很好优雅支持DCI架构。正是JavaEE这样软件平台包括EJB限制DCI架构的实施,我前面批判的软件平台落后性就是指JavaEE标准平台。

传统基于接口继承的OO设计其本质问题在于:从接口到实现的至顶向下过程是概念特化(specialization),比如:从动物特化到狗/猫/鸟;而现实世界除了概念特化还有自低向上的概念泛化(generalization),比如:从狗/猫/鸟抽象出动物。

目前的主流OO语言(Java,C#,C++)的接口继承机制是对概念特化的支持,而并没有对概念泛化的支持。个人理解,DCI中的“角色”带有概念泛化的意味。举个例子:刘国梁原来是乒乓球队员,后来退役当上了教练;那么如果按特化设计,他要实现IPlayer和ICoach接口,一开始就有ICoach不合理,退役还有IPlayer也不合理;如果按泛化设计,在不同的场合(Context),根据其能力他表现出不同的接口。

Ruby等动态语言的Duck Typing机制也可认为是对概念泛化的支持。

2010年03月18日 15:30 "weidagang2046"的内容
DCI中的“角色”带有概念泛化的意味 ...

基本是这个意思,因为角色不断在切换,如果开始套死,就无法应付变化了。

不过,Ruby动态类型不是很被看好,Death by Duck-Typing,Scala还是不错的,性能内存都不错。

2010年03月18日 16:08 "banq"的内容
因为角色不断在切换,如果开始套死,就无法应付变化了

让我想到曾经看过的一段javascript程序,如下

var life = {};
for(life.age = 1; life.age <= 3; life.age++)
{
switch(life.age)
{
case 1: life.body = "卵细胞";
life.say = function(){alert(this.age+this.body)};
break;
case 2: life.tail =
"尾巴";
life.gill =
"腮";
life.body =
"蝌蚪";
life.say = function(){alert(this.age+this.body+
"-"+this.tail+","+this.gill)};
break;
case 3: delete life.tail;
delete life.gill;
life.legs =
"四条腿";
life.lung =
"肺";
life.body =
"青蛙";
life.say = function(){alert(this.age+this.body+
"-"+this.legs+","+this.lung)};
break;
};
life.say();
};

地址:http://www.cnblogs.com/leadzen/archive/2008/02/25/1073404.html

没有类

object就是对象的类型。在JavaScript中不管多么复杂的数据和代码,都可以组织成object形式的对象。

但JavaScript却没有 “类”的概念!

对于许多面向对象的程序员来说,这恐怕是JavaScript中最难以理解的地方。是啊,几乎任何讲面向对象的书中,第一个要讲的就是“类”的概念,这可是面向对象的支柱。这突然没有了“类”,我们就象一下子没了精神支柱,感到六神无主。看来,要放下对象和类,达到“对象本无根,类型亦无形”的境界确实是件不容易的事情啊。

这段JavaScript程序一开始产生了一个生命对象life,life诞生时只是一个光溜溜的对象,没有任何属性和方法。在第一次生命过程中,它有了一个身体属性body,并有了一个say方法,看起来是一个“卵细胞”。在第二次生命过程中,它又长出了“尾巴”和“腮”,有了tail和gill属性,显然它是一个“蝌蚪”。在第三次生命过程中,它的tail和gill属性消失了,但又长出了“四条腿”和“肺”,有了legs和lung属性,从而最终变成了“青蛙”。如果,你的想像力丰富的话,或许还能让它变成英俊的“王子”,娶个美丽的“公主”什么的。不过,在看完这段程序之后,请你思考一个问题:

我们一定需要类吗?

还记得儿时那个“小蝌蚪找妈妈”的童话吗?也许就在昨天晚,你的孩子刚好是在这个美丽的童话中进入梦乡的吧。可爱的小蝌蚪也就是在其自身类型不断演化过程中,逐渐变成了和妈妈一样的“类”,从而找到了自己的妈妈。这个童话故事中蕴含的编程哲理就是:对象的“类”是从无到有,又不断演化,最终又消失于无形之中的...

摘录
“类”,的确可以帮助我们理解复杂的现实世界,这纷乱的现实世界也的确需要进行分类。但如果我们的思想被“类”束缚住了,“类”也就变成了“累”。想象一下,如果一个生命对象开始的时就被规定了固定的“类”,那么它还能演化吗?蝌蚪还能变成青蛙吗?还可以给孩子们讲小蝌蚪找妈妈的故事吗?

摘录
所以,JavaScript中没有“类”,类已化于无形,与对象融为一体。正是由于放下了“类”这个概念,JavaScript的对象才有了其他编程语言所没有的活力。如果,此时你的内心深处开始有所感悟,那么你已经逐渐开始理解JavaScript的禅机了。


[该贴被oojdon于2010-03-19 16:47修改过]

OOJDON这个帖子很有意思,这其实就是函数式语言的一个特点,以场景功能行为为主,而不是以静态的类为主。

事情总有两个极端,Js和Java正好处于这两个极端,实际上,我们发现,实体类还是有其存在的必要,保持其固有属性的特征还是相当多的应用范畴,否则数据库就无从记录数据了。

DCI架构试图在承认固有属性的类特征前提下,动态地引入功能场景这些JS具有的特点,下一个BIG语言,如果在这个方面融合得很无缝,很巧妙,就肯定非常流行。

见讨论:异步架构思维:使用Akka实现领域建模
[该贴被banq于2010-03-23 18:06修改过]

2010年03月17日 13:12 "banq"的内容
现在是的思维逻辑是:对象被使用时需要的属性和行为不必一定要在编写代码时写入,而是在运行时再注入或MiXIN混合进去。 ...

Qi4j:Behavior depends on Context好像就是这个意思
还有Scala:trait

2010年03月17日 13:12 "banq"的内容
第一步:根据用户创建一个阅读场景对象:
ReadContext readcontext = RootContext.create(userId)
第二步:由阅读场景对象来执行交互行为阅读:
readcontext.view(readed); ...

这样做有什么优势,有没有复杂化?

2010年03月31日 20:33 "xinying_ge"的内容
这样做有什么优势,有没有复杂化? ...

我自己来回答,^_^
The key thing I want to achieve is readability of code, maintainability of code, and ease of change. If you instead work with a POJO approach, putting all the code for these roles into one class, that complicates all of the above. Once you get to a certain size of your project, having all code in one place will make it less readable, less maintainable, and harder to change. Much harder...

Another key benefit of doing things this way is that it really enables reuse on a whole new level. You can create roles, contexts and interactions that implement a particular usecase, and then reuse that over and over again in various parts of your domain model. Whether this is useful or not depends on the size and complexity of your project.

oojdon 的回帖太好了
另:
JDON总是在谈论最新的技术 ^^

学习一下