|
锁定老贴子 主题:域对象 & 面向对象 & 结构化编程
该帖已经被评为精华帖
|
|
|---|---|
| 作者 | 正文 |
|
最后更新时间:2006-09-11
本来我尽量避免关于方法论方面的主义之争,但一些话如骨鲠在喉,不吐不快。
软件领域方法论大师的著作发人深省,通常代表着软件开发的未来模式。当然,我们在读大师之后,掩卷沉思之余,最好也保持自己的独立意见。 希望本文能够唤起一些对基本概念和基本功的重视(追逐新潮概念之余,同时也固本培源 :D)。 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用不好的原因,恰恰是因为面向过程编程的基本功不过关。 其实,系统分层这个思路,就是来自于面向过程编程的最基本原则 – 库函数的设计要上层调用下层,层与层之间不能交叉调用依赖。比如,操作系统内核,系统函数库,应用函数库的设计。 基本功这个东西,一点都不酷,一点都不时髦,但这是立身之本。 祝大家新的一年,与时俱进,固本培元。:-) 声明:JavaEye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
|
|
| 返回顶楼 | |
|
最后更新时间:2005-01-02
从这个文章看不出楼主是直接支持哪一种形式,好象是支持第二种多一点?
关于DomainObject需要依懒xxxDAO,我看也没什么不好,在设计别的应用时(比如字处理,绘图)没有xxxDAO的问题,是因为对象是直接从内存而来,new出一个对象时所用的“DAO”就是java的“内存对象DAO”,而DomainObject是从DB而来,java没有对应的内置机制直接从数据库new,所以这个xxxDAO要由我们来写,所以在DomainObject中用xxxDAO是很合理的一种做法,这个xxxDAO只是java内置内存PersistentManager的一种DB版本,比如以下的DomainObject版的Account, 在一生成就注定要存入数据库的: [code:1] public class Account { public Account(String name) { _name = name; Global.persistentManager().startTracking(this); //置于session的控制之下,以便能随时和DB同步 } } [/code:1] |
|
| 返回顶楼 | |
|
最后更新时间:2005-01-03
我用的是方案2,必须承认违背了一些面向对象的原则,但用起来感觉也不错,至少开发起来速度挺快的。对于Domain Object里面包含业务方法,不太明白该怎么做,能带来多少好处,所以一直关注partech的文章。
不过我想不论怎么做都会是各有利弊吧,毕竟技术还是这些技术,只是思路不同。 |
|
| 返回顶楼 | |
|
最后更新时间:2005-01-03
buaawhl 写道 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) 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层面的数据和操作之间并不存在任何必然的关系,这些关系今天有,明天就可能消失,后天又可能会再跑出来。 |
|
| 返回顶楼 | |
|
最后更新时间:2005-01-03
age0 写道 举个简单的例子:电信业务的计费方案,通话数据几乎一成不变,但计费规则却是根据市场情况不断进行调整。对于这种情况,方案一和方案二都无法满足要求。
这种情况你可以使用DIP,通过定义一个计算费用的接口来实现。 将Act作为该接口的唯一参数。随着市场的不断调整,你只需要调整,Operation上层的Policy层的类就是了,只要打电话的模式不变,打电话Act就可以不变。 age0 写道 Domain Object最大的错误就在于将OO层面的封装概念原封不动照搬过来,然而事实上Domain层面的数据和操作之间并不存在任何必然的关系,这些关系今天有,明天就可能消失,后天又可能会再跑出来。 赫赫,看样子你小子还是个彻底的不可知论者呢?! |
|
| 返回顶楼 | |
|
最后更新时间:2005-01-03
引用 在现实生活中,任何一个可以由语义表达的概念都是一个Object,也就是说不仅Account是一个Object,甚至是Withdraw和Deposit也是一个Object。但方案一和方案二都没有反映出Withdraw和Deposit的Object概念,可见这个Domain Object实在是不够Domain Object。
偶觉得Withdraw和Deposit是不是object应该看需求而定,即:是不是需要把这两个东西看成是object。简单的情况把这两个东西看成是domain object的方法MS也蛮合理的。 如果需要的话,Withdraw和Deposit这两个object应该具有一些什么属性呢?偶想不出这样的情况。而且partech那篇《Domain Model 探索》的帖子里面提到如何建模业务中的活动,将所有的业务活动都建模成一个Act类,偶觉得就是你所说的把Withdraw和Deposit看成object的情况,不知道偶有没有理解错。 |
|
| 返回顶楼 | |
|
最后更新时间:2005-01-03
jeffrey_he 写道 我用的是方案2,必须承认违背了一些面向对象的原则,但用起来感觉也不错,至少开发起来速度挺快的。对于Domain Object里面包含业务方法,不太明白该怎么做,能带来多少好处,所以一直partech的文章。
不过我想不论怎么做都会是各有利弊吧,毕竟技术还是这些技术,只是思路不同。 应该说将业务实体看作被动对象,而不包含业务方法,程序还是可以运行的,并且也可以达到业务需求。 然而,面向过程到面向对象的转变正是讲操作结构的方法同该结构合并而得来的。 实际上你采用方案2,那么你的程序结构的范式就是面向过程的。只不过现在 你“操作结构”的方法可以放到一个叫类的东西中。 相反,将操作结构的方法同该结构合并,你可以得到“智能的”对象,该对象知道自己能完成什么操作,具有什么样的职责,同时它也可以向其他对象发送消息或事件,来完成特定的任务。 将业务实体看作是主动还是被动。这是程序范式的选择,面向过程的范式已经相当成熟,但面向对象的范式更加具有诱惑力,当然前提是你能熟练的运用只有面向对象才能提供的接口,继承,多态等概念和一些面向对象的原则如:DIP,单一职责,开闭原则,里氏替换原则等等。 |
|
| 返回顶楼 | |
|
最后更新时间:2005-01-03
我看了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很多的时候,尤其应该关注这个问题。 |
|
| 返回顶楼 | |
|
最后更新时间:2005-01-03
partech 写道 jeffrey_he 写道 我用的是方案2,必须承认违背了一些面向对象的原则,但用起来感觉也不错,至少开发起来速度挺快的。对于Domain Object里面包含业务方法,不太明白该怎么做,能带来多少好处,所以一直partech的文章。
不过我想不论怎么做都会是各有利弊吧,毕竟技术还是这些技术,只是思路不同。 应该说将业务实体看作被动对象,而不包含业务方法,程序还是可以运行的,并且也可以达到业务需求。 然而,面向过程到面向对象的转变正是讲操作结构的方法同该结构合并而得来的。 实际上你采用方案2,那么你的程序结构的范式就是面向过程的。只不过现在 你“操作结构”的方法可以放到一个叫类的东西中。 相反,将操作结构的方法同该结构合并,你可以得到“智能的”对象,该对象知道自己能完成什么操作,具有什么样的职责,同时它也可以向其他对象发送消息或事件,来完成特定的任务。 将业务实体看作是主动还是被动。这是程序范式的选择,面向过程的范式已经相当成熟,但面向对象的范式更加具有诱惑力,当然前提是你能熟练的运用只有面向对象才能提供的接口,继承,多态等概念和一些面向对象的原则如:DIP,单一职责,开闭原则,里氏替换原则等等。 你看看,刚说过不要把Domain层面的数据与行为纠缠在一起,你又来个什么“将操作结构的方法同该结构合并,可以得到‘智能的’对象”,还要加上什么面向对象的范式、DIP、单一职责之类的无聊原则。 jeffrey_he使用的方案二虽然本质上和方案一没什么区别,但总算知道要将行为和数据区别对待,这是可喜可贺的进步。OO相对PO来说确实是一定程度的提升,但这种提升完全是建基于PO之上的,可以说是PO的进化和补充而非PO的替代品。所以在设计时不需要拘泥于什么OO原则之类的条条框框,该PO的就PO,该OO的就OO。 现在我们将方案二用Director模式改造一下成为方案三 对象组成: Director,Account,WithDraw Result Director.Withdraw(int AccountID, int money) { Account ac = Account.LoadByID(AccountID); return WithDraw.Withdraw(Account); } |
|
| 返回顶楼 | |
|
最后更新时间:2005-01-03
buaawhl 写道 方案一:
Account代表我的账户,代表我的一个身份,那么当然是一个主动的对象。 Account取钱,存钱是里所当然的。withdraw, deposit两个方法应该是Account的方法。调用方法: account.withdraw(money) account.deposit(money) 方案二: Account就是户头,就是一个被动的金额数据记录。 User每次申请银行管理机构(出纳),AccountManager来操作这个Account。 调用方法: AccountManager.withdraw(account, money) AccountManager.deposit(account, money) 部分同意关于多态的观点。 但是我认为你举的这两个方案涉及了过多的实现的细节,不能说明Domain Object的差别,事实上在我看来这两种实现方式所对应的Domain Object很可能都是一样的。 业务建模层面的对象和设计的对象可以完全不同的。 如果存款、取款这两种方法仅仅涉及到一个帐户,那么我认为在建模时应该将其归到“帐户”对象下。 但是如果存款、取款涉及到多个对象(例如partech提出的那种业务活动)那么是不能将这种方法归到任何一个单独的对象里面。 从建模的角度来说,我的做法和partech的做法是一样的,建模成为一个业务活动及其持久化信息(反过来也可以说有一个业务对象及其相关控制逻辑)。 |
|
| 返回顶楼 | |













