域对象 & 面向对象 & 结构化编程

上一篇 / 下一篇  2011-07-26 19:43:54 / 个人分类:java编程

1. Domain Object的重新提出的背景

Domain Object并不是一个全新的概念,而是继承以前的纯面向对象开发的思路。
由于当前O/R Mapping, DAO开发结构的层次划分,导致出现了大量的纯粹数据对象。这些数据对象只带有getter, setter属性,而不具有属于自己的方法,起着Data Transfer Object的作用。
Domain Object 则是重新提出并进一步探讨纯面向对象编程的概念:对象不仅应该具有数据,而且应该具有自己的方法。

这个过程和Spring的出现过程很像。
EJB时代之前,大家本来就是采用着轻量编程模型,只是那个时候,轻量编程架构还不成体系。EJB时代中,还坚持轻量编程模型,难免被看作顽固保守。EJB时代之末,轻量编程架构Spring大获成功。

 

2. Domain Object的划分准则

Domain Object是纯粹的OO对象(这话说起来有些别扭 :-)。
Domain Object的划分就是Object属性和方法的划分。这个划分有没有准则?我的看法是,没有准则。
有这样的说法,面向对象的业务划分,就是根据实际生活中的具体事物进行划分。这可以作为一个大的指导原则,但没有具体的可操作性。
让程序中的Object完全映射实际生活中的Object,是人类的一个伟大理想,是人工智能,是虚拟现实。
下面举个例子。比如,withdraw, deposit两个方法,是放在Account类的里面还是外面?
方案一:
Account代表我的账户,代表我的一个身份,那么当然是一个主动的对象。
Account取钱,存钱是里所当然的。withdraw, deposit两个方法应该是Account的方法。调用方法:
account.withdraw(money)
account.deposit(money)
方案二:
Account就是户头,就是一个被动的金额数据记录。
User每次申请银行管理机构(出纳),AccountManager来操作这个Account。
调用方法:
AccountManager.withdraw(account, money)
AccountManager.deposit(account, money)

 

3. 面向对象的真正意义 – 多态

面向对象的真正意义并不是为了能够方便的把数据和操作封装起来,映射一个实际业务中的对象。如果是为了这个目的,我们永远找不到一个可操作的准则。
比如,上面的两种划分方法,在实际的类结构设计中都存在,都有一定的道理。而且还存在言之成理的更多的其他划分方法。

面向对象的真正意义,在于处理多态
上面的两种划分方法,如果只存在一种Account类型的情况下(比如只有银行柜台账户),那么编程上没有根本的区别。只是这个加钱、减钱的动作的位置不同 -- 是在Account里面做,还是在AccountManager里面做?
在有多个Account类型的情况下,那情况就大不一样了。
比如,有电子信用卡远程帐户 ECardAccount,有柜台存折账户PaperAccount。这两种账户的业务规则都是不同的。
比如,电子信用卡远程帐户的取钱,要收取一定比率的手续费,而柜台存折账户就不需要。

这个时候,两种划分方法的编程上的优劣,就体现出来了。
按照第一种划法方法(姑且成为Domain Object法),只要为相同的Account接口,实现两个不同的类,ECardAccount,PaperAccount,分别实现不同的withdraw,deposit就可以了。
第二种方法,就有些麻烦了。需要在AccountManager里面用一个 if else,或者switch来判断Account的类型,是ECard信用卡,还是Paper存折。

没错,多态就是用来消除if else, switch的。把接口从具体实现抽取出来的目的,就是为了实现上的多态。这个例子也很简单,属于所有OOP课本的第一个入门例子的级别。
这里不厌其烦地举出这个基本例子,就是为了说明:如果有多态的需求,那么应该使用Domain Object,如果没有多态的需求,那么随便,怎么样方便痛快,就怎么设计。毕竟,我们追求的最终目标是清晰、明快、简洁的代码,而不是为了符合某种经典结构。

 

4. 系统分层分包 & 类、包之间的交叉循环引用

我们还是看上面的例子,假设只有一个Account类型。
按照第一种划分方法,withdraw和deposit两个方法都在Account类里面。
假设withdraw方法需要根据金额大小,去查另一个数据表Fee费用表的费率,以便计算手续费。account.withdraw()方法还需要调用FeeDAO的方法,或者由一个代理调用。不管是采用什么方式,account和其他类之间的关系就复杂起来。层次调用关系也复杂起来。account同时是数据对象,也是业务对象。
account的获取和使用过程如下:

Account account = AccountDAO.getAccount(...);// DAO引用了Account
account.withdraw(...);// 其中调用了FeeDAO, Account引用了DAO

假设account处于business层。我们看到,business层和DAO层之间出现了循环关联引用。DAO -> business -> DAO
当然,Account, AccountDAO,FeeDAO都可以是接口。而接口之间的交叉或循环引用,在面向对象设计中,是无可厚非的。比如,著名的Observer模式的Observer和Observable接口之间就是典型的交叉引用。

不过,我的本人习惯是,这种类之间、包之间、Jar之间的交叉循环引用,应该尽量避免。不为别的,就为了所谓的Unit Test,类、包的裙带关系也是越少越好。

我们再来看,按照第二种划分方法的情况,withdraw和deposit两个方法都在AccountManager类里面。
Account属于Data Transfer Object层,AccountManager属于business层。
我们来看Account的获取和使用过程。
Account account = AccountDAO.getAccount(...); // DAO引用account
AccountManager.withdraw(account, ...); //里面调用FeeDAO,

我们看到,business -> DAO -> DTO。层次之间没有交叉循环引用的情况。

 

5. 面向对象 vs 面向过程

面向对象,还是面向过程,这是个典型的关于方法论的争论话题。
有这样的观点,如果一个程序员从一开始就是用Small Talk这样的纯面向对象语言,而不是从C这样的过程语言转过去,那么就能够建立良好的面向对象思维。
我想,这也许是对的。但这里似乎有一种隐含的意思,好像面向过程的思维习惯是一个根深蒂固的痼疾,是阻碍面向对象方针贯彻的万恶之首。
以至于有这样的趋势,全面否认了面向过程编程的经典设计思想和丰富遗产。
我觉得,这对面向过程编程来说,是不公平的。至少对于C++, Java这种半面向对象语言来说,面向过程编程的基本功也是很重要的。很多情况下,OO用不好的原因,恰恰是因为面向过程编程的基本功不过关。
其实,系统分层这个思路,就是来自于面向过程编程的最基本原则 – 库函数的设计要上层调用下层,层与层之间不能交叉调用依赖。比如,操作系统内核,系统函数库,应用函数库的设计。
基本功这个东西,一点都不酷,一点都不时髦,但这是立身之本。

Domain Object号称让程序中的Object完全映射实际生活中的Object,基本上是吹嘘成分居多。在现实生活中,任何一个可以由语义表达的概念都是一个Object,也就是说不仅Account是一个Object,甚至是Withdraw和Deposit也是一个Object。但方案一和方案二都没有反映出Withdraw和Deposit的Object概念,可见这个Domain Object实在是不够Domain Object。

在OO层面,将数据以及相关操作封装在一起是理所当然的基本概念,但在Domain层面这一概念则完全行不通,与OO层面相比Domain层面数据与相关操作之间的关系几乎可以说是脆弱不堪的。举个简单的例子:电信业务的计费方案,通话数据几乎一成不变,但计费规则却是根据市场情况不断进行调整。对于这种情况,方案一和方案二都无法满足要求。

Domain Object最大的错误就在于将OO层面的封装概念原封不动照搬过来,然而事实上Domain层面的数据和操作之间并不存在任何必然的关系,这些关系今天有,明天就可能消失,后天又可能会再跑出来。

 

偶觉得Withdraw和Deposit是不是object应该看需求而定,即:是不是需要把这两个东西看成是object。简单的情况把这两个东西看成是domain object的方法MS也蛮合理的。
如果需要的话,Withdraw和Deposit这两个object应该具有一些什么属性呢?偶想不出这样的情况。而且partech那篇《Domain Model 探索》的帖子里面提到如何建模业务中的活动,将所有的业务活动都建模成一个Act类,偶觉得就是你所说的把Withdraw和Deposit看成object的情况,不知道偶有没有理解错。

 

应该说将业务实体看作被动对象,而不包含业务方法,程序还是可以运行的,并且也可以达到业务需求。
然而,面向过程到面向对象的转变正是讲操作结构的方法同该结构合并而得来的。
实际上你采用方案2,那么你的程序结构的范式就是面向过程的。只不过现在
你“操作结构”的方法可以放到一个叫类的东西中。
相反,将操作结构的方法同该结构合并,你可以得到“智能的”对象,该对象知道自己能完成什么操作,具有什么样的职责,同时它也可以向其他对象发送消息或事件,来完成特定的任务。

将业务实体看作是主动还是被动。这是程序范式的选择,面向过程的范式已经相当成熟,但面向对象的范式更加具有诱惑力,当然前提是你能熟练的运用只有面向对象才能提供的接口,继承,多态等概念和一些面向对象的原则如:DIP,单一职责,开闭原则,里氏替换原则等等。

 

我看了Partech的Domain Object文章,编程模型步步深入,并提出了关联、解耦方面的见解。后面其他网友的讨论,也不断揭示出新的视角和观点。颇有理论和实践价值。
但看到后来的讨论有些拘泥于一些名词、概念、定式。我觉得,由于立场和视角的不同,还有现实问题的复杂性,纯粹概念的争论通常很难有结果。
有感而发,就写了这个帖子。意在说明,Domain Object的划分标准没有定则,关注点应该放到 多态需求 这个重点上,而不是 符合经典范式 这个重点。

我的设计思路的选择如下:
1. 如果有多态的需求,不管类关系、层次多么复杂,即使无法避免类层次之间的交叉循环引用依赖,那么也应该采用 Domain Object 的设计方法。
2. 如果没有多态的需求,那么 Domain Object并不是必需的,可以采用,也可以不采用。
3. 如果没有多态的需求,而且类关系、层次比较复杂。使用了Domain Object,如果不能避免 类层次之间的交叉循环引用依赖,那么,权衡利弊,可以舍弃Domain Object的设计方案。

---

这也是我对关联的看法,类与类、package与package之间,最好是单向关联,尽量避免双向关联。即使无法避免,也尽量让双向关联存在于同一个package中。
比如,订单 <--> 产品; 存货 <--> 产品。两组双向关联,涉及的范围就很广,把订单和存货两个没有之间关系的类,也相互依赖了。
订单这个类编译的时候,需要产品这个类的存在,产品这个类的编译又需要 存货这个类的存在。三者之间没有任何一个类能够单独编译,必须连在一起编译。

如果只是 订单 --> 产品; 存货 --> 产品。两组单向关联,那么至少产品这个类是可以单独编译的。

我的做法是避免关联。订单、 产品、存货 都可以单独编译,不相互依赖。它们之间的关系单独抽取出来,这个抽取出来的关系分别和这些类关联。过犹不及,我的这种方式也不见得好,比如,就没有办法使用关联对象的一些优势了。

类之间的关联是这种情况。package之间的关联也是这种情况。
一般来说,上层package依赖于下层package,而下层package不依赖上层package,之间也是单向关联。
如果package之间出现了交叉循环引用依赖关系,那应该是设计上的一个大失误。(如果jar之间出现了交叉循环引用依赖关系,那就是更大的设计失误)
一般的做法是这样,
如果下层package需要接受上层package的接口,就是说,上下两层package都需要使用同一个interface进行控制传递,那么可以把这个通用的interface再单独分离出来,成为一个更下层的package,由上两层引用。
比如,
business package里面的一个类,businessA 的方法里面有这样的dao pakage调用。
daoA.findA(..., filter). 其中的filter是一个结果过滤条件接口IFilter的实现。
那么这个IFilter应该放在哪个package里面合适呢?

如果放在business package里面,而daoA类在dao package里面,用到了IFilter,那么dao package就依赖于business package。dao和business package之间就产生了交叉循环引用依赖关系。两个Package必须一起编译。没有任何一个package可以单独编译。

如果放在dao package里面,就可以避免这个问题。当然,IFilter这个接口从业务角度来看,不一定适合放在dao package里面。那么,可以多分出来一个package, 叫做basic 或 util,把IFilter放到里面。现在我们又要注意避免 basic 或 util 这个package引用上面business, 和 dao两个package的定义了。

在类、package很多的时候,尤其应该关注这个问题。

 

部分同意关于多态的观点。
但是我认为你举的这两个方案涉及了过多的实现的细节,不能说明Domain Object的差别,事实上在我看来这两种实现方式所对应的Domain Object很可能都是一样的。

业务建模层面的对象和设计的对象可以完全不同的。

如果存款、取款这两种方法仅仅涉及到一个帐户,那么我认为在建模时应该将其归到“帐户”对象下。
但是如果存款、取款涉及到多个对象(例如partech提出的那种业务活动)那么是不能将这种方法归到任何一个单独的对象里面。

从建模的角度来说,我的做法和partech的做法是一样的,建模成为一个业务活动及其持久化信息(反过来也可以说有一个业务对象及其相关控制逻辑)。

 

我简单的浏览了这几天关于Domain Object的讨论(讨论串太长,用语太晦涩,实在没有办法仔细读),我觉得其实在讨论的核心问题就是我们究竟应该用Rich Domain Object呢? 还是应该用Thin Domain Object呢?

Rich的方案主张把关于Domain Object相关的逻辑操作都绑定到Domain Object上面,Thin的方法主张分离逻辑操作,使得Domain Object变得比较单纯的数据类。从O/R Mapping兴起之后,本来大家没有太多疑惑的了,但是Martin Fowler突然批评Thin的方法是贫血的Domain Object,使得很多人又开始疑惑起来。

我的主张是Thin Domain Object的,但是Thin Domain Object <> 只提供getter/setter的实体类,关于逻辑方法操作究竟如何划分,何者划分到控制类,何者划分到实体类,我有一个原则:

有状态的操作划分给实体类,无状态的操作划分给控制类。

为什么要这样划分,大家可以去仔细想想,实体类是带状态的,那些有状态的操作和实体的对象实例有着非常紧密的耦合关系,所以耦合在一起是合适的;而那些无状态的操作,他们针对的是类的操作,与对象实例并无紧密耦合关系,因此解耦是合适的。

 

经过一轮舌战之后,战局又开始出现向空对空进一步演化的趋势,为了避免本贴的继续沉沦,大家还是就事论事比较好。

还是以楼主的Account例子为基础,稍微加入变化以更接近真实情况。

我们有一个简单的银行系统为客户提供存取服务,根据市场情况经历了三个阶段的演变。

第一阶段,我们只有一种类型的客户,只提供单一的withdraw和deposit业务。
第二阶段,决定增加一种VIP客户,提供VIP专用的VIPWithdraw和VIPDeposit业务,原来的客户转化为普通客户,原来的业务也转化为NormalWithdraw和NormalDeposit。
第三阶段,再增加一种中层客户,Withdraw业务享受VIP服务,Deposit则采用Normal服务。

这个系统虽然简单,但也算是有代表性的,各位不妨尽情发挥。

 

我想说的是,我们无法预测未来,所以我们唯一知道的是Normal和VIP服务肯定会有差异,但无论差异是多是少、会不会在未来有所改变,都应该不会影响我们的架构设计。

坚决反对这样的架构设计……
要么过于抽象,无法适应实际的需要
要么过于繁复,实现成本太高


TAG:

 

评分:0

我来说两句

日历

« 2024-05-02  
   1234
567891011
12131415161718
19202122232425
262728293031 

数据统计

  • 访问量: 1238
  • 日志数: 8
  • 书签数: 9
  • 建立时间: 2011-04-19
  • 更新时间: 2011-07-26

RSS订阅

Open Toolbar