James O. Coplien和Trygve Reenskaug最近发表了系列文章的第一篇,该系列文章介绍一种面向对象编程的新架构方法,该方法基于DCI(数据、上下文和交互)模式。
作者在第一篇文章里主张,即便面向对象编程有助于描述结构,但它并不能充分表明用户的心智模式,因为它不能表示“最终用户的行为需求”。为了说明“行为”到底指什么,他们举了一个储蓄账户对象的例子,该对象可以减少余额,也能提款。按照Coplien和Reenskaug的意思,“这两种行为完全不同”:“减少余额只是数据的特征:它是什么。提款则反映了数据的目的:它做了什么。”事实上,能减少余额在任何情况下都是以数据为特征的——这一点是不变的。相反,提款则涉及“与ATM或审计跟踪之间的交互”——这是动态的,不再关乎“是什么”,而关乎“做什么”。
虽然用户模型很自然地结合了是什么和做什么这两个部分,但“在面向对象里很少有内容会帮助开发人员在代码中描述做什么,在MVC里则根本就没有。”“面向对象将这两部分混在一起”,很难把“简单、不变的数据模型和动态的行为模型区分开来”,但从架构和维护的角度来说,把两者分清楚可是很必要的。另外,纯粹的面向对象要求对大型算法进行分割,将各细分部分(也就是方法)分布到对象中,对象与给定的方法有着紧密的联系。即使有些算法能存在于单个对象中,“有意思的业务功能往往还是会跨多个对象。”
James和Trygve提倡用DCI模型描述这些动态的行为模型,DCI模型基于以下三个概念:
——数据,描述领域对象,表示不变的部分;
——交互,以角色的语言去描述,角色是“对象能进行的行为的集合”; ——上下文,可看成是“一个表格,将角色的一个功能(表格的一行)映射到一个对象方法上(表格的列是对象)。表格根据Context对象中程序员提供的业务智能进行填充,对给定的用例来说,Context知道什么对象应该扮演什么角色。” 作者用转账用例给读者做了具体的说明。转账会涉及储蓄账户和投资账户,但用户在讨论这个用例的时候更喜欢用“源账户”和“目的账户”。这全都是角色,而转账的交互可以用角色的算法描述。上下文不同,角色可以由不同的对象去扮演:在这个具体的例子中,源账户角色将关联到储蓄账户对象上。
代码中能表示角色的一般设计概念是Trait,但Trait的实现依赖于特定编程语言中存在的结构:Scala中的Traits、Squeak Smalltalk中的Squeak Traits、C++中的模板等„„DCI方法的最大好处是,作者提供的示例代码“几乎就是用例描述文字的自然展开”:
比起让逻辑主观任意地跨越许多类的边界,DCI将更容易理解——因为更符合最终用户的心智模型中逻辑的自然组织。
这篇文章引发了很多评论和批评,让James和Trygve提供更多精确的DCI概念。 Michael Feather和很多其他评论者认为,把转账的任务指派给源账户太随意了,这实际上并不符合用户的心智模型,在用户看来,转账并不是由哪个账户来完成的,而是由银行或交易对象完成,“交易对象映射到交互的用户概念”。比如说,John Zabroski建议用分析类TransferSlip。另一些人则认为,DCI所涉及的正是人们早已知道的内容:某些语言中的“Traits”,“函数编程的一般思想(算法比较紧要、应该能表达清楚)”,等等„„
James O. Coplien回应道,DCI“试图结合二十世纪八十年代面向对象里很多很好的领域建模概念,重现过程语言(比如Fortran)过去用算法表达给我们带来的方便性”。像Scala这些语言中的Traits是一种实现方法,但在其它语言里可以用不同的结构去完成DCI架构。实际上,重要的并不是文中建议的工具或使用的例子,而是区分以下内容的架构方法:1)任何情况下都专属于领域对象的行为;2)特定于上下文的行为,这种行为属于业务逻辑,往往跨越多个对象。正如Bill Venners所说,“如果应用的十个用例都涉及账户概念,结果可能每个用例都有一些行为进入了Account类”,这对设计者来说也是个很大的挑战。所以,运用DCI让“对象在每个上下文中有一个不同的类”是“改善OO程序可理解性的一种尝试”:
[„„]这篇文章指出,有时候你可能在对象中放置了太多的行为,因为在不同上下文中要用到这些行为的不同子集。作者建议用Traits对额外的内容进行建模,Traits可以映射到用户心智模型的任务上去。然后在某个特定的上下文或用例中,再往基本的领域对象中添加上下文需要的Traits。
Coplien指出了DCI使代码更易于阅读和调试的四个理由,以强调DCI带来的可读性:
1. 跨业务功能的上下文切换比较少,而且更接近心智模型(基于角色),而不是程序员
模型(基于领域);
2. 包含多态(inclusion polymorphism)几乎完全消失了。调用Foo就得到Foo:而不是
子类型化层次中派生出的众多Foo里的一个。
3. 我可以找到有业务价值的测试点:也就是说,我可以做到真正的BDD(行为驱动开
发)。这更易于开发测试用例,以支持调试。
4. 不太需要进行运行时调试,因为代码在编译时就更加易读。
Trygve Reenskaug强调说,要理解DCI,需要“减少对类抽象的关注,勇于接受更适用于以上对象结构的额外抽象”,还需要“增加一种既能扩充类,又仍然保持对象身份的对象抽象”,即角色。
查看英文原文:Data, Context and Interaction : A New Architectural Approach by James O. Coplien and Trygve Reenskau
DCI是数据Data 场景Context 交互Interactions的简称,DCI是一种特别关注行为的模式(可以对应GoF行为模式),而MVC模式是一种结构性模式,MVC模式由于结构化,而可能忽视了行为事件。我在javascript事件总线一文中也谈过这个问题,Javascript这种函数式functional语言能够帮助我们更加注重行为事件。
DCI可以说是函数式functional编程比如Scala带来的一个理念,The DCI Architecture: A New Vision of Object-Oriented Programming一文(以下简称DCI Architecture)从OO思想根源来深入解剖DCI对传统面向对象的颠覆。
DCI可以使用Scala的traits方便实现,Java中可以使用AOP中的Mixin来实现,也是一种面向组合编程,这点DDD领域驱动框架Qi4j做得比较好。忘记Scala,Qi4J是下一个 Java?
DCI Architecture认为传统MVC只是表达了用户界面交互中的结构,而没有表达交互行为:
它以字处理器中拼音检查为例,拼音检查这个行为功能放在哪里?是dictionary 还是一个全局的拼音检查器呢?无论放在哪个对象内部,都显得和这个对象内聚性不高,由此带来多个调用拼音检查行为对象之间的协作耦合,在DDD中,好像认为这种情况是使用Service服务来实现;在SOA看来,拼音检查属于一种规则,可由规则引擎实现,服务整合流程和规则。
DCI架构则不同于DDD这种有些折扣的处理方法,而是思路复位,重新考虑架构,从对象的数据object Data, 对象之间的协作the Collaborations between objects, 和表达需求用例中操作者角色之间的交互这三个出发点来考虑。个人感觉又把桥模式演习了一遍,其实Qi4j代表的Composer组合模式或Mixin不就是在运行时,把对象以前没有的行为给注射进入,达到根据运行需求搭桥组合的目的。
DCI Architecture也总结了算法和对象的关系,这点在Jdon也曾经热烈讨论过,按照OO思想,应该把算法切分塞进对象中,Eric在DDD一书中也阐述过,不要因为大量算法实现(属于“做什么”),而忽视了“是什么”,我也在函数式编程functional programming的特点 中进行了复述。
当然,算法派还是相当不甘心的,这次总算凭借Scala等函数式语言进行了一次“反扑”,哈哈,DCI Architecture从交互行为入手,提出了如果算法横跨多个对象,不能被切割怎么办呢?这个问题表面上好像提得很好,那么过去我们是怎么解决呢?在SOA中,这种算法被表达为流程 工作流或规则,通过服务来进行聚合(也是一种Composer),所以,是不是可以认为DCI架构是SOA架构的另外一个翻版?
DCI Architecture认为:数据模型data model, 角色模型role model, 协作交互模型collaboration model(算法属于 协作交互模型)应该是程序语言核心关心点,应该在语言层次关注这三个方面。大概这是和SOA区别所在,传统观点:语言一般低于架构,当然,语言和架构遵循水涨船高准则。
DCI Architecture是怎么认为数据模型呢?它认为模型应该是哑的,也就是静止的,所以才叫数据性对象。这个我应该不能认同,如果是这样,数据模型实际上就是失血贫血模式了,只有setter/getter方法的数据模型。
DCI Architecture那么认为角色模式是什么呢?感觉其说得不是很明白,因为它用代码案例来表达,这种从抽象直接跳到具化的思维方式我不是很喜欢,感觉逻辑上无法前后一致,因为对具体实例的逻辑解释有很多。
在两个账户之间转账,DCI Architecture认为在我们一般人脑海中,转账这个模式是独立于账户的一个模型,它应该属于一种交互interaction模型。 由此引入了roles角色模型,正如对象表达它是什么,而角色表达的是有关对象做的一系列行为结合。
角色模型之所以对于我们如此陌生,因为我们以前的OO思维是来自OO程序,而以前的所谓OO程序包括Java/C都缺乏对角色模型的支持。角色介入混合的交互模型其实不是新概念,过去称为algorithms算法(和我们通常数学算法概念有些区别)。
当然我们可以将这些交互行为按照对象边界划分办法细分到一个个对象中去,不幸的是,对象边界本身划分实际上意味着它已经代表一些东西,比如领域知识。目前很少有这方面的建模知识:将算法逐步精化细分到正好匹配数据模型的粒度(然后就可以装到数据模型中,成为其方法了)。如果算法不能精化细分,那么我们就把算法整个装到一个对象中去,这样可能将算法中涉及到其他对象和当前对象耦合,比如上面转账这个算法,如果整合到账户Account模型中,因为转账涉及到其他账户和money对象,那么就将因为行为操作带来的耦合带到当前账户对象中了;当然,如果算法可以精化细分,那么我们把它切分到几个部分,封装成几个对象的方法,这些方法都是无法表达算法算法高内聚性的琐碎小方法,可谓面目全非,实际上,我们过去就是这么干的。
角色提供了和用户相关的自然的边界,以转账为例子,我们实际谈论的是钞票转移,以及源账户和目标账户的角色,算法(用例 角色行为集合)应该是这样: 1.账户拥有人选择从一个账户到另外一个账户的钞票转移。 2.系统显示有效账户 3.用户选择源账户 4.系统显示存在的有效账户 5.账户拥有人选择目标账户。 6.系统需要数额 7.账户拥有人输入数额
8.钞票转移 账户进行中(确认金额 修改账户等操作)
设计者的工作就是把这个用例转化为类似交易的算法,如下: 1.源账户开始交易事务 2.源账户确认余额可用 3.源账户减少其帐目
4.源账户请求目标账户增加其帐目 5.源账户请求目标账户更新其日志log 6.源账户结束交易事务
7.源账户显示给账户拥有人转账成功。
代码如下:
template throw InsufficientFunds(); } else { self()->decreaseBalance(amount); recipient()->increaseBalance (amount); self()->updateLog(\"Transfer Out\", DateTime(), amount); recipient()->updateLog(\"Transfer In\", DateTime(), amount); } gui->displayScreen(SUCCESS_DEPOSIT_SCREEN); endTransaction(); } 以上几乎涵盖了用例的所有需求,而且易懂,能够真正表达用户需求心理真正想要的。这称为methodful role 角色role体现了一种通用抽象的算法,他们没有血肉,并不能真正做任何事情。在某些时候这一切归结为那些表现领域模型的对象。 数据模型表达的“是什么 what-the-system-is”,那么有一个bank和子对象集合account, 而算法表达的“做什么what-the-system-does”则是在两个账户之间转移钞票。 到这里,我有一个疑惑,我们倡导DSL,是希望把“是什么”和“怎么做”分离,这里“做什么”和“怎么做”是不同含义吗?我过去认为算法属于怎么做,属于实现部分,但DCI Architecture却认为它属于“做什么”部分,看来对算法定义不同,算法如果是数学算法规则公式,应该属于“怎么做”(使用算法实现),如果算法属于用户角色的行为,那倒是属于“做什么”问题,但是在DDD中,我们认为“做什么”应该属于“是什么”的一部分,DCI Architecture将其分离。 为什么分离?因为“做什么”和具体用户角色有关,通俗讲,可以看成是人和物相互交互的结果,是一种用例场景,人和物可能有各种交互场景,这就成为Context,是 Use Case scenario的Context。 看来,DCI Architecture是将“是什么”和“做什么”进行分离,然后根据需求在不同场景动态结合,还是桥模式的味道。 DCI Architecture “是什么”问题,哈哈,有点绕人,DCI Architecture自己也是有关“是什么”的。 DCI Architecture一文下半部就是如何实现它的架构思想,是关于“怎么做”的了,建议传统语言在编译时,就将角色的行为或算法混合Mixin到数据模型类中,这是典型的AOP思想。 下图就是DCI Architecture架构把MVC模式肢解,将C和V用对应的Context来替代。 这样,DCI架构真正含义可以归结如下: 1.数据data:是领域对象中代表领域类概念的那部分。 2.场景context:根据运行时即时调用,将活的对象实例带到符合用例需求的场景中 3.交互interactions, 描述需求用户心目中角色的活动算法。 就象上图中,把场景Context看成是一张表,角色行为作为横行加入,而数据模型作为纵行加入。 具体实现,可以在运行时,通过动态反射将业务逻辑行为注射到领域模型对象中,动态语言比较方便,C++ 和 C#使用pre-load预加载,Scala使用hybrid 混合,DCI Architecture一文没有提到AOP,可以使用AOP中静态weave方式混合,现在******it等动态代理框架都支持静态weave,包括AspectJ/Spring,在编译时就将业务行为注射到模型中。 DCI Architecture一文接下来详细介绍了Scala中的traits 是如何实现这一注射的。traits 能够让方法在程序运行时注射到一个对象实例中: trait TransferMoneySourceAccount extends SourceAccount { this: Account => // This code is reviewable and testable! def transferTo(amount: Currency) { beginTransaction() if (availableBalance < amount) { . . . . } } . . . . //通过下面特别的对象创建方式生成符合用例的源账户和目标账户 val source = new SavingsAccount with TransferMoneySourceAccount val destination = new CheckingAccount with TransferMoneyDestinationAccount 个人思考:在代码编译时混合注射已经不是新鲜方式,Spring2.0开始已经可以做到,Scala以一种更易懂代码方式实现,现在需要思考:我们这样做的目的是什么?就是实现Context场景混合,说白了,就是到用户现场烧菜。 条条大路通罗马,为实现这一目标,我们可以采取另外一种方式,用户现场的本质是什么?用户现场为什么是活的,Context为什么是活的?因为用户的动作,动作引发事件,因此,事件模式可能是Context的本质。 如果是这样,只要我们遵循事件编程模型如EDA架构,也许也能实现DCI架构?比如通过Domain Events来激活角色行为: 账户拥有人操作自己的账户(领域模型),这个账户领域模型发出事件,驱动目标账户进行帐目更新,最后返回给账户拥有人,转账成功。 绕了半天,什么OO,什么算法,用事件模式就搞定了? 因篇幅问题不能全部显示,请点此查看更多更全内容