关于业务对象本质的思考(1)

关于业务对象本质的思考

[摘要]:本文基于前人在OO、DDD等领域的研究成果,结合个人工作经验及感悟,对业务对象(Business Object)的本质进行了提炼和总结,并就BO三要素以及BO的获取和验证等问题进行了阐述,旨在加深OOA/D人员对BO的正确认识,分析并设计出更优质的软件产品。

1 引言

对于采用OO思想,并具有N层架构的计算机程序而言,业务对象(Business Object)一般位于业务逻辑层(也叫领域层[1]),作为领域模型元素的一部分,描述了来自于业务域中的一个人、事、物或概念[2],主要用来解决商业逻辑(即业务操作)问题,是为实现当前软件(或系统)的某一(或某些)特定功能而服务的。因此,它仅仅是从当前业务域的角度,对现实世界的一次抽象。

业务对象、业务实体、实体、领域对象在某种程度上是可以互换的术语[此处仅仅说是“某种程度上”,目的在于分析BO三要素的实质,针对于不同的理论,这几个名词确实存在差别,但本文不做赘述]。

2 对象三要素

依据Wikipedia对object的定义[2],对象具有以下三个要素:

■ 标识(identity),是唯一区别其他对象的标志;

■ 状态(state),描述对象所蕴含的信息;

■ 行为(behavior),对象所持有的、描述对象如何被使用的方法。

BO are state and behavior together.下文仅对状态和行为进行阐述,BO的标识不做赘述。

2.1 状态

相对于字段、变量、属性等词,用“状态”一词来描述对象的“性格特点”最为合适。字段、变量、属性等,无非是描述对象状态的不同表现手法。就目前个人对BO状态的理解而言,BO的状态信息有两类:固有信息和动态信息。其中,固有信息是对象从诞生时与生俱来的信息(就像一个新生命的诞生一样,出生年月日、肤色、性别等信息是生来就有的);动态信息,即随着BO的成长,它会走入某一个人生场景中,扮演了某一个角色,从而在这个场景中被赋予了一些额外的信息,如:小明是个学生(从入学时,学生的基本信息被赋予),某天他去市图书馆借阅图书,此时他在借书这个场景中扮演了借书者这个角色,从而具有了借阅证、借阅信息等动态信息。

2.1.1   固有信息

个人认为业务对象在被构建(初始化)后,应达到一个相对比较稳定,且具有一定业务含义的状态,即业务对象的属性应已进行了相应的初始化设置,这样的构建才算合理、完整。与人类世界类似,人在出生时,就已经构造好了婴儿的鼻子、眼睛、手等内容,虽然此时他还没有衣服、母语、身份证等信息,但他在出生时,是一个相对较稳定且有意义的个体,就可以完成婴儿的核心操作,如:哭、呼吸、挥手等。试想,一个对象在构建之后,还需要通过频繁的setter()方法后才可以完成核心业务操作,这是良好封装的表现么?对调用者而言,核心目标是使用BO所公布的方法,用之前还要先setter这个,再setter那个,显然是一种很烦人的,至少setter()这个和那个,不是调用者的本意,更不是调用者的职责

提前构造好该构造好的信息(注意:不是所有的信息都要一股脑的全构造好)是保证BO不被滥用的第一步。此处容易产生疑问,若一个BO在构造时涉及大量的初始信息,也统统的从构造函数里面作为参数注入么?关于这方面的顾虑,我的解答:首先,构造其本质就是初始化BO实例,无关痛痒的new 一个空的或不完整的BO实例一样是没啥意义(甚至是扰乱视听);其次,参数多,说明对象将被构造得彻底,带来更多稳定性,可试图对参数进行分类封装,以减少这方面的纠结(但个人认为,没必要);最后,建议结合spring的依赖注入、Bean的配置等内容进行理解。

2.1.2   动态信息

进入某一场景,扮演某一角色(我更喜欢用“戴上帽子”这个说法),将拥有额外的动态属性。同“固有属性”的构建,某一业务对象走入特定的场景,扮演了另一角色时,也应该将这些动态属性予以设置。一句话,对象应尽量在构建自身的过程中完成自身状态的设置。

Student  trace = new Student(“trace”,1900.10.10,man….);

trace.drink();

IBorrower  borrower = trace.ActAs<IBorrower>();

borrower.borrowBooks();

2.1.3   属性封装

在OO的世界里,封装的概念是最简单的,但却是最关键且最难以把握的。对内部信息的封装是作为合格OOA/D人员必须遵循的最基本原则。

【问】:不封装属性,将业务对象的信息暴露出来,程序正常实现了,好像也没出什么大问题?!

【答】:恭喜你,你通过对那么多唾手可得的信息,实现了业务功能(暂且不说程序代码结构如何,是否内聚/低耦),看上去没有问题。但若需求变化,甚至是高层策略都变化的话,如何应付?

信息的不封装(或封装不完整),已经让太多的程序员吃尽苦头,而且往往都是自己亲手埋下的苦果,并总伴随着“如果重新再来,我肯定不会这么设计”的后悔和无奈。信息不封装(或封装不完整)带来的副作用:

1、内聚,难。业务对象的信息过多的暴露出去,容易滋生强盗逻辑,想捏回去形成一团,难!信息全部都暴露给外界,调用者还需要你BO干嘛?原因很简单:我能够伸手拿到你的任何信息,想实现什么就实现什么,可以为所欲为。如果你还有胆量暴露一些更改BO属性的权限,那我岂不是想怎么改就怎么改。你(业务对象)能控制得了?你还想内聚?做梦!

2、解耦,难。一个成语叫“覆水难收”,放出去的信息将被调用者肆意使用,而且呈现出快速蔓延的趋势,一张复杂的耦合网必然产生。等回头开始重构解耦时,发现堆积如山的代码、耦合似网的结构,已经让你无从下手。动一下,就引起全身阵痛。常见的做法:(1)刨一小块,改改变量名、方法名,移一些代码,用接口再包装一下(隔离嘛),循环的修改,最终发现进展依然是非常缓慢,几乎还没触及到业务核心;(2)先用方法(1)试试,一阵子后,MD,烦死了,直接推倒重新搞。这些做法还需要考虑一个问题:放出去的接口和信息已被调用者大量使用,怎么办?

 

针对上述的苦痛,推荐下面的做法:

1、在对象构建时,把能设置的状态信息尽可能的予以赋值,提前封装;

2、尽可能的不要公布内部信息。能够private的,尽可能的私有。除非迫不得已,尽量不要将setter() 放出,仅使其read only。[个人感受]:以前写代码,从来不顾及private/protect/public,统统public,导致的恶果已经让我吃了好几壶。

2.2 行为

业务行为才是软件真正所关注的问题,对象的行为方式是对象价值的重要体现,也是区别于其他对象的重要标志。因此,我们说“BO因职责而存在!”。

2.2.1   贫血VS充血

关于贫血模型和充血模型的争论从未休止过,但本文仅从BO的角度论述二者的差异(仅为个人之见)。

■ 贫血对象

由不具有任何行为的业务对象形成的领域模型,称为“贫血模型”[2]。对只有属性的getter/setter方法,不具有业务行为的BO,可认为是“贫血对象”。丧失业务逻辑行为的贫血对象,和Value Oject类似,扮演了Data Container的角色,而在业务域中的逻辑操作方面将失去能力(或被遗弃、边缘化)。

■ 充血对象——按大师的说法,与BO直接相关的行为职责将划归到BO中,使其在领域模型中扮演重要的角色。

但是二者不能绝对的说谁好、谁不好,应该一分为二的去看,它们各自具有其特点,应用在不同的场景中。特别地,对于那些需求难以完全吃透、明确,或许用贫血模型较充血模型要更好把控局势。“用户需求——业务——领域”是一个对知识掌握程度递增的过程,领域模型的建立应基于对客观业务域的透彻掌握,不能偏左,也不可以偏右(不就是博弈么?没有最优秀的东西,只存在考虑诸多因素下的较为合适的东西)。

2.2.2   职责单一

在谈“职责单一”前,先说说business core。BizCore是系统核心价值(业务骨架、灵魂)的体现。本人对BizCore的理解:它应该是最精炼、纯粹、简单、直接、轻量级的业务核心。因此,不属于核心业务逻辑范畴的职责和行为(如:持久化操作),尽量抛出去交给该处理它们的对象去处理。[在写下这段文字时,很想再加入DDD的某些概念,但是用一个新概念解释一个原本不太晦涩的概念,实在是不妥]

有了这个边界(或者说原则),再谈谈SRP(职责单一原则)。经常会看到God class(上帝类,你可以认为它就是一个充血充得快爆掉的对象),它几乎可以干所有涉及到它的工作,从而形成代码有几百行乃至上千行的牛X类(本人见过的最牛X类,接近5k行代码)。请问:有了这么一个牛X类,其他类不就成了浮云和鸡肋了么?这样可能引发很多问题:

1、理解难。几千行的代码,没有几个人有耐心阅读,几乎没人能够完全理解其表达的业务含义。我敢保证:这样的类,会对方法名的表意性带来巨大的冲击,如:getFlowDataFromTaliformAfterSave….(),哇靠!这个方法名算好的,更匪夷所思的方法名将让执着的程序员头都要爆掉。

2、修改难。没有很好的理解,如何修改?如何重构?

3、扩展难。丢了它玩不转,不丢它又引来一堆麻烦,对于一个几千行的实现类来说(除非大多都是public static 的方法),吃资源不说,接口隔离、应对扩展方面也是比较吃力的。

2.2.3   谁拥有数据,谁持有行为

BO行为的本质是对BO自身状态的改变,以实现业务目标,并且这种状态的更改可能还需要其他协作者的参与(关联关系)。因此,我们可以说“谁拥有数据,谁就持有更改这些数据(状态)的行为”。

现实中,跟BO有关系的行为可能较多,全部纳入到BO中,会造成BO的臃肿和污染,违背SRP。所以,行为的归属需要遵循一些原则:

■ 可重用度高或是对象固有、与BO状态密切关联的方法放在BO中。

■ 可重用度低或者不是对象所固有(而依赖于特定场景)、与BO状态没有密切联系的方法放在BO管理者或服务层。

上面的原则,和DDD中关于领域模型(domain model)的行为归属类同。甚至我觉得领域模型中的对象,即为最纯粹的BO。

2.2.4   方法属于客户

BO就是一个黑盒子,其中包含了逻辑和数据,而对象的使用者不知道里面有什么数据,也不知道实际的运行逻辑。使用者所能做的就是与对象进行交互,以完成当前的业务目标。因此,对象的行为是为客户而定。

正如前面我们所说,行为(方法、职责)是BO存在的根本,而行为就是为了交互,为供调用者所使用。调用者会根据自己的需要,向它认为应该由谁提供行为的BO发出操作申请,一切都是以“客户(调用者)”为中心而服务的。

2.2.5   行为封装

关于对象行为的封装,有两个层面:

1、千万不要以为自己拥有这么多数据,就可以肆意的发布任何方法,真正被调用者使用的方法其实也很少。跟现实中的人一样,过多的对外暴露行为,别人会把你的信息四处传播,到时候你想改变一下自己的形象,难!因此,仅对外暴露稳定的、合理的、刚刚满足客户需求的API就足够。

2、行为在不同级别的范围内应该封装,仅自己内部使用的话,private就OK了,如果需要让子类持有,protect一下就OK了。总之,在定义BO的行为时,握紧手中的那把尺子,尽量谨慎行事,不要过早的给自己挖坑、负债。

————————以下内容在下一篇文章中涉及————————

3 对象的获取

1、原始资料中的名词获取;

2、抽象角度

3、抽象层次

4、业务域vs领域vs技术域

4 良好BO的验证

5 总结与展望

1、基于数据库表的分析与设计;

2、封装变化

3、OO需要一分为二

4、关于“业务架构师”

6 参考文献

[1] Eric Evans. 领域驱动设计[M] 清华大学出版社

[2]大象:Thinking in UML

[3] http://en.wikipedia.org/wiki/Business_object

[4] http://stackoverflow.com

[5] Martin C Robert. 敏捷软件开发 原则、模式与实践(C#版)[M] 人民邮电出版社 p107

[6]Steve Freeman, Nat Pryce. Growing Object-Oriented Software, Guided by Tests.

posted @ 2011-12-13 21:35  Trace.wu  阅读(2315)  评论(4编辑  收藏  举报