UML软件工程组织

 

 

领域模型
 
2008-03-24 作者:ROBBIN 来源:forum.javaeye.com
 
为了补大家的遗憾,在此总结下ROBBIN的领域模型的一些观点和大家的补充,在网站和演讲中,robbin将领域模型初步分为4大类:
1、失血模型
2、贫血模型
3、充血模型
4、胀血模型

那么让我们看看究竟有这些领域模型的具体内容,以及他们的优缺点:

一、失血模型

失血模型简单来说,就是domain object只有属性的getter/setter方法的纯数据类,所有的业务逻辑完全由business object来完成(又称

TransactionScript),这种模型下的domain object被Martin Fowler称之为“贫血的domain object”。下面用举一个具体的代码来说明,代码

来自Hibernate的caveatemptor,但经过我的改写:

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

java代码: 

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方法省略不写,避免篇幅太长
}

java代码: 

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

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

java代码: 

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中,保证一次调用只打开关闭一次)

java代码: 

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方法。

二、贫血模型

简单来说,就是domain ojbect包含了不依赖于持久化的领域逻辑,而那些依赖持久化的领域逻辑被分离到Service层。
Service(业务逻辑,事务封装) --> DAO ---> domain object
这也就是Martin Fowler指的rich domain object

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

java代码: 

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的代码同上,省略。

java代码: 

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中。

经过上面的讨论,如何区分domain logic和business logic,我想提出一个改进的区分原则:

domain logic只应该和这一个domain object的实例状态有关,而不应该和一批domain object的状态有关;

当你把一个logic放到domain object中以后,这个domain object应该仍然独立于持久层框架之外(Hibernate,JDO),这个domain object仍然可

以脱离持久层框架进行单元测试,这个domain object仍然是一个完备的,自包含的,不依赖于外部环境的领域对象,这种情况下,这个logic

才是domain logic。
这里有一个很确定的原则:logic是否只和这个object的状态有关,如果只和这个object有关,就是domain logic;如果logic是和一批domain

object的状态有关,就不是domain logic,而是business logic。


Item的placeBid这个业务逻辑方法没有显式的对持久化ItemDao接口产生依赖,所以要放在Item中。请注意,如果脱离了Hibernate这个持久化

框架,Item这个domain object是可以进行单元测试的,他不依赖于Hibernate的持久化机制。它是一个独立的,可移植的,完整的,自包含的

域对象。

而loadItemById和findAll这两个业务逻辑方法是必须显式的对持久化ItemDao接口产生依赖,否则这个业务逻辑就无法完成。如果你要把这两

个方法放在Item中,那么Item就无法脱离Hibernate框架,无法在Hibernate框架之外独立存在。

这种模型的优点:
1、各层单向依赖,结构清楚,易于实现和维护
2、设计简单易行,底层模型非常稳定
这种模型的缺点:
1、domain object的部分比较紧密依赖的持久化domain logic被分离到Service层,显得不够OO
2、Service层过于厚重

三、充血模型

充血模型和第二种模型差不多,所不同的就是如何划分业务逻辑,即认为,绝大多业务逻辑都应该被放在domain object里面(包括持久化逻辑)

,而Service层应该是很薄的一层,仅仅封装事务和少量逻辑,不和DAO层打交道。
Service(事务封装) ---> domain object <---> DAO
这种模型就是把第二种模型的domain object和business object合二为一了。所以ItemManager就不需要了,在这种模型下面,只有三个类,他

们分别是:

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

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

java代码: 

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、更加符合OO的原则
2、Service层很薄,只充当Facade的角色,不和DAO打交道。
这种模型的缺点:
1、DAO和domain object形成了双向依赖,复杂的双向依赖会导致很多潜在的问题。
2、如何划分Service层逻辑和domain层逻辑是非常含混的,在实际项目中,由于设计和开发人员的水平差异,可能导致整个结构的混乱无序。
3、考虑到Service层的事务封装特性,Service层必须对所有的domain object的逻辑提供相应的事务封装方法,其结果就是Service完全重定义一遍所有的domain logic,非常烦琐,而且Service的事务化封装其意义就等于把OO的domain logic转换为过程的Service TransactionScript 。

该充血模型辛辛苦苦在domain层实现的OO在Service层又变成了过程式,对于Web层程序员的角度来看,和贫血模型没有什么区别了。

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

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

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

四、胀血模型

基于充血模型的第三个缺点,有同学提出,干脆取消Service层,只剩下domain object和DAO两层,在domain object的domain logic上面封装事务。
domain object(事务封装,业务逻辑) <---> DAO
似乎ruby on rails就是这种模型,他甚至把domain object和DAO都合并了。

该模型优点:
1、简化了分层
2、也算符合OO

该模型缺点:
1、很多不是domain logic的service逻辑也被强行放入domain object ,引起了domain ojbect模型的不稳定
2、domain object暴露给web层过多的信息,可能引起意想不到的副作用。
 

组织简介 | 联系我们 |   Copyright 2002 ®  UML软件工程组织 京ICP备10020922号

京公海网安备110108001071号