在当前的开发者社区,广泛流行一种被Martin
Fowler称为贫血领域模型的构架模式。该模式由于大师的批判而饱受指责。这个模式有个致命的缺陷:在处理复杂领域时常常表现不佳。很多迹象表明,当我们面对复杂应用时,最好还是转向一个基于丰富领域模型的构架。
尽管丰富领域模型有着显而易见的好处,但也给实践带来了挑战,这既有构建技术上的原因,也有设计方法上的原因。对于构建技术,如Annotation、Aspect和DI等复杂技术的使用,最终能够清晰的被掌握,但在设计方法上,往往由于实践的不同而难以取得共识。
本文的目的仅仅是在技术上给出一种由贫血领域模型向丰富领域模型转换的方案,供有相同需要的同行参考。
1.前提和限制
无论使用哪种领域模型,通常都需要借助一些工具的支持,这些工具包括Ioc容器和O/R映射工具。因作者经验所限,当提及这些工具时,只意味着Java世界里的Spring和Hibernate。
有关贫血领域模型的论述请参考:
http://www.martinfowler.com/bliki/AnemicDomainModel.html
贫血领域模型的最佳实践请参考:
https://appfuse.dev.java.net
丰富领域模型的最权威指南请访问:
http://www.domaindrivendesign.org
由于语境的不同,不同的分层模式的术语具有不同的含义,但它们所要实现的任务,是可以分离出来的,而这些任务,目前还没有造成广泛的语义混乱。为了更好的比较本文中提及的两种构架模式,将它们所要完成的任务定义如下:
任务 |
描述 |
表现逻辑 |
接收来自系统外部的请求,将请求代理到其他模块,并将返回的处理结果,通过某种方式呈现给请求者。 |
应用逻辑 |
是用例的外观的实现,协调用例的真正实现者完成一次应用程序相关的功能。 |
领域逻辑 |
对问题领域最本质内容进行建模,实现体现用户核心价值的功能。 |
持久化逻辑 |
与数据的外部存储交互。 |
基础服务 |
更加的以技术为中心,为软件系统的各模块提供基础支持。 |
2.贫血领域模型构架
2.1.分层模式
即使同样打着贫血模型的标签,它们也会有不同的风格。下面是比较典型的一种:
层 |
任务 |
对象 |
描述 |
表现层 |
表现逻辑 |
模型对象 |
Model。领域层中的实体/值对象,也可以用独立的对象。 |
视图 |
View |
控制器 |
Controller。通过某种机制而获得对用户请求的响应。 |
领域层 |
在领域逻辑中混合了应用逻辑 |
服务 |
Service。同时处理领域逻辑和应用逻辑 |
实体 |
Entity。领域模型的静态视图 |
数据源层 |
持久化逻辑 |
DAO对象 |
有很多实现方式,如JDBC、Hibernate、iBATIS、JPA、EJB CMP |
基础设施层 |
基础服务 |
|
|
2.2.示例代码
现在假设有一个在线购物网站,我们要浏览产品列表,然后选中了一个感兴趣的产品,此时我们需要查看该产品的详细信息。
1)页面发出请求
下面是一个可能的JSF代码片断:
<h:commandlink value="#{product.name}"
action="#{product.edit}"></h:commandlink>
<f:param
value="#{product.id}" name="productid"></f:param>
|
2)分派到控制器
假设请求被调度到控制器yourpackage.action.ProductAction:
package yourpackage.action;
......
public class ProductAction extends BaseAction
{
//通过依赖注入的服务
private ProductService productService;
//作为模型对象的领域对象
private Product product;
private Integer id;
//响应查看产品信息的事件
public String edit() {
product
= productService.getProduct(id);
return “product”;
//JSF将其解析为product.xhtml视图
}
} |
3)领域层的服务
一般会有一个服务接口ProductService,然后是该接口的实现ProductServiceImpl:
package yourpackage.service;
......
public class ProductServiceImpl extends BaseService
implements ProductService {
//通过依赖注入的DAO对象
private ProductDao productDao;
//获取产品信息的领域逻辑方法
public Product getProduct(Integer id) {
return
productDao.getProduct(id);
}
} |
4)领域层的实体
下面是JPA风格的实体,该实体仅用于承载数据,而没有领域行为(贫血之说由此而来)。
package yourpackage.model;
......
@Entity
public class Product {
@Id @GeneratedValue(strategy=GenerationType.AUTO)
@Column(name="Id")
public
Long getId() {
return id;
}
@Column(name="Name",
length=30, nullable=false)
public
String getName() {
return name;
}
} |
5)DAO对象
这里同样需要一个接口,其实现类如果用JPA的话:
package yourpackage.dao;
......
public class ProductDaoJpa extends BaseDaoJpa
implements ProductDao {
public Product getProduct(Integer id) {
return (Product) getJpaTemplate().find(Product.class,
id);;
}
......
} |
2.3.分析
优点:
* 获得了分层的最基本的好处。
* 易于理解,快速掌握:没有采用更复杂的技术,并且有丰富的示例资源。
* 有广泛的工具支持,非常容易的从Spring/Hibernate类型的框架获益。
* 适合于模型简单,以CRUD操作为主的领域。
缺点:
* 模型的领域表达能力欠缺。
* 代码职责分配不合理。
3. 丰富领域模型
3.1分层模式
典型的DDD风格的分层模式如下:
层 |
任务 |
对象 |
描述 |
表现层 |
表现逻辑 |
模型对象 |
领域层中的实体/值对象,也可以用独立的对象。 |
视图 |
|
控制器 |
|
应用层 |
应用逻辑 |
服务 |
对用例建模 |
领域层 |
领域逻辑 |
服务 |
对领域操作建模 |
实体 |
对领域概念建模、并可被持久化 |
值对象 |
Value Object。对领域概念建模 |
存储库 |
Repository。隔离持久化 |
基础设施 |
持久化逻辑和基础服务 |
数据映射 |
通常以DAO模式封装持久化操作 |
基础服务 |
|
在实践中,我个人更倾向于将数据映射从基础设施层分离出来,这样会有一个更清晰的层次结构。
无论哪种领域模型,在是否将领域对象传递到表现层作为模型数据的问题上一直存在争议。要做出恰当的设计决定,需要在灵活性和严谨性之间做出平衡。有一些技术上的方法可以使领域对象在作为DTO对象进行传递时得到保护,可以参考下面的文章《Protecting
the Domain Model》:
http://api.blogs.com/the_catch_blog/2005/05/protecting_the_.html
这些做法在一定程度上弥补了直接传递领域对象带来的负面影响。
3.2有什么不同?
从上面的表格我们可以发现,要做的事还是那些事,只不过部分职责被重新分配了而已。相对于贫血领域模型,在新的分层模式中:
* 不变的是表现层;
* 被合并的是数据源层,现在成了基础设施的一部分;
* 增加的是应用层;
* 被重新组织的是领域层,其职责被分配到相应的对象中:实体、值对象、服务和存储库。
这里最大的变化有三个:
1)应用逻辑被从领域逻辑中独立出来,形成了新的应用层。该层的接口按照用例进行设计,因此粒度较大;该层反映的是用系统所实现的任务,如果需要,也能反映工作流程。
2)领域层只反映领域逻辑中最核心部分,因此也是最复杂的(如果领域复杂程度超过技术复杂程度的话)。其中的实体不但持有数据,还具备丰富的行为;服务是一些领域相关的操作(这不同于应用层的服务,更不同于基础设施层的服务);通过存储库隔离了与数据技术的联系。
3)领域层可以独立地访问其他层的服务和资源。这是一个有争议的话题,同时很有技术上的挑战性,下文会通过实例代码进行说明。
4.重构到丰富领域模型构架
4.1.技术方案
为了简便起见,下文中使用A和B这两个别名分别代表“贫血领域模型”和“丰富领域模型”,使用名称空间表示模型中的层,如B::应用层表示丰富领域模型的应用层。
通过前面的比较,可以很容易的得到下面转换的方案:
1)保持表现层不变
2)分离出来应用层。从A::领域层移出应用逻辑形成B::应用层。
3)重构一个纯粹的领域层。将A::领域层的领域逻辑部分进行分解:
# 将概念性的逻辑重构到B::领域层.实体和B::领域层.值对象;
# 将操作性逻辑重构到B::领域层.服务;
# 将数据源访问抽象为存储库接口,存储库的实现由Ioc容器注入。
4)重构数据访问对象,使其实现领域层的存储库接口。
5)基础设施层保持不变。
不多做解释,下面通过代码进行说明。
4.2. 示例代码
还是前面的在线购物网站,但这次使用了一个稍微能体现领域逻辑的用例:列出与当前产品相关的其他产品。
1)页面发出请求
下面是一个可能的JSF代码片断:
- <h:commandLink action="#{product.related}" value="#{相关产品}">
- <f:param name="productid" value="#{product.id}" />
- weaver>
- aspectj>
然后,需要在JVM加载参数中指定下面的内容:
-javaagent: <path-to-ajlibs>/aspectjweaver.jar</path-to-ajlibs>
|
现在可以说大功告成了。
2)DAO对象
现在DAO需要实现的接口变成了领域层的ProductRepository:
package yourpackage.dao;
import yourpackage.domain;
......
public class ProductDaoJpa extends BaseDaoJpa
implements ProductRepository {
public
Product getProduct(Integer id) {
return (Product) getJpaTemplate().find(Product.class,
id);;
}
public
Product getRelatedProduct(Integer id) {
//这里是具体的实现代码
}
} |
5.结论
要构建一个纯粹的领域模型,往往需要在领域对象中直接使用外部服务,如数据库访问,外部资源,或其他必要的服务。在没有强大的Annotation、AOP及DI技术的支持下,这些外部服务或资源很难被注入到领域对象中,由此形成了贫血模型的设计风格。
特别地,对于Spring用户来说,在2.0版发布之后,问题的解决变得更容易一些。利用Spring集成的AspectJ实现向领域对象进行依赖注入,可以使领域对象可以表达更丰富的逻辑,从而过渡到丰富领域模型。在这个关键问题解决之后,剩下的问题也就迎刃而解了。 |