锁定老帖子 主题:再次小结领域模型的种种观点
该帖已经被评为精华帖
|
|
---|---|
作者 | 正文 |
发表时间:2005-12-15
Readonly 写道 robbin 写道 好吧,那么我是web程序员,我从页面传一个id过来,要先load一下该Account,显示account的信息。那么请问:你提供给我什么API?是
account.getAccountById(id); 还是 accountService.getAccountById(id); 还是 accountDao.getAccountById(id); ? new Account(id); 赫赫,这个我倒没想到,估计也得要拦截构造其的返回,返回一个loadedAcount instance . 对于web上来一个accountID ,我还是会在Action中这么写 account = queryDAO.findObjectByPK(Account.class,accountID) ; 得到account对象后,也就可以做你的逻辑了。 |
|
返回顶楼 | |
发表时间:2005-12-15
这个要坚决支持下firebody,刚才又努力研究了一下ror,有点想法:我们的service层和DAO层完全可以合并.负责那些无状态操作.或者就像firebody所说,叫logicobject也行.其他的domain logic完全并入domain object,不觉得这样有什么问题
|
|
返回顶楼 | |
发表时间:2005-12-15
Readonly 写道 robbin 写道 好吧,那么我是web程序员,我从页面传一个id过来,要先load一下该Account,显示account的信息。那么请问:你提供给我什么API?是
account.getAccountById(id); 还是 accountService.getAccountById(id); 还是 accountDao.getAccountById(id); ? new Account(id); new Account(id) 看起来很不错。很有吸引力的一种做法。 只是语义上难以完善。如何处理 ID 不存在的情况?只能使用Exception了。 上述Robbin给出的3种用法都可以用 返回null 表示ID不存在。 |
|
返回顶楼 | |
发表时间:2005-12-15
buaawhl 写道 Readonly 写道 robbin 写道 好吧,那么我是web程序员,我从页面传一个id过来,要先load一下该Account,显示account的信息。那么请问:你提供给我什么API?是
account.getAccountById(id); 还是 accountService.getAccountById(id); 还是 accountDao.getAccountById(id); ? new Account(id); new Account(id) 看起来很不错。很有吸引力的一种做法。 只是语义上难以完善。如何处理 ID 不存在的情况?只能使用Exception了。 上述Robbin给出的3种用法都可以用 返回null 表示ID不存在。 ReadOnly的做法在Hibernate会没有问题,赫赫。 可是在EJB3 / JDO可能会有一些问题。 真是头痛。 因为我们需要一个状态管理,不从EJB3 容器拿出来的话,状态管理做不了。 |
|
返回顶楼 | |
发表时间:2005-12-15
firebody 写道 Readonly 写道 robbin 写道 好吧,那么我是web程序员,我从页面传一个id过来,要先load一下该Account,显示account的信息。那么请问:你提供给我什么API?是
account.getAccountById(id); 还是 accountService.getAccountById(id); 还是 accountDao.getAccountById(id); ? new Account(id); 赫赫,这个我倒没想到,估计也得要拦截构造其的返回,返回一个loadedAcount instance . 对于web上来一个accountID ,我还是会在Action中这么写 account = queryDAO.findObjectByPK(Account.class,accountID) ; 得到account对象后,也就可以做你的逻辑了。 现在问题又来了,我们知道,就算是读数据库操作也应该使用事务(请参考事务的ACID定义),按照你的用法,实际上你把domain object,service object,Dao interface统统暴露给Web层了。这样就有两个问题: 1、你的domain object,service object,dao impl统统都需要配置事务,这是一种非常不合理的用法。事务只是一个业务的横切面,被用在业务逻辑层,现在被你用在了各个层面的各个角落,造成了事务的滥用和扩散。 2、domain object,service object和dao interface所有的底层API你全部都需要暴露给Web程序员,由他们自己去找他们需要的操作究竟散落在domain里面呢?还是dao里面呢?还是service里面呢? |
|
返回顶楼 | |
发表时间:2005-12-15
robbin 写道 firebody 写道 Readonly 写道 robbin 写道 好吧,那么我是web程序员,我从页面传一个id过来,要先load一下该Account,显示account的信息。那么请问:你提供给我什么API?是
account.getAccountById(id); 还是 accountService.getAccountById(id); 还是 accountDao.getAccountById(id); ? new Account(id); 赫赫,这个我倒没想到,估计也得要拦截构造其的返回,返回一个loadedAcount instance . 对于web上来一个accountID ,我还是会在Action中这么写 account = queryDAO.findObjectByPK(Account.class,accountID) ; 得到account对象后,也就可以做你的逻辑了。 现在问题又来了,我们知道,就算是读数据库操作也应该使用事务(请参考事务的ACID定义),按照你的用法,实际上你把domain object,service object,Dao interface统统暴露给Web层了。这样就有两个问题: 1、你的domain object,service object,dao impl统统都需要配置事务,这是一种非常不合理的用法。事务只是一个业务的横切面,被用在业务逻辑层,现在被你用在了各个层面的各个角落,造成了事务的滥用和扩散。 2、domain object,service object和dao interface所有的底层API你全部都需要暴露给Web程序员,由他们自己去找他们需要的操作究竟散落在domain里面呢?还是dao里面呢?还是service里面呢? 1) 我承认,这是缺点。 事务不应该在领域逻辑里暴露,确实需要一个Service Facade来包装。 2) 这点我倒不是很赞同,可以约定俗成,Service 的实现一样存在这样的问题。 现在回到ajoo在某个贴子里提到的一个想法: 从DAO里面load出来后就DI所需的服务,这个做法我一直比较反对,不过赫赫,我现在觉得他的做法是很不错的,我想我会实际采用这种做法以获得足够充血的领域模型。 不过这里有个问题,如果服务的注入发生在DAOImpl中,那么对于lazy load就很难注入服务了,可以通过postLoad callBack支持,赫赫。 另外提到的事务,我想事务的声明和管理应该不是问题。 DAO我会给它事务的声明,这个我想问题不大。只要不在DOmain model上声明就行,至于Service,我是比较反感无缘无故多处得这么一层,当然因为需求所致的除外, 在Web应用中,事物的开关我觉得放在Action.execute也是一种可以考虑的做法。具体的做法可以有很多,比如template method ,spring 扩展等等。 |
|
返回顶楼 | |
发表时间:2005-12-16
事务的控制放在Web Action层是不合适的。因为一个Action的execute方法经常可能跨越多个事务。这里的事务需要特别注意一下,所谓事务并不光光指要么都成功,要么都失败,这仅仅是事务四个属性中的原子性,此外事务还要保证隔离性,对读数据库的隔离性。例如一个Action中执行了四个统计查询的业务bean,我这个时候确确实实需要的是4个事务,而不是合并为一个长事务。还有很多其他的场景,这里不一一列举,结论就是事务控制放在Web Action层不合适。
事务的控制应该放在Service层来实现,而这个Service必须覆盖下面的domain logic和dao logic,否则就出现事务在各个角落的泛滥(这种情况也不是不允许,但是在团队开发中杂乱的事务很容易出问题)。由于事务本身是过程的,就决定了你底层再怎么OO,到了事务控制的Service层,都给我变成了TransactionScript了。这也是我为什么认为贫血模型的合理性。贫血模型是应用程序事务导致的,因为事务是过程性的,所以领域模型一到事务控制层,就不得不贫血了。 为了给贫血模型充血,实际你付出的代价是巨大的。先不论领域模型的post-initialition的问题,一是事务控制的四处泛滥,二是逻辑究竟该放在哪里?DAO里面?domain object里面,还是service里面?没有一个很明确的标准在团队开发中也会带来极大的混乱。 最后我想说明的是:一个逻辑,他究竟应该放在Service中,还是domain object中,还是DAO中,其实不那么容易判定,不同的人从不同的理解角度就会得出不同的结论,这在团队开发中也是一个大忌。 随便举个简单例子: 银行账号Account 存钱操作 deposit,显然是account的方法,没有疑义 取钱操作 withdraw, 显然是account的方法,没有疑义 转账操作 transfer, 有疑义了,转账不依赖于某个特定的account,是一个抽象的通用操作,简单来说,它是一个无状态的操作,如果是你,你可能认为转账是一个银行账户的紧密业务逻辑,也确实如此,所以 account.transfer(account1 ,account2)。但是这个方法调用是很别扭的,因为参与转账的是account1和account2,和你account对象有何关系?你account凭什么参与我们的转账行为呢?显然和OO相违背。不要说充血模型的拥趸,就是贫血模型提倡者的我都觉得这种用法太怪异了。 于是你会说那么我Account.transfer(account1, account2),我把transfer定义为static的可以了吧?和account对象不发生关系了。好,这样确实看起来舒服多了,但是再定睛一看,static方法呀,static方法是不能引用非static属性/方法的,这意味着什么?意味着你无法对transfer方法进行依赖注入了。还是行不同呀! 怎么办呢?我提出的标准:针对一个抽象领域模型,我们应该设计两个类:一个持久类Account,一个控制类AccountManager,凡是那些依赖特定account对象的有状态的操作归Account类,凡是那些不依赖特定account对象的无状态操作归AccountManager类。于是Account包括了deposit/withdraw, AccountManager包括了transfer,都不必定义static,都可以依赖注入了,这种充血模型才是我可以接受的。 在我实际项目中,我会继续扩展这个AccountManager,让AccountManager同时充当Account业务方法的Service封装,然后只针对这个AccountManager进行事务控制,而只暴露给Web程序员AccountManager的业务操作方法和Account实体类。 这里还需要注意一个问题,Account的业务方法是不能够暴露给Web程序员的,因为Account的业务方法没有事务控制,不过Web程序员拿到了Account,你没有办法阻止他绕过AccountManager直接使用Account的deposit/withdraw,所以我只能去掉Account的deposit/withdraw,把这两个方法挪到AccountManager中去。最后,你可以发现,充血模型在分层开发和事务控制的现实面前一步一步妥协成为了贫血模型。 一个业务逻辑根据什么标准进行划分?充血模型的拥趸们说根据业务逻辑方法和领域模型的概念上的关系。于是transfer方法归在Account类里面。 这时候,根据OO原则,transfer应该是一个static的方法,但是这样就无法DI了,怎么解决?就是把一个领域模型分离成为两个相互协作的类:Account和AccountManager。 然后再考虑事务应该在哪里控制?标准的做法是在Account/AccountManager这一层上面封装一层Service Facade,进行事务控制。这样的充血模型才算是完备的。 然而,Web层程序员终究是可以拿到account对象的,你怎么阻止他直接使用account.withdraw,而不是使用accountService.withdraw呢?没有办法!account.withdraw是没有事务控制的,一旦web程序员这样用了,程序就出bug了。 怎么办?两个办法: 办法1:在Service层进行domain object的分离,把domain object的domain logic剥离出去,把数据属性拷贝到一个DTO里面去,把这个DTO传给Web层。这个办法估计现在绝大多数人会反对了吧(除了少数顽固分子) 办法2:把account里面的withdraw/deposit剥离到AccountManager里面去,扩展AccountManager,让它充当Service Facade,进行事务控制 权衡一下,还是办法2可行。于是充血模型在一个分层项目的开发中经过层层妥协,变成了贫血模型。 请注意一个要点:为什么我在 http://forum.iteye.com/viewtopic.php?t=11712 这个帖子中提出,要根据业务方法是否依赖持久化的标准,来决定它究竟应该属于account,还是AccountManager呢? 原因一:保证单向依赖,这个原因在那个帖子里面详细谈过了 原因二:这个帖子我上面这些话都是在推导这个结论,因为依赖持久化的业务逻辑需要事务控制,而一旦需要事务控制,就不适合放在account里面,请仔细揣摩我上面的充血模型推导到贫血模型的过程。 |
|
返回顶楼 | |
发表时间:2005-12-16
补充一下遗漏的地方:
对于充血模型的事务控制,也可以不扩展AccountManager,而是同时对Account的业务逻辑方法(withdraw/deposit)和AccountManager的业务逻辑方法(transfer)进行事务控制。这里要说明的一点是,DAO是不应该被直接暴露给Web层的,对于DAO的不依赖于特定对象的无状态操作,例如findAccountById(...)等等方法,应该由AccountManager来封装一层。最后得到的模型: Web Action ---> AccountManager, Account(事务控制) <---> AcountDao 其中AccountManager和Account两个类合起来算是一个领域模型Account。AccountManager包含了无状态操作(同时封装DAO方法),Account包含了有状态操作。 如果是这样的充血模型,则是我可以接受的。 |
|
返回顶楼 | |
发表时间:2005-12-16
我倒是不认为持久化与否是区分的关键。
相反,我认为应该把持久化完全摒除在考虑范围之外,因为那完全是实现细节。只要架设对象直接储存在内存中就够了。 |
|
返回顶楼 | |
发表时间:2005-12-16
引用 转账操作 transfer, 有疑义了,转账不依赖于某个特定的account,是一个抽象的通用操作,简单来说,它是一个无状态的操作,如果是你,你可能认为转账是一个银行账户的紧密业务逻辑,也确实如此,所以 account.transfer(account1 ,account2)。
no no .. 一般这样来 account.transfer(destAccount) . 一开始就强行假设,后面的所有论据都有点站不住脚。 AccountManager有存在的必要,正如你所说的。 事务确实是非常谨慎考虑的事情。然而存在AccountManager之后,会跟Account引起很多仍然可以争议的地方,这里是仁者见仁,智者见智。 另外对于你提到的一个操作逻辑,不知道该把它放到哪个对象身上,这点我想是一个经验和设计问题,几乎所有的操作肯定是基于某个对象促发的,因为一切都是对象。就所没有任何得***Service ,***Manager,全部是Domain model ,你仍然可以很OO的将Operation ,message定义到某个领域对象上,只要你对它提出需求,你就可以设计。 引用 事务的控制放在Web Action层是不合适的。因为一个Action的execute方法经常可能跨越多个事务。这里的事务需要特别注意一下,所谓事务并不光光指要么都成功,要么都失败,这仅仅是事务四个属性中的原子性,此外事务还要保证隔离性,对读数据库的隔离性。例如一个Action中执行了四个统计查询的业务 bean,我这个时候确确实实需要的是4个事务,而不是合并为一个长事务。还有很多其他的场景,这里不一一列举,结论就是事务控制放在Web Action层不合适。
这个结论也很武断。 如果一个设计能够解决80%的问题,毫无疑问它是可以被采用的。 至于你所说的需要四个事务的查询,我想也是可以定制的。 其实我的意见: 事务的管理确实可以很透明,很灵活的配置。 但是如果说因为事务的透明管理,就一定需要一个Account--->AccountManager对象,我想这是很生硬的道理。 这么简单的一个操作CRUD, 都得有一个AcountMananger,然后你看里面的代码,假设你使用了template DAO,你会发现几乎全部的代码都是delegate to DAO .CRUD () 我不觉得这样的类有存在的必要。 事务的透明管理,你可以放在任何觉得是Service Facade的地方,比如execute(),或者一个真正的AccountManager入口。 |
|
返回顶楼 | |