事务脚本、领域模型及表模块是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 TaskFacade {

@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

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 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 getBean(String name, Class requiredType) {

return (T) beanFactory.getBean(name, requiredType);

}

}

实现了BeanFactoryAware接口的类可以在容器初始化时,将BeanFactory实例注入进来,从而获得对BeanFactory的操控能

力。为了使这个类可以顺利的实例化,还需要在Spring的配置文件中加入如下内容:

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 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 resources)

throws BusinessException {

if (resources == null || resources.size() == 0) {

setResources(null);

IndividualTask.deleteAll(getId());

return;

}

Map resourcesMap = new HashMap

String>();

Set ids = new HashSet();

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(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 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的配置文件中加入如下内容,

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

领域事件模式可以用来解除耦合,但反对到处使用,如实体调用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 toDetails(List

comments) {

Assert.notNull(comments);

List cts = new

ArrayList(comments.size());

for (Comment ct : comments) {

cts.add(toDetail(ct));

}

return cts;

}

}

以上是在使用领域模型过程的一些简单总结,还有很多想说的,今天就写这么多了。

java 域模型_基于Spring实现领域模型模式 - RUP实践者指南 - JavaEye技术网站相关推荐

  1. java路由器开发_基于spring cloud的智能路由

    smart-route 基于spring cloud的智能路由,功能如下 开发模式:优先调用本地服务, order=0 SIT优先:优先调用指定IP服务, order=100 远程调试:远程调试指定服 ...

  2. 招聘管理系统软件java源码_基于Spring Boot的java开源招聘源码-铭阳招聘管理系统...

    铭阳招聘管理系统 铭阳招聘管理系统,采用流行的框架Spring Boot+mybatis+ehcache开发,实现了权限管理,solr全文搜索引擎,系统具执行效率高.模板自由切换.后台管理功能灵活等诸 ...

  3. java寄存器_汇编学习 1 寄存器的作用 寻址方式 - DraculaW - JavaEye技术网站

    首先 是寄存器的介绍 寄存器名     说明                            功能 eax:            累加器                 加法乘法指令的缺省寄存 ...

  4. 文件表单带数据一起提交spring_基于 Spring 实现管道模式的最佳实践

    管道模式(Pipeline Pattern) 是 责任链模式(Chain of Responsibility Pattern) 的常用变体之一.在管道模式中,管道扮演着流水线的角色,将数据传递到一个加 ...

  5. 基于 Spring 实现管道模式的最佳实践

    本篇为设计模式第二篇,第一篇可见设计模式最佳套路 -- 愉快地使用策略模式 管道模式(Pipeline Pattern) 是责任链模式(Chain of Responsibility Pattern) ...

  6. eclipse java敏捷_基于Spring+Hibernate+Eclipse进行敏捷Java开发(2)

    敏捷绘图 在继续下面的讨论前,请允许我简短地介绍一种新颖而相当简单的技术-敏捷绘图:我正是借助于它绘制了上面的图3-7.这一技术成为略显"笨重"的统一建模语言(UML)的一种替代, ...

  7. 事件驱动java实现_基于spring实现事件驱动

    通过阅读该篇博客,你可以了解了解java的反射机制.可以了解如何基于spring生命周期使用自定义注解解决日常研发问题.具体源码可以点击 链接 . 问题描述 在日常研发中,经常会遇见业务A的某个act ...

  8. Java毕业设计_基于spring的一汽大众4s店汽车销售系统的设计与实现

    基于spring的一汽大众4s店汽车销售系统的设计与实现 基于spring的一汽大众4s店汽车销售系统的设计与实现mysql数据库创建语句 基于spring的一汽大众4s店汽车销售系统的设计与实现or ...

  9. java oauth sso 源码_基于Spring Security Oauth2的SSO单点登录+JWT权限控制实践

    概 述 在前文<基于Spring Security和 JWT的权限系统设计>之中已经讨论过基于 Spring Security和 JWT的权限系统用法和实践,本文则进一步实践一下基于 Sp ...

最新文章

  1. 拦截器获取不到sesssion作用域的值_ES6--块级作用域
  2. Win7下共享文件(以及凭据管理简单介绍)
  3. android 安全讲座第三层 linux权限基础知识
  4. OpenGL 行星asteroids系统的实例
  5. 【华为云技术分享】使用CloudIDE快速体验基于华为云ModelArts SDK的AI开发
  6. python 两两组合
  7. 实现下拉菜单的宽度与登录人ID长度的匹配
  8. 国内安装K8S镜像源
  9. 360浏览器集成IE8内核
  10. java 定时任务表达式(网络总结)
  11. python图像拉伸_python处理图像
  12. 2021-2027全球与中国筒式过滤器外壳市场现状及未来发展趋势
  13. ip: either “dev“ is duplicate, or “type“ is garbageip: either “dev“ is duplicate, or “txqueuelen“ i
  14. ai关键词整理(分享)
  15. 将linux内核烧进arm板,ARM开发板上uClinux内核移植
  16. Unity 移动键Q的三种用法 For Mac,Windows类同
  17. 练习:自撸整数进制转换器(二、八、十六进制转十进制)
  18. DirectX图形开发(一)-基本概念
  19. js删除指定html及子标签,js中如何删除某个元素下面的所有子元素?(两种方法)...
  20. c语言中字符指针加加操作,C语言 指针操作练习

热门文章

  1. 做空机构12次围攻下,跟谁学逆势续写高增长神话
  2. Qt之QToolButton 实现动态拖拽Drag、Drop功能
  3. RecyclerView使用问题集合http://www.jianshu.com/p/333fe22cabc6
  4. c语言黑色星期五代码解析,C语言判断黑色星期五
  5. 普中C51烧录时芯片超时可能的原因
  6. 《2022国民抑郁症蓝皮书》:94%的患者接受线上问诊
  7. 【Spring框架】——5.Bean的作用域及自动装配
  8. 最新版vagrant_2.2.7_x86_64 window版本分享
  9. 微服务基础模块搭建过程
  10. VirtualBox 使用总结