您的位置:首页 > 编程语言 > Java开发

基于Spring实现领域模型模式

2010-03-30 01:05 141 查看



事务脚本、领域模型及表模块是Martin Fowler在《企业应用架构模式》中总结的三种领域逻辑组织模式。各有各的优点和缺点,这里不打算讨论它们各自的适用场景,只简单总结一下在应用领域模型模式方面的一些经验与教训。
1.    背景
近几年Struts(1或2) + Spring + Hibernate(IBatis)组合在Java EE企业应用开发中频频出现。这里也不例外,所采用的主要技术平台即是SSH。本文总结的领域模型模式应用也主要是在这样一个技术背景之下。
在企业应用的架构设计方面,我通常组合使用多种架构风格和模式,它们主要是:

  • 按技术职责分层模式
  • 组件化模式
  • 按通用性程度分层


基于职责分层就是一般意义上的分层,划分也采用相对常见的方式,即表现层、应用层、领域层和持久层。

组件化模式也是我最近几年非常喜欢的一种架构模式,它可以提高系统的可维护性和可复用性。逻辑组件参考用例模型并主要在分析模型中初步创建,在设计模型中进一步细化。组件的粒度级别为业务组件(各种组件级别包括:分布式组件、业务组件和系统级组件),业务组件可以包含所有4个职责层,即表示层、应用层、领域层和持久层,且持久层逻辑上包括数据表。业务组件的对外接口一般设置在应用层级别,其他元素通常不对外暴露。对于数据库表更不例外,一般地,属于某个业务组件的数据库表禁止其他组件直接使用。建立一个良好的组件模型是很费工夫的事情,需要在分析模型上花费很大的精力,在设计模型中还需要进一步优化。但付出总是会有回报的,系统可修改、可维护性、可复用性会有很大提升。当然有时我们确实不需要这种回报。

最后一个常用的架构模式是按组件的通用性进行分层,这也是RUP里的分层,即特定于应用层、通用业务层、中间件层和系统层。如果开发多个类似产品,各个产品中不相同的组件位于特定应用层,各个产品在业务层面可以复用的组件位于通用业务层,与业务领域无关的组件放在中间件层。系统层主要是DBMS、OS级别的东西。这种架构模式主要服务于系统化复用。

介绍完成应用领域模型模式的背景之后,应该进入正题了。

2.    应用领域模型
2.1.    主要设计元素
展现层主要包括:

  • View:主要是视图元素,如JSP。
  • Command:主要是Struts2中的Action,通常Action以应用层中的DTO为属性来充当PresentationModel。

应用层主要包括:

  • ApplicationService:由接口和实现两部分组成,主要用来委托职责给实体或协调多个实体的协作,是真正的控制类,一般为每个用例创建一个应用服务类。Struts1/2中的servlet或filter也常被称为控制器,但在此种场景下,更准确的名称应该是前端控制器,属于展现层。
  • DTO:数据传输对象。
  • Assembler:组装器,负责将实体转化为数据传输对象。

领域层主要包括:

  • Entity:处理核心领域逻辑。
  • DomainService:是不属于单一实体且比较稳定的领域逻辑的处理者

持久层主要包括:

  • DAO:可以由接口和实现两部分组成,数据访问对象。

组件接口元素主要包括:

  • ComponentFacade:组件对外接口,其实现形式与ApplicationService的实现相同。
  • ComponentFacadeFactory:其他组件在使用该组件时,不能直接创建组件接口,该元素为创建组件接口提供了一个工厂,隐藏了组件的内部实现。
  • DTO:与上面DTO同。

 


 
 
2.2.    让实体处理领域逻辑
实体或领域对象是否是领域逻辑的核心处理者应该是事务脚本和领域模型模式之间的本质区别。“贫血领域模型”的本质应该更贴近事务脚本模式,它们有着类似的优缺点。
以下是任务管理组件的领域模型:
 

 
注:示例中蓝色字体的实体不属于任务管理组件。

让实体处理领域逻辑是一件简单的事,但让实体处理哪些领域逻辑却是一个需要思考的问题。

为实体指派职责的基本原则是“谁拥有谁负责”,即“信息专家模式”。实体应该处理哪些它拥有相关信息和能力的领域职责。这与现实世界是一致的,如一个医生、一个木匠,生病了要找医生,因为医生拥有治病救人的专业知识,打家具要找木匠,因为木匠有做家具的技能。

在上面的领域模型中,创建、更新和删除任务的职责应该放到Task上,这自不必说。那么统计项目实际工时的职责应该放到哪里呢?首先,考查Project对象,它知道自己有哪些迭代,那么它可以用来合计各个迭代的工时。再考查迭代对象,它知道自己有哪些任务,那么它可以累加所有任务的工时。依此类推Task则要负责自己工时的计算。当然这个过程中我们可能会遇到性能问题,这需要在性能目标和可维护性目标之间做出平衡。

“信息专家模式”是一个基本原则,但不是唯一的原则。关于如何分配职责是一个很大的话题这里就不再深入了。

在让实体处理领域逻辑时,应用服务通常只是将职责委托给实体,或协调多个实体完成业务逻辑,应用服务层很薄。如:
@Service("taskFacade")
@Transactional(readOnly = true, rollbackFor = Throwable.class)
public class TaskFacadeImpl implements Task 4000 Facade {
@Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE)
  public long createTask(long projectId, long periodId, Integer periodTag,      Integer index, String name, int priority, Date startDate, Date endDate,
      int points, String description, List<ResourceDetail> resources)
      throws BusinessException {
    Task root = getRootTaskFromCache(projectId, periodId, periodTag);
    if (root == null) {
      root = EntityFactory.getEntity(Task.class);
      root.save(projectId, periodId, periodTag);
      addRootTaskToCache(projectId, periodId, periodTag, root.getId());
    }
    return root.addSubTask(index, name, priority, startDate, endDate, points,description, resources);
  }

  @Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE)
  public void moveUpTask(long taskId) throws BusinessException {
    Task task = Task.get(taskId);
    if (task == null)
      throw new BusinessException("任务不存在。");
    task.moveUp();
  }
……
}
2.3.    赋予实体持久化能力
实体要处理领域逻辑,特别是相对复杂的领域逻辑,没有持久化能力是不行的。Martin Fowler推荐Domain Model与Active Record一起使用。在它们一起使用时,实体就有了持久化能力,如保存、更新和删除自己。


 
那么在使用Struts2 + Spring + Hibernate这样的技术平台时如何给实体赋予持久化能力呢?这里主要是在实体上加了一个静态方法用于获得管理该实体集合的Dao。如实体Task中处理如下
public class Task implements Entity {

……

private static TaskDao dao() {
    return IocBeans.getInstance().getBean("taskDao", TaskDao.class);
  }
}

Dao是Spring的IOC管理对象,如TaskDao的具体实现:
@Repository("taskDao")
public class HibernateTaskDao extends HibernateDao implements TaskDao {

  @Autowired
  public void setSessionFactoryEx(SessionFactory sessionFactory) {
    super.setSessionFactory(sessionFactory);
  }

  public Task get(long id) {
    return super.get(Task.class, id);
  }
  public List<Task> findAll(QueryCriteria criteria, int firstResult,
      int maxResults) {
    ……
  }
……
}

其中,HibernateTaskDao可以通过BeanFactory根据BeanId“taskDao”获得。这里为了获取BeanFactory并方便获得相关Bean实例而创建了类IocBeans,其内容如下
public final class IocBeans implements BeanFactoryAware {

  private BeanFactory beanFactory;

  public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
    this.beanFactory = beanFactory;
  }

  private IocBeans() {
  }

  private static IocBeans instance;

  public static IocBeans getInstance() {
    if (instance == null)
      instance = new IocBeans();
    return instance;
  }

  @SuppressWarnings("unchecked")
  public <T> T getBean(String name, Class<T> requiredType) {
    return (T) beanFactory.getBean(name, requiredType);
  }
}

实现了BeanFactoryAware接口的类可以在容器初始化时,将BeanFactory实例注入进来,从而获得对BeanFactory的操控能力。为了使这个类可以顺利的实例化,还需要在Spring的配置文件中加入如下内容:

<bean class="org.xpup.infras.lang.IocBeans" factory-method="getInstance" />

经过以上努力实体就拥有了持久化能力,与Active Record不同的是这里将数据访问逻辑移入Dao中。Dao中通常只有添加、删除和查找方法,而没有更新方法。使用Hibernate让我们很幸运,我们不需要在Dao中加入更新方法,因为在我们改变实体对象的属性时,Hibernate会帮我们自动完成更新。下面是Task对象的几个领域方法:
public long save(long projectId, long periodId, Integer periodTag)
      throws BusinessException {
    return save(projectId, periodId, periodTag, "任务虚根", TaskDetail.PRIORITY_3,
        null, null, 0, null, null);
  }

  protected long save(long projectId, long periodId, Integer periodTag,
      String name, int priority, Date startDate, Date endDate, int points,
      String description, List<ResourceDetail> resources)
      throws BusinessException {
    Assert.notNull(name);

    setProjectId(projectId);
    setPeriodId(periodId);
    setPeriodTag(periodTag);
    setName(name);
    setPriority(priority);
    set_startDate(startDate);
    set_endDate(endDate);
    setPoints(points);
    setDescription(description);
    setPercentageCompleted(0.0);
    setStatus(TaskDetail.STATUS_OPEN);

    long id = (Long) obtainDao().save(this);
    if (resources != null && resources.size() > 0)
      saveOrUpdateResources(resources);
    return id;
  }
public void saveOrUpdateResources(List<ResourceDetail> resources)
      throws BusinessException {
    if (resources == null || resources.size() == 0) {
      setResources(null);
      IndividualTask.deleteAll(getId());
      return;
    }

    Map<Long, String> resourcesMap = new HashMap<Long, String>();
    Set<Long> ids = new HashSet<Long>();
    for (ResourceDetail resource : resources) {
      IndividualTask itask = IndividualTask.find(getId(), resource
          .getResourceId());
      if (itask == null) {
        itask = EntityFactory.getEntity(IndividualTask.class);
        itask.save(this, resource.getResourceId(), resource.getPercent(),
            resource.isPrincipal());
      } else {
        itask.update(resource.getPercent(), resource.isPrincipal());
      }
      ids.add(itask.getId());
      StringBuilder sbRes = new StringBuilder();
      sbRes.append(itask.getPersonId());
      sbRes.append("|");
      sbRes.append(resource.getPercent());
      sbRes.append("|");
      sbRes.append(resource.isPrincipal() ? "Y" : "N");
      resourcesMap.put(itask.getId(), sbRes.toString());
    }

    StringBuilder sb = new StringBuilder();
    int i = 1, length = resourcesMap.keySet().size();
    for (Long key : resourcesMap.keySet()) {
      sb.append(resourcesMap.get(key));
      if (i++ < length)
        sb.append("_");
    }
    setResources(sb.toString());

    IndividualTask.deleteAllExcept(getId(), new ArrayList<Long>(ids));
  }
public void moveTo(long parentId, Integer index) throws BusinessException {
    Assert.isTrue(index == null || index >= 0);
    if (getParent() == null)
      return;

    if (getParent().getId().longValue() == parentId) {
      int count = getParent().getChildCount();
      if (count == 1)
        return;
      if (index == null && getIndex() == count - 1)
        return;
      if (index != null && index.intValue() == getIndex())
        return;
    }

    Task parent = Task.get(parentId);
    Task p = parent;
    while (p != null) {
      if (this.equals(p))
        return;
      p = p.getParent();
    }

    if (parent == null)
      throw new BusinessException("父任务不存在。");
    if (parent.getProjectId() != getProjectId())
      throw new BusinessException("任务不在同一个项目中,不能移动。");

    this.off();
    if (!getParent().equals(parent)) {
      this.setParent(parent);
      if (this.getPeriodId() != parent.getPeriodId()) {
        this.changePeriodTo(parent.getPeriodId(), parent.getPeriodTag());
      }
    }

    Task child = parent.getFirstChild();
    if (child == null) {
      parent.setFirstChild(this);
      this.setNextSibling(null);
      return;
    }
    int i = 0;
    Task lastChild = null;
    while (child != null) {
      if (index != null && index.intValue() == i) {
        if (index == 0)
          parent.setFirstChild(this);
        Task prev = child.getPrevSibling();
        if (prev != null)
          prev.setNextSibling(this);
        this.setNextSibling(child);
        return;
      }
      i++;
      if (child.getNextSibling() == null)
        lastChild = child;
      child = child.getNextSibling();
    }
    this.setNextSibling(null);
    lastChild.setNextSibling(this);
  }

2.4.    隐藏数据访问对象
这里使用数据访问对象Dao分离数据访问逻辑,并管理实体集合。在职责上,它同DDD中的Repository是同义的。这里叫Dao而没有叫Repository的主要原因是在实际项目中需要向很多人解释这个并不算新的新概念是很麻烦的,所以就依然使用了Dao这个名称。

无论是服务还是实体都需要使用其他实体或实体的集合来完成任务,如在服务中的方法:
@Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE)
  public void moveUpTask(long taskId) throws BusinessException {
    Task task = Task.get(taskId);
    if (task == null)
      throw new BusinessException("任务不存在。");
    task.moveUp();
  }

这里,要向上移动任务,必须先找到任务,再调用任务的移动方法。无论是查找单一任务实例还是集合,都最终需要使用Dao来完成。如果在这里直接使用Dao来获得实体,就必须先获得Dao,这样的代码将会在很多地方出现,即麻烦又难看,所以将这类对集合操作或查找实体的操作封装到实体当中。只是在逻辑上,这样的方法不属于实体的任何一个实例,而是属于类,所以全部采用静态方法,如:
public class Task implements Entity {

  ……

  public static Task get(long id) {
    return dao().get(id);
  }

  public static Task findRoot(long projectId, long periodId, Integer periodTag) {
    return dao().findRoot(projectId, periodId, periodTag);
  }

  public static List<Task> findAll(QueryCriteria criteria, int firstResult, int maxResults) {
    return dao().findAll(criteria, firstResult, maxResults);
  }

  public static long count(QueryCriteria criteria) {
    return dao().count(criteria);
  }

  private static TaskDao dao() {
    return IocBeans.getInstance().getBean("taskDao", TaskDao.class);
  }

}

所有这些静态方法只是简单的将职责委派给Dao来处理,这样Dao就被隐藏在实体的背后,服务类看不到它,其他实体也看不到它,只有本实体类可以使用它。这样可以很大程度地简化的领域模型实现。

2.5.    私有化实体属性写方法
对实体进行有效地封装可以提高内聚性、降低耦合性。比较理想的情况下,我们追求最小暴露,即属性和方法能私有就不设置为公共。这是一件很难在团队中贯彻执行事情,因为这会增加很多成本。这是一件我喜欢做但从不强求的工作,除了方法之外,对于属性的写方法我总会将其可见性设置为protected,如:
public class Comment implements Entity {

  private Long id;

  private Long taskId;

  private Long personId;

  private String comment;

  private Date addedDate;

  protected CommentDao obtainDao() {
    return dao();
  }

  public Long getId() {
    return id;
  }

  protected void setId(Long id) {
    this.id = id;
  }

  public Long getTaskId() {
    return taskId;
  }

  protected void setTaskId(Long taskId) {
    this.taskId = taskId;
  }

  public Long getPersonId() {
    return personId;
  }

  protected void setPersonId(Long personId) {
    this.personId = personId;
  }

  public String getComment() {
    return comment;
  }

  protected void setComment(String comment) {
    this.comment = comment;
  }

  public void setComment(String comment) {
    this.comment = comment;
  }

  public Date getAddedDate() {
    return addedDate;
  }

  public void setAddedDate(Date addedDate) {
    this.addedDate = addedDate;
  }

  ……
}

对属性的写方法的限制访问可以很大程度上降低来自服务类的对实体对象的误操作,当然也需要付出比较高的成本。如在保存时,就需要这样写了
public void save(long taskId, long personId, String comment) {
    setTaskId(taskId);
    setPersonId(personId);
    setComment(comment);
    setAddedDate(DateUtils.today());
    obtainDao().save(this);
  }

2.6.    访问其他组件
实体常常会访问其他组件,这时就建立了实体同其他组件的耦合关系。但如果被引用的组件接口是相对稳定的,这一般也没什么关系,如
public class Person implements Entity {
  ……
public Long save(String username, String password, String realName,
      String email, String telephone, String mobileTelephone)
      throws BusinessException {
    Assert.notNull(realName, "Person的realName必须填写。");
    Assert.notNull(username, "Person的username必须填写。");

    if (!StringUtils.hasText(realName))
      throw new BusinessException("用户姓名必须包括有效字符。");
    if (!StringUtils.hasText(username))
      throw new BusinessException("用户登录名必须包括有效字符。");

    SecurityExFacade securityExFacade = SecurityExFacadeFactory.getInstance()
        .createSecurityExFacade();
    long userId = securityExFacade.createUser(username, password);
    setUserId(userId);
    setUsername(username);
    setRealName(realName);
    setEmail(email);
    setTelephone(telephone);
    setMobileTelephone(mobileTelephone);

    // 检查email是否已经存在?如果存在禁止新建人员。
    if (obtainDao().exists(email))
      throw new BusinessException("电子邮件地址已经存在。");

    return (Long) obtainDao().save(this);
  }
}

这里,在创建人员时,调用安全管理组件的接口来创建一个系统用户。安全组件接口相对稳定,不会有太大的变化,因此实体在处理领域逻辑时,直接使用接口就可以。但有的时候,被调用者并不是很稳定、或根本不知道被调用者是谁,这时就应该使用发布订阅模式或者此种情境下的领域事件模式。还是那么幸运,Spring为我们提供了这个模式的支持。使用这种模式我们可以对调用者和被调用者进行解耦。例如,在此处可以将黑色字体的代码修改为:
UserCreatedEvent event = new UserCreatedEvent(username, password);
AppContext.getInstance().publishEvent(event);
long userId = event.getId();

在两个组件的连接处编写事件监听器,
public class UserEventListener implements ApplicationListener {

  public void onApplicationEvent(ApplicationEvent event) {

    if (event instanceof UserCreatedEvent) {
      SecurityExFacade securityExFacade = SecurityExFacadeFactory.getInstance()
          .createSecurityExFacade();
      UserCreatedEvent e = (UserCreatedEvent) event;
      long userId = securityExFacade.createUser(e.getUsername(), e
            .getPassword());
      e.setId(userId);
    }

  }
}

然后,在Spring的配置文件中加入如下内容,
<bean id="userEventListener"  class="org.xpup.pm.cnct. UserEventListener" />

这样就可以以事件模式工作了,注意这里的事件模式是同步的。

领域事件模式可以用来解除耦合,但反对到处使用,如实体调用Dao或实体调用组件内的实体。

2.7.    使用数据传输对象
几年来我一直在将实体转化为DTO还是不转DTO之间徘徊。不转DTO好处是非常用明显的,服务可以直接将查询结果抛给表现层,这样既减少代码量,又降低了数据复制过程中新建对象的开销。但问题也是存在的,首先,组件间调用不转DTO是不行的,因为如果不转相当于将组件内部的细节给公开了,实体可以公开被访问,实体上的方法也可以被访问,这样的风险很大,也不利于组件间的解耦。那么组件内呢?其实实体也同样暴露给了表现层,虽然是组件内,不同人开发不同层时,这种问题也很严重。另外,实体也可能被表现层开发人员作为表现层模型的一部分,即在action中声明一个属性,这个属性就是实体对象,表现层用它收集页面提交的数据或向页面展示数据。这时各自需求不同很难协调。所以,最后决定除非小项目,否则一定引入DTO对象。实体在转换成DTO时使用Assembler,如
public final class CommentDetailAssembler {

  private CommentDetailAssembler() {
  }

  public static CommentDetail toDetail(Comment comment) {
    if (comment == null)
      return null;
    String personName = null;
    Assert.notNull(comment.getPersonId());
    PersonDetail person = OrgUtils.getPerson(comment.getPersonId());
    if (person != null)
      personName = person.getRealName();

    return new CommentDetail(comment.getId(), comment.getTaskId(), comment
        .getPersonId(), personName, comment.getComment(), comment
        .getAddedDate());
  }

  public static List<CommentDetail> toDetails(List<Comment> comments) {
    Assert.notNull(comments);
    List<CommentDetail> cts = new ArrayList<CommentDetail>(comments.size());
    for (Comment ct : comments) {
      cts.add(toDetail(ct));
    }
    return cts;
  }
}
以上是在使用领域模型过程的一些简单总结,还有很多想说的,今天就写这么多了。

阅读更多
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐