论坛首页 Java企业应用论坛

总结一下最近关于domain object以及相关的讨论

浏览 206906 次
该帖已经被评为精华帖
作者 正文
   发表时间:2005-03-24  
在最近的围绕domain object的讨论中浮现出来了三种模型,(还有一些其他的旁枝,不一一分析了),经过一番讨论,各种问题逐渐清晰起来,在这里我试图做一个总结,便于大家了解和掌握。

第一种模型:只有getter/setter方法的纯数据类,所有的业务逻辑完全由business object来完成(又称TransactionScript),这种模型下的domain object被Martin Fowler称之为“贫血的domain object”。下面用举一个具体的代码来说明,代码来自Hibernate的caveatemptor,但经过我的改写:

一个实体类叫做Item,指的是一个拍卖项目
一个DAO接口类叫做ItemDao
一个DAO接口实现类叫做ItemDaoHibernateImpl
一个业务逻辑类叫做ItemManager(或者叫做ItemService)

public class Item implements Serializable {
    private Long id = null;
    private int version;
    private String name;
    private User seller;
    private String description;
    private MonetaryAmount initialPrice;
    private MonetaryAmount reservePrice;
    private Date startDate;
    private Date endDate;
    private Set categorizedItems = new HashSet();
    private Collection bids = new ArrayList();
    private Bid successfulBid;
    private ItemState state;
    private User approvedBy;
    private Date approvalDatetime;
    private Date created = new Date();
    //  getter/setter方法省略不写,避免篇幅太长
}


public interface ItemDao {
    public Item getItemById(Long id);
    public Collection findAll();
    public void updateItem(Item item);
}


ItemDao定义持久化操作的接口,用于隔离持久化代码。

public class ItemDaoHibernateImpl implements ItemDao extends HibernateDaoSupport {
    public Item getItemById(Long id) {
        return (Item) getHibernateTemplate().load(Item.class, id);
    }
    public Collection findAll() {
        return (List) getHibernateTemplate().find("from Item");
    }
    public void updateItem(Item item) {
        getHibernateTemplate().update(item);
    }
}

ItemDaoHibernateImpl完成具体的持久化工作,请注意,数据库资源的获取和释放是在ItemDaoHibernateImpl里面处理的,每个DAO方法调用之前打开Session,DAO方法调用之后,关闭Session。(Session放在ThreadLocal中,保证一次调用只打开关闭一次)

public class ItemManager {
    private ItemDao itemDao;
    public void setItemDao(ItemDao itemDao) { this.itemDao = itemDao;}
    public Bid loadItemById(Long id) { 
        itemDao.loadItemById(id);
    }
    public Collection listAllItems() {
        return  itemDao.findAll();
    }
    public Bid placeBid(Item item, User bidder, MonetaryAmount bidAmount,
	                    Bid currentMaxBid, Bid currentMinBid) throws BusinessException {
            if (currentMaxBid != null && currentMaxBid.getAmount().compareTo(bidAmount) > 0) {
    	throw new BusinessException("Bid too low.");
    }
    
    // Auction is active
    if ( !state.equals(ItemState.ACTIVE) )
    	throw new BusinessException("Auction is not active yet.");
    
    // Auction still valid
    if ( item.getEndDate().before( new Date() ) )
    	throw new BusinessException("Can't place new bid, auction already ended.");
    
    // Create new Bid
    Bid newBid = new Bid(bidAmount, item, bidder);
    
    // Place bid for this Item
    item.getBids().add(newBid);
    itemDao.update(item);     //  调用DAO完成持久化操作
    return newBid;
    }
}


事务的管理是在ItemManger这一层完成的,ItemManager实现具体的业务逻辑。除了常见的和CRUD有关的简单逻辑之外,这里还有一个placeBid的逻辑,即项目的竞标。

以上是一个完整的第一种模型的示例代码。在这个示例中,placeBid,loadItemById,findAll等等业务逻辑统统放在ItemManager中实现,而Item只有getter/setter方法。
   发表时间:2005-03-24  
第二种模型,也就是Martin Fowler指的rich domain object是下面这样子的:

一个带有业务逻辑的实体类,即domain object是Item
一个DAO接口ItemDao
一个DAO实现ItemDaoHibernateImpl
一个业务逻辑对象ItemManager

public class Item implements Serializable {
    //  所有的属性和getter/setter方法同上,省略
    public Bid placeBid(User bidder, MonetaryAmount bidAmount,
                        Bid currentMaxBid, Bid currentMinBid)
    	throws BusinessException {
    
    	// Check highest bid (can also be a different Strategy (pattern))
    	if (currentMaxBid != null && currentMaxBid.getAmount().compareTo(bidAmount) > 0) {
    		throw new BusinessException("Bid too low.");
    	}
    
    	// Auction is active
    	if ( !state.equals(ItemState.ACTIVE) )
    		throw new BusinessException("Auction is not active yet.");
    
    	// Auction still valid
    	if ( this.getEndDate().before( new Date() ) )
    		throw new BusinessException("Can't place new bid, auction already ended.");
    
    	// Create new Bid
    	Bid newBid = new Bid(bidAmount, this, bidder);
    
    	// Place bid for this Item
    	this.getBids.add(newBid);  // 请注意这一句,透明的进行了持久化,但是不能在这里调用ItemDao,Item不能对ItemDao产生依赖!
    
    	return newBid;
    }
}


竞标这个业务逻辑被放入到Item中来。请注意this.getBids.add(newBid);  如果没有Hibernate或者JDO这种O/R Mapping的支持,我们是无法实现这种透明的持久化行为的。但是请注意,Item里面不能去调用ItemDAO,对ItemDAO产生依赖!

ItemDao和ItemDaoHibernateImpl的代码同上,省略。

public class ItemManager { 
    private ItemDao itemDao; 
    public void setItemDao(ItemDao itemDao) { this.itemDao = itemDao;} 
    public Bid loadItemById(Long id) { 
        itemDao.loadItemById(id); 
    } 
    public Collection listAllItems() { 
        return  itemDao.findAll(); 
    } 
    public Bid placeBid(Item item, User bidder, MonetaryAmount bidAmount, 
                            Bid currentMaxBid, Bid currentMinBid) throws BusinessException { 
        item.placeBid(bidder, bidAmount, currentMaxBid, currentMinBid);
        itemDao.update(item);    // 必须显式的调用DAO,保持持久化
    }
}


在第二种模型中,placeBid业务逻辑是放在Item中实现的,而loadItemById和findAll业务逻辑是放在ItemManager中实现的。不过值得注意的是,即使placeBid业务逻辑放在Item中,你仍然需要在ItemManager中简单的封装一层,以保证对placeBid业务逻辑进行事务的管理和持久化的触发。

这种模型是Martin Fowler所指的真正的domain model。在这种模型中,有三个业务逻辑方法:placeBid,loadItemById和findAll,现在的问题是哪个逻辑应该放在Item中,哪个逻辑应该放在ItemManager中。在我们这个例子中,placeBid放在Item中(但是ItemManager也需要对它进行简单的封装),loadItemById和findAll是放在ItemManager中的。

切分的原则是什么呢? Rod Johnson提出原则是“case by case”,可重用度高的,和domain object状态密切关联的放在Item中,可重用度低的,和domain object状态没有密切关联的放在ItemManager中。

我提出的原则是:看业务方法是否显式的依赖持久化。

Item的placeBid这个业务逻辑方法没有显式的对持久化ItemDao接口产生依赖,所以要放在Item中。请注意,如果脱离了Hibernate这个持久化框架,Item这个domain object是可以进行单元测试的,他不依赖于Hibernate的持久化机制。它是一个独立的,可移植的,完整的,自包含的域对象

而loadItemById和findAll这两个业务逻辑方法是必须显式的对持久化ItemDao接口产生依赖,否则这个业务逻辑就无法完成。如果你要把这两个方法放在Item中,那么Item就无法脱离Hibernate框架,无法在Hibernate框架之外独立存在。
2 请登录后投票
   发表时间:2005-03-24  
第三种模型印象中好像是firebody或者是Archie提出的(也有可能不是,记不清楚了),简单的来说,这种模型就是把第二种模型的domain object和business object合二为一了。所以ItemManager就不需要了,在这种模型下面,只有三个类,他们分别是:

Item:包含了实体类信息,也包含了所有的业务逻辑
ItemDao:持久化DAO接口类
ItemDaoHibernateImpl:DAO接口的实现类

由于ItemDao和ItemDaoHibernateImpl和上面完全相同,就省略了。

public class Item implements Serializable {
    //  所有的属性和getter/setter方法都省略
   private static ItemDao itemDao;
    public void setItemDao(ItemDao itemDao) {this.itemDao = itemDao;}
    
    public static Item loadItemById(Long id) {
        return (Item) itemDao.loadItemById(id);
    }
    public static Collection findAll() {
        return (List) itemDao.findAll();
    }

    public Bid placeBid(User bidder, MonetaryAmount bidAmount,
                    Bid currentMaxBid, Bid currentMinBid)
    throws BusinessException {
    
        // Check highest bid (can also be a different Strategy (pattern))
        if (currentMaxBid != null && currentMaxBid.getAmount().compareTo(bidAmount) > 0) {
        	throw new BusinessException("Bid too low.");
        }
        
        // Auction is active
        if ( !state.equals(ItemState.ACTIVE) )
        	throw new BusinessException("Auction is not active yet.");
        
        // Auction still valid
        if ( this.getEndDate().before( new Date() ) )
        	throw new BusinessException("Can't place new bid, auction already ended.");
        
        // Create new Bid
        Bid newBid = new Bid(bidAmount, this, bidder);
        
        // Place bid for this Item
        this.addBid(newBid);
        itemDao.update(this);      //  调用DAO进行显式持久化
        return newBid;
    }
}


在这种模型中,所有的业务逻辑全部都在Item中,事务管理也在Item中实现。
0 请登录后投票
   发表时间:2005-03-24  
robbin 写道
第三种模型印象中好像是firebody或者是Archie提出的(也有可能不是,记不清楚了),简单的来说,这种模型就是把第二种模型的domain object和business object合二为一了。所以ItemManager就不需要了,在这种模型下面,只有三个类,他们分别是:

Item:包含了实体类信息,也包含了所有的业务逻辑
ItemDao:持久化DAO接口类
ItemDaoHibernateImpl:DAO接口的实现类

由于ItemDao和ItemDaoHibernateImpl和上面完全相同,就省略了。

public class Item implements Serializable {
    //  所有的属性和getter/setter方法都省略
   private static ItemDao itemDao;
    public void setItemDao(ItemDao itemDao) {this.itemDao = itemDao;}
    
    public static Item loadItemById(Long id) {
        return (Item) itemDao.loadItemById(id);
    }
    public static Collection findAll() {
        return (List) itemDao.findAll();
    }

    public Bid placeBid(User bidder, MonetaryAmount bidAmount,
                    Bid currentMaxBid, Bid currentMinBid)
    throws BusinessException {
    
        // Check highest bid (can also be a different Strategy (pattern))
        if (currentMaxBid != null && currentMaxBid.getAmount().compareTo(bidAmount) > 0) {
        	throw new BusinessException("Bid too low.");
        }
        
        // Auction is active
        if ( !state.equals(ItemState.ACTIVE) )
        	throw new BusinessException("Auction is not active yet.");
        
        // Auction still valid
        if ( this.getEndDate().before( new Date() ) )
        	throw new BusinessException("Can't place new bid, auction already ended.");
        
        // Create new Bid
        Bid newBid = new Bid(bidAmount, this, bidder);
        
        // Place bid for this Item
        this.addBid(newBid);
        itemDao.update(this);      //  调用DAO进行显式持久化
        return newBid;
    }
}


在这种模型中,所有的业务逻辑全部都在Item中,事务管理也在Item中实现。


这个我想解释一下,也存在一些疑问。

1.事务我是不希望由Item管理的,而是由容器或更高一层的业务类来管理。

2.如果Item不脱离持久层的管理,如JDO的pm,那么itemDao.update(this); 是不需要的,也就是说Item是在事务过程中从数据库拿出来的,并且声明周期不超出当前事务的范围。

3.如果Item是脱离持久层,也就是在Item的生命周期超出了事务的范围,那就要必须显示调用update或attach之类的持久化方法的,这种时候就应该是按robbin所说的第2种模型来做。

PS:书已到手,连夜苦读,希望能看出什么门道来。
0 请登录后投票
   发表时间:2005-03-24  
在上面三种模型之外,还有很多这三种模型的变种,例如partech的模型就是把第二种模型中的DAO和Manager三个类合并为一个类后形成的模型;例如frain....(id很长记不住)的模型就是把第三种模型的三个类完全合并为一个单类后形成的模型;例如Archie是把第三种模型的Item又分出来一些纯数据类(可能是,不确定)形成的一个模型。

但是不管怎么变,基本模型归纳起来就是上面的三种模型,下面分别简单评价一下:

第一种模型绝大多数人都反对,因此反对理由我也不多讲了。但遗憾的是,我观察到的实际情形是,很多使用Hibernate的公司最后都是这种模型,这里面有很大的原因是很多公司的技术水平没有达到这种层次,所以导致了这种贫血模型的出现。从这一点来说,Martin Fowler的批评声音不是太响了,而是太弱了,还需要再继续呐喊。

第二种模型就是Martin Fowler一直主张的模型,实际上也是我一直在实际项目中采用这种模型。我没有看过Martin的POEAA,之所以能够自己摸索到这种模型,也是因为从02年我已经开始思考这个问题并且寻求解决方案了,但是当时没有看到Hibernate,那时候做的一个小型项目我已经按照这种模型来做了,但是由于没有O/R Mapping的支持,写到后来又不得不全部改成贫血的domain object,项目做完以后再继续找,随后就发现了Hibernate。当然,现在很多人一开始就是用Hibernate做项目,没有经历过我经历的那个阶段。

不过我觉得这种模型仍然不够完美,因为你还是需要一个业务逻辑层来封装所有的domain logic,这显得非常罗嗦,并且业务逻辑对象的接口也不够稳定。如果不考虑业务逻辑对象的重用性的话(业务逻辑对象的可重用性也不可能好),很多人干脆就去掉了xxxManager这一层,在Web层的Action代码直接调用xxxDao,同时容器事务管理配置到Action这一层上来。Hibernate的caveatemptor就是这样架构的一个典型应用。

第三种模型是我很反对的一种模型,这种模型下面,Domain Object和DAO形成了双向依赖关系,无法脱离框架测试,并且业务逻辑层的服务也和持久层对象的状态耦合到了一起,会造成程序的高度的复杂性,很差的灵活性和糟糕的可维护性。也许将来技术进步导致的O/R Mapping管理下的domain object发展到足够的动态持久透明化的话,这种模型才会成为一个理想的选择。就像O/R Mapping的流行使得第二种模型成为了可能(O/R Mapping流行以前,我们只能用第一种模型,第二种模型那时候是不现实的)。
0 请登录后投票
   发表时间:2005-03-24  
嗯,总结得不错。

关于这个具体例子,我会给出一个我的实现,可能会是第四种哦。:wink:
最近在客户这里,培养客户对我们软件的信心,时间不好自己控制,等等吧。
0 请登录后投票
   发表时间:2005-03-24  
Archie 写道
1.事务我是不希望由Item管理的,而是由容器或更高一层的业务类来管理。

2.如果Item不脱离持久层的管理,如JDO的pm,那么itemDao.update(this); 是不需要的,也就是说Item是在事务过程中从数据库拿出来的,并且声明周期不超出当前事务的范围。

3.如果Item是脱离持久层,也就是在Item的生命周期超出了事务的范围,那就要必须显示调用update或attach之类的持久化方法的,这种时候就应该是按robbin所说的第2种模型来做。


我这里提到的事务全部都是容器事务管理。但是即使是容器事务,也需要有一个事务的载体吧? EJB模型中,SessionBean是事务的载体,在你的模型中,事务的管理也只能施加在Item的业务逻辑方法上。如果你想抽象出来上一层业务类来进行事务处理,那么你的模型已经变成第二种模型了。

这里的业务逻辑操作可能脱离,也可能不脱离持久层,我们不能做他一定不脱离的假设,所以必须写update。况且这只是一个简单的逻辑,对于复杂的逻辑来说,你不光依赖一个update了,你可能依赖更多的DAO接口方法了。
0 请登录后投票
   发表时间:2005-03-24  
我的本意就是第二种模型,事务万万不能放在Item里边管理啊!
0 请登录后投票
   发表时间:2005-03-24  
Archie 写道
我的本意就是第二种模型,事务万万不能放在Item里边管理啊!


嘿嘿,那就更加可悲了,我们争了那么多天,吵了好几百贴,最后发现大家原来都是观点一致的,那怎么又会争论起来的呢? 不是白白浪费大家感情麽?
0 请登录后投票
   发表时间:2005-03-24  
总结的不错!我也来凑个热闹吧,发发自己的观点。

Robbin总结的三点模型,我1、2用过,所味的第3点还无福消受啊

一、关于第一种模型我在一个实际项目中做过这样的设计,给我的感觉不是很好,主要有以下几点:
  《1》要写太多的贫血类,其实可以做一个工具来自动生成,也不错。
   《2》业务逻辑感觉不能很好的得到体现,这样增加了BussImp类的复杂度, 且许多操作是不应该由BussImp来承担, 但也有个好处那就是设计相对的容易些
    《3》这不是OO的正统设计,这是受Boost的影响
二、至于第二种模型是我一直都在用的,这种模型对设计人员的要求相对的要高,为什么?那是因为对 ItemClass 要比以前更难以把握,其实设计就是归类,但就是这个归类就不那么的简单,且每个人都有自己的归类方法,很难形成统一。
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics