对象的责任与职责

对象和数据的主要差别就是对象有行为,行为可以看成责任职责(responsibilities以下简称职责)的一种,理解职责是实现好的OO设计的关键。“Understanding responsibilities is key to good object-oriented design”—Martin Fowler 。

对象设计:角色、责任和协作"(Object Design: Roles, Responsibilities, and Collaborations)一书对对象的职责进行了完整阐述。

对象过去一直被看成是被操作的数据,这也是失血贫血模式的来由,这还是一种将对象看成数据结构或集合的另外一种表现,对象是有自主行为的,对象是一种类似机器人的概念。

职责的革命影响力
在DDD领域驱动设计中,通常很多人会误将现实中实体完全映射到软件中领域实体,这是一种谬误,现实中实体不是一对一反映到软件中,这其中包括一个抽象过程。我们必须明白:软件领域中一个实体对象通常能够扮演多个现实中实体角色。

由于软件领域中对象根据不同场景可以扮演不同角色,对象的方法可以看成这些角色不同职责的表现,打个比喻:你在家是一个父亲,在单位是一个领导,如果我们建立两个实体父亲和领导就不合适了,没有经过抽象分析,其实只有一个领域模型实体,就是“你”,只不过在不同场景,你扮演不同角色,角色不同反映在角色的权利或职责行为不同,父亲的职责责任与领导的职责责任是不一样的。

同一个领域对象通过不同方法扮演不同角色就带来实现上的一个问题,这就衍生出一种DCI架构,我们建模时,不可能将其扮演的所有角色行为都塞入实体对象中,而是应该根据不同运行场景来动态分配职责,所以,传统OO语言如Java .NET等有些难点,当然可以通过AOP的MIXIN方式变相达到,但是不是很优雅,Qi4J所谓面向组合概念实际就是这样,而Jdonframework则从EDA事件驱动方面婉转来通过事件消息来驱动不同职责响应,最新面向函数式语言如Scala这方面就优雅直接了。

职责概念给OO世界带来巨大革命性变化,使得我们分析需求必须从职责驱动重新看待需求了,DDD一书中分析货运这个案例时,也未能从职责来审视,比如它为运输历史专门建立一个记录对象,如果从职责概念看,运输对象应该知道它自己过去的历史,这是它的职责,所以,运输历史获得应该是运输实体对象的一个方法,而不应该为其单独建立对象。

DDD一书很多方面还是存在数据提取的影子,这是它的历史局限造成的,为了摆脱失血模式纯粹数据对象的影子,对象的职责上升到前所未有的高度。

如何发现职责
如果改变传统将对象看成是死静止的实体概念,将实体对象看成是活的,你就很容易发现其行为职责,现实世界的对象可能是做事情或代表一些信息或东西,但是现实对象不做决定。软件中对象是活的,根据分配给他们的职责能做决定,类似智能机器人。

职责来自于你的软件是如何工作。来自于软件的HOW。寻找和分配职责需要灵感,是一个创造性活动,是一个充满探索冒险发现新奇的乐趣活动,从下面几个方面寻找需求中职责:
1.来自用例分析中顺序图消息发送。
2.构造invention、 约束表达、策略、算法、规格Specification和描述Description都可以成为职责。
3.系统要做的事情或要管理的信息
4.将实体对象看成一个演员(拟人化),扮演一个角色应该知道哪些事情knows something、会做那些事情do sth.,能够控制或决定什么事情。

简明扼要,判断职责的主要就是:它是否知道know这些东西,它是否会做这些东西,或做一些判断决定等。

职责分配
将职责分配给对象,使得对象有形有态。
按照高凝聚原则分配。
使用“如果没有这个职责,会怎样”。
如果发现职责太广泛,不能分配到单个对象中,那么就切分职责,由这些小职责组合成更大职责。

所谓凝聚原则:和DDD中的高聚合概念比较类似,关注类内部;一个类是否充分实现其职责目标?类中方法是否都是为实现这个职责服务的?高聚合代表鲁棒性 重用性和可理解性。

总结
一旦领域对象具有丰富的行为,变成富模型,或充血模型,它实际上就是一个Actor,Actor之间可以通过协作消息进行联系,而Scala的不同于多线程机制的Actor模型底层实现又更好地支撑了这样的富模型,这也是为什么Scala开始非常流行的原因之一。

使用职责来分析需求,建立丰富对象,类似文学中的拟人化手法,将设计想象要赋予现实中静止不会说话的客观事物,当然,不能过,合适即可,目前最大问题是不够。

[该贴被banq于2010-02-03 10:58修改过]

看懂了,从面向对象的设计角度来看这个帖子就很好理解。面向数据库的设计和面向对象的设计最大的不同就是,后者赋予了对象丰富的行为,而什么样的行为应该赋予这个对象,就是职责的所在,或者说提升为需求场景的所在。
[该贴被jeffrey4chartcrm于2010-02-03 12:02修改过]

也需要注意面向对象和面向过程的区别,面向过程是数据和职责功能分离,以职责功能为主,职责功能去操作数据。

使用面向对象的语言并不保证面向对象的思想。如何定义的对象以及如何相互作用才是决定是否面向对象。 所以,你尽管使用Java的SSH组合,如果你把功能职责放在服务Service类中,通过Service驱动DAO操作数据库,操作数据库生成的数据对象,这是典型的面向数据库和面向过程狼狈为奸。

面向对象是将功能职责纳入到对象中,和数据封装起来,形成一个有自主判断和基本逻辑的Actor,就类似于DDD中的聚合和聚合根对象,根对象有职责维持聚合边界一致性的职责。

对于对象职责的分配以前看过一本书<<UML和模式应用>>这本书将的非常好。里面有GRASP原则--通用职责分配软件模式(General Responsibility Assignment Software patterns),它里面讲了以下几个模式:

1 创建者(Creator) :

决定对象应该有谁来创建的问题。一般情况下是包含类创建被包含的类,比如Order创建OrderLine等。

2 信息专家(Information expert):

用此模式来确定如何给对象分配职责的问题。一般把职责分配给那些包含此职责有关信息的对象。这样也体现了高内聚性模式。这里面其实也是将行为和对象的数据统一起来,分配某个对象职责,就看看当前的对象里面有没有完成此职责的数据,如果有就可以分配职责。

3 低耦合(Low coupling)

面向对象的设计,讲究对外封装,对内解耦,一个模块或者一个类,我们暴漏给外界的接口是粗粒度的,经过封装的,而模块或者对象内部需要进行细分,进行解耦,类与类,模块与模块之间耦合度越低,那么可扩展性就越好,维护起来也容易,不会造成牵一发而动全身的局面。

如何使得类于类之间低耦合呢,我觉得迪米特法则非常重要,迪米特法则就是通常说的“不要和陌生人说话”,一般类的行为主要可以由以下几种形式来实现:
>>方法参数中的对象,通过调用方法参数中对象方法来实现职责
>>对象自身的方法,通过对象自身的方法实现职责
>>对象内部聚合的对象,通过对象内部聚合的方法实现职责。
>>方法创建或者实例化的对象方法,通过对象自身创建的对象的方法实现职责

以上四种方式都是我们可以接受的,下面这种方式,我们一般要避免,比如a.methodA().methodB(),调用者对象调用了methodA方法返回的对象的方法,这样就会使得调用对象和methodB所在对象发生了耦合,此时我们可以完全将methodB的调用封装在a里,那么此时调用者就只与a通信,而不是和a,以及methodB所在对象都耦合在一起。

4 控制器(Controller).

控制器模式主要解决将系统事件处理的职责分配谁的问题,控制器模式指出将处理系统事件的职责分配给控制器类,比如MVC模式中的C就属于这种模式,还比如Struts中的ActionServlet这个Front controller(前段控制器 J2EE核心模式一书).

5 高内聚(High Cohesion)

一个类如果是高内聚的,那么类和类之间也将变的松耦合,如果类和类之间耦合太多,那么势必类就丧失了内聚性,因此我们在分配职责的时候,高内聚和低耦合是相辅相成的。

6 多态性(polymorphism)

多态性就是OO中的多态,多态其实也是一种具体依赖抽象原则的体现,比如我们有一个产品有个打折策略,而打折策略非常多,那么这个时候怎么办呢?我们当然是定义一个抽象的打折接口,然后将不同的打折策略实现这些接口,而不是让每个不同的打折策略具有不同的接口。

7 纯虚构(pure fabrication)

我们在面向对象的分析和设计当中,难免会遇到一些行为不能很好的分配给实体或者domain model,那么这个时候就需要虚构出一些概念,比如DDD中的Repository就属于一种纯虚构的东东。

8 间接性(indirection)

间接性从表面就可以知道它是为了解耦的,比如RBAC,通过引入Role将User和Permission解耦,这样通过引入一个间接的对象,使得原来紧耦合的对象变的松耦合,这样也方便维护,同时也是为低耦合原则服务的。

9 防止变异(protected variations)

这个原则我们也经常遇到,按照我的理解,它应该是封装,类似于DDD中的防腐层,软件系统中的子模块或者子系统应该将不确定的因素封装在内部,而不要因为子模块而影响到外部系统。其实这也是一种封装思想的体现。

xmuzyu 讲得不错,这篇帖子我是针对http://www.jdon.com/jivejdon/thread/38041#23126971写的。

主要涉及分析设计中最具创造性最难的一个环节:创造对象。
需求分析越详细越好,如果不能面面俱到,也依靠日后反复迭代,但是个人经验是:用例图 需求顺序图或四色图 状态图或交互图是必须的。

DDD提出了实体 值对象和服务三个模型,这三个模型和代码层面比较接近,很多人疑惑如何从分析层面的四色图到DDD的实体呢?

虽然DDD定义了实体以及聚合根和边界,但是没有详细告诉我们如何在需求中寻找聚合边界和实体,书中主要是以案例为主,没有上升到方法论;而且DDD主要对实体特征定义比较多,如实体必须有唯一标识等,忽视实体的职责定义,感觉DDD的实体和数据库概念实体差不多,这是DDD书籍最大的不足。

从四色图需求DDD的转换过程是一个创造性活动,创新创造就没有模式准则了,但是有对结果的评论标准,标准就是两个:
1.耦合coupling :关注类之间关系;关注协作,对象之间的依赖程度;是依赖的量化;有松耦合和紧耦合;松耦合更易于导入变化。GoF设计模式主要是关注这个。

2.凝聚cohesion : 关注类内部;关注职责,一个类是否充分实现其职责目标?类中方法是否都是为实现这个职责服务的?高聚合代表鲁棒性 重用性和可理解性。这个是我们很少涉及的。知识盲点

从事创建对象时掌握四个步骤和原则,也是每天座右铭:
1.抽象Abstractions 从实际中抽象

2.职责Responsibilities 寻找职责

3.高聚合High-cohesion: 关注特征属性和职责,分配职责,特征属性是否该对象固有特征?职责是否和该对象固有行为?特征属性对应对象的字段;职责对应对象的方法,这样,对象的字段和方法就有了,实体对象就创建出来了。当然这其中可能一些特征属性包装成值对象,形成聚合群,有一个根实体。

4.松耦合Loose-coupling:关注对象间协作,一个大对象划分两个,大对象中职责就变成两个对象的协作。协作讲究约定,如DBC,设定前置条件 后置条件和不变性,这个过程也涉及聚合边界的划分,一些专注做对象内部工作,不需要和外部对象协作的对象职责专门封装起来,比如内部算法,形成单独的算法类,这个算法类也是聚合群中一个类。当然聚合群中,也有类负责专门协作的。

[该贴被banq于2010-02-04 10:03修改过]

2010年02月03日 22:08 "xmuzyu"的内容
以上四种方式都是我们可以接受的,下面这种方式,我们一般要避免,比如a.methodA().methodB(),调用者对象调用了methodA方法返回的对象的方法,这样就会使得调用对象和methodB所在对象发生了耦合,此时我们可以完全将methodB的调用封装在a里,那么此时调用者就只与a通信,而不是和a,以及methodB所在对象都耦合在一起。

--我是很支持这个观点,但好像这种情况不好避免,如果methodB()并非a的职责,我们还是不得不这样做的,不是吗?

2010年02月04日 10:12 "freeren"的内容
如果methodB()并非a的职责,我们还是不得不这样做的

如果methodB()并非a的职责,还是将methodB()从属B,在A中建立一个委托方法,在该方法中委托methodB()实现,这也是一种方法重构吧。

一看到这篇文章就想到了Martin Fowler的《分析模式》,那里面的chapter 2就是关于责任模式的。但是因为是刚刚开始看,加上以前也在jdon里面了解过四色模型,呵呵,对这个主题相当感兴趣!

此主题探讨非常经典,我受益良多呀。

越来越崇拜banq老师了,还有咱论坛里N多厉害角色,哈哈,

mark

其实不赞同你说的:"你在家是一个父亲,在单位是一个领导,如果我们建立两个实体父亲和领导就不合适了,没有经过抽象分析,其实只有一个领域模型实体,就是“你”,只不过在不同场景,你扮演不同角色,角色不同反映在角色的权利或职责行为不同,父亲的职责责任与领导的职责责任是不一样的。"其实角色的不同是体现在职责责任不同,所以最好把这个职责分开,这也是单一职责原则的原理所在。还有就是面向过程和面向对象结合在一起有时候是非常适合的,比如说设计模式里面的命令设计模式,很好的解决了一个动作的抽象问题。

2014-05-21 15:25 "@hexiaogang"的内容
最好把这个职责分开 ...

说的对,在每个场景具有单一职责,某人在单位这个场景下是领导角色,具有一些领导的职责比如签字授权;在家这个场景下是父亲,有父亲的等应有的职责。

这不正好是DCI吗?C++不是正好支持多继承吗?正好对每个职责实现一个实现类,然后具体的实体类全部继承相应的职责实现类,为什么搞那么复杂?