让精心构建的对象模型高效地工作有很多底层的技术问题需要解决,其中如何满足领域对象的业务方法在计算过程中对数据的需求是一个普遍存在的问题(实际上,在实际应用中,我们会遇到更为复杂的情况,不只是有数据的需求,还可能出现对应用层面发生依赖)。对于这一问题,目前有两种模型可供借鉴,那就是基于数据访问的集合类和领域事件模式。
基于数据访问的集合类(Data Access Based Collection)
基于数据访问的集合类是我在开发oobbs系统时设计的一种模式。这一模式通过一个抽象的接口来代表某一对象依赖的一组集合。当这一对象实例化时,一个基于数据访问的集合实现类会注入到这个对象中,所有通过这一集合进行的操作,比如遍历,增删元素等都是被实现类转化成数据访问操作。基于数据访问的集合类很像是一个缩水版的Repository。还是以Forum的public List<Thread> getThreads()方法为例,我们认为getThreads是Forum的一个典型的业务方法,但是由于一个Forum拥有众多的Thread,这使得我们根本不容许一次将这个集合全部加载出来。即使是在hibernate这类提供了lazy和extra lazy加载机制ORM工具里,也无法避免当我们直得去迭代这一集合时,它们会被一次性全部加载。而另一方面,实际的应用请求也不会一次请求所有的Thread,更常见的情况是以分页的形式,一小批次一小批次地请求。因此基于数据访问的集合模式使用一个集合接口做为一个占位符,并声明了一些基本的集合操作:比如返回某一区间内的子集(为分页而服务)和add,remove等操作,而实现类里,这些方法是以数据访问的方式实现的。下面是集合的接口定义。它看起来很像一个普通的集合。
- package oobbs.domainmodel;
- import java.io.Serializable;
- import java.util.List;
- /**
- * The collection interface represents a set of objects, it's like the
- * java.util.Collection, however, there no real objects in this collection, it
- * only looks like a collection, its method's implementation is database access
- * operation! see <code>oobbs.infrastructure.persistence.AbstractHibernateCollection</code>
- * @author laurence.geng
- */
- public interface Collection<Entity, PK extends Serializable, Owner> {
- void setOwner(Owner owner);
- void setOwnerName(String ownerName);
- /**
- * Adds an object. This method will persist entity to database directly!
- * @param e an entity instance. * @return the pK the generated primary key
- * after insert into database.
- */
- PK add(Entity e);
- void addAll(java.util.Collection<Entity> c);
- /**
- * Removes the entity. This method will remove this entity from database
- * directly.
- */
- void remove(Entity e);
- void removeAll(java.util.Collection<Entity> c);
- boolean contains(Entity o);
- boolean isEmpty();
- int size();
- /**
- * The most important method. It returns a subset of the whole collection.
- * the returned subset is fetched from database by sql, hql or other data
- * access way, The Collection itself never load all elements once time!
- */
- List<Entity> toList(int startIndex, int offset);
- void flush();
- }
下面则是基本于hibernate的集合接口实现类。它实现了所有的基本的操作。在Forum类中就会这样一个字段以及相应的getter和setter:
- @Transient
- private Collection<Thread, Long, Forum> forumThreads;
- @Autowired
- /**
- * Sets ForumThreadCollection.
- * ForumThreadCollection is injected by this setter. When a collection instance injected, set this forum to its forum!
- */
- public void setForumThreads(@Qualifier("forumThreads") Collection<Thread, Long, Forum> forumThreads) {
- this.forumThreads = forumThreads;
- this.forumThreads.setOwner(this);
- this.forumThreads.setOwnerName("forum");
- }
- /**
- * Gets this forum's thread collection.
- */
- public Collection<Thread, Long, Forum> getForumThreads() {
- return forumThreads;
- }
其中注入的forumThreads对象是一个名为ForumThreadHibernateCollection的类,它继承了AbstractHibernateCollection类,因为没有特殊的需要,没有重写任何方法。而下面展示的是service中对这集合的一次使用:
- List<Thread> threads = forum.getForumThreads().toList(startThreadIndex,threadTotal);
我们来分析一下Domain Collection这一模式的优劣。我认为它最大的优点在于它能够以一个字段的形式存在于单端关联对象中,这使得单端对象的定义饱满,完成符合并体现了一对多双向关联中双方依赖关系。这一点是使用hiberate映射无法实现的,因为我们不能在Forum中映射@OneToMany(mappedBy="forum") private Set<Thread> threads;
但是它的缺点也是非常明显并且似乎是无法克服的,那就是它只能用来表示直接关联的集合,如果单端对象想通过这一集合进一步遍历元素中更深层次的二级,三级集合时,domain collection就显得力不从心了。比方说:在论坛的首页上往往会罗列出各个Forum的一些基本信息,其中之一就是该Forum有多少帖子(Post),相应的,Forum对象会有这样一个方法public Long getPostCount();很显然,Post是Forum的二级集合,一个Forum需要先得到它的Thread集合,再从每个Thread中得到Post集合。我们可以为了这一方法再提供一个ForumPostHibernateCollcetion用来代表一个Forum的所有Post的集合,但是这个集合已经和模型的定义发生了偏离,因为并没有从Forum到Post的直接关联。而更加普遍的问题的是:我们会常常遇到某一个单端实体从它直接依赖的对象开始进行深度地导航(体现在SQL上就是对多个表的join操作),这时候我们不能为每一种导航而创建一个从出发点到结束点的domain collection。在这种情况下,获取数据必须通过另外一种方式进行了,那就是Domain Event模式。
领域事件(Domain Event)
Domain Event模式最初由udi dahan提出,发表在自己的博客上:http://www.udidahan.com/2009/06/14/domain-events-salvation/这一模式得到广泛的认可。它所要应对的正是将领域对象从对repository或service的依赖中解脱出来,避免让领域对象对这些设施产生直接依赖。它的做法就是当领域对象的业务方法需要依赖到这些对象时就发出一个事件,这个事件会被相应的对象监听到并做出处理。在我的oobbs系统中,我对这一模式做了一些改进,主要是消除静态方法和规范事件模型。在我的方案中,这一机制由这样几个角色:DomainEventDispatcher,DomainEvent和DomainEventListener.
DomainEventDispatcher会以字段的形式存在于领域对象中,负责在业务方法中dispatch领域事件。下面是所有Dispatcher的基类:
- package oobbs.domainmodel;
- import java.util.HashMap;
- import java.util.Map;
- /**
- * The DomainEventDispatcher take charge of listener registration and dispatch
- * domain event to corresponding listener to handle. Usually, one dispatch per
- * domain object.
- */
- public class DomainEventDispatcher {
- /** The listener map. all registered listeners are stored in this map. */
- protected Map<String, DomainObejctListener> listeners = new HashMap<String, DomainObejctListener>();
- /** * Adds a listener. * * @param listener the listener */
- public void addListener(DomainObejctListener listener) {
- listeners.put(listener.getName(), listener);
- }
- /** * Removes all listeners. */
- public void removeAllListeners() {
- listeners.clear();
- }
- }
一般来说一个领域对象会有一个对应的event dispatcher,这个dispatcher会有一组重载的dispatch方法,用于分发不同的领域事件。下面是oobbs中Forum对象的event dispatcher.
- package oobbs.domainmodel.forum;
- import oobbs.Constants;
- import oobbs.domainmodel.DomainEventDispatcher;
- import oobbs.domainmodel.ResultCollector;
- /**
- * * The ForumEventDispatcher dispatch all events which about Forum object. * @author
- * laurence.geng
- */
- public class ForumEventDispatcher extends DomainEventDispatcher {
- public void dispatch(GetForumThreadEvent event, ResultCollector result) {
- ForumListener forumListener = (ForumListener) listeners.get(Constants.FORUM_REPO_AS_FORUM_LISTENER);
- forumListener.handleGetForumThreadEvent(event, result);
- }
- public void dispatch(GetForumPostCountEvent event, ResultCollector result) {
- ForumListener forumListener = (ForumListener) listeners.get(Constants.FORUM_REPO_AS_FORUM_LISTENER);
- forumListener.handleGetFroumPostCountEvent(event, result);
- }
- public void dispatch(GetForumThreadCountEvent event, ResultCollector result) {
- ForumListener forumListener = (ForumListener) listeners.get(Constants.FORUM_REPO_AS_FORUM_LISTENER);
- forumListener.handleGetForumThreadCountEvent(event, result);
- }
- }
系统中会有很多的domain event,下面是所有领域事件的基类:
- package oobbs.domainmodel;
- /**
- * The supper class of all domain events. all events should provide event
- * source, the domain object which fired this event.
- */
- public class DomainEvent {
- /** The event source, the domain object which fired this event. */
- protected Object source;
- public DomainEvent(Object source) {
- super();
- this.source = source;
- }
- /** * Gets the event source. * * @return the event source */
- public Object getSource() {
- return source;
- }
- }
下面就是刚才提到的例子中返回Forum某一部分(分页)Thread的事件,在这个事件中我们看到一个事件可以携带一些参数,供listener使用:
- package oobbs.domainmodel.forum;
- import oobbs.domainmodel.DomainEvent;
- /**
- * The Event that forum request to get its threads.
- * @author laurence.geng
- */
- public class GetForumThreadEvent extends DomainEvent {
- /** The start index of request thread. */
- private int startIndex;
- /** The count of request thread. */
- private int count;
- public GetForumThreadEvent(Forum source, int startIndex, int count) {
- super(source);
- this.startIndex = startIndex;
- this.count = count;
- }
- public int getStartIndex() {
- return startIndex;
- }
- public int getCount() {
- return count;
- }
- }
而下面就是我们所有listener的基类:
- package oobbs.domainmodel;
- /**
- * The super class of all domain object listeners. it handles events from
- * domain objects. Each listener has to provide a name as key for registering
- * itself to dispatcher.
- * @see DomainObejctEvent
- * @author laurence.geng
- */
- public interface DomainObejctListener {
- /** * Gets the name. * * @return the name */
- public String getName();
- }
下面是Forum的listenerr接口,这个接口会有很多handle方法,代码只展示了一个。
- package oobbs.domainmodel.forum;
- import oobbs.domainmodel.DomainObejctListener;
- import oobbs.domainmodel.ResultCollector;
- /**
- * * The forum listener. It handles all events from Forum object. * * @see
- * ForumEvent * @author laurence.geng
- */
- public interface ForumListener extends DomainObejctListener {
- /**
- * * Handle the event that a forum requests to get its threads. * * @param
- * event the event * @param result the result
- */
- public void handleGetForumThreadEvent(GetForumThreadEvent event,ResultCollector result);
- }
然后 是这个接口一个实现类,在oobbs中,做为forum的repository的实现类:ForumHibernateRepository,自然成为实现这一接口的最佳选择:
- /**
- * * The Forum's repository with hibernate implementation, besides, it's a forum
- * listener which handle * all events come from forum object.
- */
- public class ForumHibernateRepository extends AbstractHibernateRepository<Forum, Long> implements ForumRepository {
- /*
- * (non-Javadoc)
- *
- * @see
- * oobbs.domainmodel.forum.ForumListener#handleGetForumThreadEvent(oobbs
- * .domainmodel.forum.GetForumThreadEvent,
- * oobbs.domainmodel.ResultCollector)
- */
- @Overridepublic
- void handleGetForumThreadEvent(final GetForumThreadEvent event, ResultCollector result) {
- List<Thread> threads = (List<Thread>) getHibernateTemplate().executeWithNativeSession(new HibernateCallback() {
- @SuppressWarnings("unchecked")
- public Object doInHibernate(Session session) throws HibernateException, SQLException {
- logger.info("Start to load threads of forum.");
- // Get forum.
- String getForuumThreadHql = "from Thread as thread where thread.forum=:forum";
- List<Thread> threads = (List<Thread>) session
- .createQuery(getForuumThreadHql)
- .setCacheable(true)
- .setParameter("forum", event.getSource())
- .setFirstResult(event.getStartIndex())
- .setMaxResults(event.getCount()).list();
- return threads;
- }
- });
- result.add(threads);
- }
- }
最后我们看一看这一切是如被触发的。我们来看forum的这个方法:oobbs.domainmodel.forum.Forum.getThreads(int, int):
- public List<Thread> getThreads(int startIndex, int count) {
- GetForumThreadEvent event = new GetForumThreadEvent(this, startIndex, count);
- ResultCollector result = new ResultCollector();
- forumEventDispatcher.dispatch(event, result);
- return (List<Thread>) result.getUniqueResult();
- }
很简洁的四行代码:分别new一个event和result collector,然后用ForumEventDispatcher来dispatch这个事件,然后收集返回的处理结果。上述所有组件都是围绕这里而设计的,我们希望在领域对象的业务方法里不会出现任何repository或service,DomainEvent模式很好的满足了我们的需求,并且是以一种非常优雅而简洁的方式。
当然,在上面讲述的整个机制中,我们漏掉了一环没有讲,那就是dispatcher是如何被实例化并完成注册listener工作的。由于oobbs使用了spring的IOC管理对象, dispatcher是由IOC创建并完成注册listener等初始化工作的。下面的类完成了这一系列工作:
- package oobbs.infrastructure.appcontext;
- import oobbs.domainmodel.DomainObejctListener;
- import oobbs.domainmodel.forum.ForumEventDispatcher;
- import oobbs.domainmodel.forum.ThreadEventDispatcher;
- import org.apache.log4j.Logger;
- import org.springframework.beans.BeansException;
- import org.springframework.beans.factory.config.BeanPostProcessor;
- import org.springframework.context.ApplicationContext;
- import org.springframework.context.ApplicationContextAware;
- /**
- * * The ApplicationBeanPostProcessor will do some initialization work when bean
- * is created by spring ioc container, * such as: Adding listeners for domain
- * event dispatchers and so on. * @author laurence.geng
- */
- public class ApplicationBeanPostProcessor implements BeanPostProcessor,
- ApplicationContextAware {
- /** The Constant logger. */
- private static final Logger logger = Logger
- .getLogger(ApplicationBeanPostProcessor.class);
- /** The application context. */
- private ApplicationContext applicationContext;
- /*
- * (non-Javadoc) * @see org.springframework .beans.factory
- * .config.BeanPostProcessor #postProcessBeforeInitialization
- * (java.lang.Object, java.lang.String)
- */
- public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
- return bean; // we could potentially return any object reference here...
- }
- /*
- * (non-Javadoc) * @see
- * org.springframework.beans.factory.config.BeanPostProcessor
- * #postProcessAfterInitialization(java.lang.Object, java.lang.String)
- */
- public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
- logger.debug("Bean '" + beanName + "' created : " + bean.toString());
- // Adding listeners for domain event dispatchers.
- if ("forumEventDispatcher".equals(beanName)) {
- ForumEventDispatcher forumEventDispatcher = (ForumEventDispatcher) bean;
- forumEventDispatcher.addListener((DomainObejctListener) applicationContext.getBean("forumRepository"));
- }
- if ("threadEventDispatcher".equals(beanName)) {
- ThreadEventDispatcher threadEventDispatcher = (ThreadEventDispatcher) bean;
- threadEventDispatcher.addListener((DomainObejctListener) applicationContext.getBean("threadRepository"));
- }
- return bean;
- }
- /*
- * (non-Javadoc) * @see
- * org.springframework.context.ApplicationContextAware#setApplicationContext
- * (org.springframework.context.ApplicationContext)
- */
- @Override
- public void setApplicationContext(ApplicationContext arg0) throws BeansException {
- this.applicationContext = arg0;
- }
- }
小结