3.基于Spring的应用程序的设计和实现(构建领域对象模型)

构建领域对象模型

 

 

一个领域对象模型(DOM)是一系列的对象模型组成的问题领域。(比较拗口)。比较详细的描述可以参考《Patterns of Enterprise Application Architecture》,或者《Domain-Driven Design: Tackling Complexity inthe Heart of Software》。这里只讲下它的大概概念,虽然不打算在这里讲得非常细,但还是会解释为什么以及如何构建领域对象模型。

Spirng和领域对象模型

也许你会奇怪,为啥在讲spring的时候要搭上这么一个话题。因为spring构建的应用程序中,唯一一个没有被spring管理的就是领域对象模型(虽然spring中有@componet标签可以管理,但是大多数时候我们都是选择在应用程序中来管理)。而之所以这么做的原因就是spring没有必要去管理它!通常,我们在service层或者数据持久层使用new()操作来创建一个领域对象模型。虽然spring支持实例化对象(使用bean 标签,等等),但一般不这么做,因为这些模型和除了其本身外,和外部容器没什么依赖性。于是你就疑问了,那,我们去管他干啥??答案很简单,DOM是应用程序中一个至关重要的部分,它将影响程序的很多由spring管理的的其他内容,确保其正确将是保证程序正确的重要因素。

DOM并不仅仅是一个有值的对象

要理解DOM,首先得知道的就是它不仅仅是一个有值的的对象(通常被叫做数据传输对象)。数据传输对象通常是用来解决原来EJB对象传输之间的缺陷。用其来传输那些需要被远程调用的对象。

注:官方解释的数据传输对象并不等价于有值对象。具体请参考网站:http://martinfowler.com/bliki/ValueObject.html 

DOM是一个基于基本对象来表现问题领域的方式,旨在允许程序员按照普通对象的方式来构建。通常对象只包含一些状态属性,而领域对象模型不仅包含状态属性,也包含一些行为(当然也可以不包含)。另外一个根本的不同点是数据传输对象是用来传输数据的,而领域对象模型是用来抽象现实世界的对象。我们之后会讨论到,领域对象模型将没有一个固定的定义,需要你自己来选择它所代表的属性和方法的颗粒度大小。

通常,在一个程序中可能即存在值对象,也存在领域对象。在这种情况下,值对象常用在service层和别的层交互时,比如表现层和数据持久层。这些值对象将在合适的时候被转化为领域对象用来在表现层表现。然而,这种做法在这里我们并不推荐。一个原因是维护工作会因此而变得复杂,因为将值对象转换为领域对象的同时也意味着将其转换为其他值对象。另一个原因是在spring中,数据持久和web框架已经非常成熟,可以直接通过map方式将数据库层对象转换为领域对象,即用于表现,也用于数据层持久化。

为何要构建领域对象模型

创建一个DOM需要一些前期工作来抽象模型然后准备一些数据层代码来表现这个模型。然而,这些工作都是值得的,因为它将节省你正直实现时调试bug的时间。事实发现,一个好的DOM能使解决业务问题更容易。既然领域对象模型是根据问题领域构建而不是根据程序机制。一个好的DOM也将使得开发人员更好的将业务需求转化为程序代码。

构建领域对象模型

对于构建领域对象模型,目前有很多争论。一些坚持应当根据数据对象来构建。而另一些则声称:“让业务模型来驱动”。实际中,我们发现取2者的折中反而会得到比较好的效果,既能够得到比较好的表现形式也比较容易去构建。对于小型的项目,比如只有5-6张表的那种,通常只需根据数据库的表一一构建领域模型即可。虽然那些对象不是严格意义上的领域对象,但它们已经足够接近了。事实上,许多小型项目的领域对象模型也的确能够满足数据对象的要求。而对于大型项目,需要花费更多的精力在构建那些抽象现实世界的领域模型。

通常在构建时,需要关注如下三点:

-问题领域的结构是如何的

-领域对象将如何被使用

-底层数据持久该如何构建

值得注意的是,我们理想中的模型将尽可能少的影响数据持久的性能。

总体而言,也许你的逻辑代码中会返回多个类型对象,但一个DOM却是一个单独颗粒的。比如,假设我们有个进销存系统的订单。通常一个订单是由一个order对象(代表订单)与多个orderLine对象(代表订单的内容条目)。即一个order下面存在多个orderLine,然而这样的设计是没有必要的并且实现起来也比较麻烦。合适的做法是创造一个大颗粒度的领域对象,把这些东西都包含进去。也许在开发过程中你会发现,DOM中包含一些在数据库中压根就不存在的对象。比如,还是刚才的例子,进销存系统中存在一个叫做购物车的东西。(也许用Cart对象和CartItem对象来表示)。这些临时性的数据通常只在用户的session内存中做修改而不入数据库;根据领域模型的构建原则,我们不仅仅构建与数据库一一对应的值对象,而是根据业务领域而来。这是非常值得强调的。

所以,在构建一个领域对象模型时,我们得先深入的了解我们的业务/问题领域,确定在这些领域中需要些什么对象,然后根据现实世界的抽象合理的确定颗粒度大小,之后分辨出哪些部分是需要与数据持久层打交道的。时刻牢记在心,我们构建领域对象模型的初衷是设置一些列对象来帮助开发人员和业务人员更好的抽象出我们的问题需求,方便大家的理解和开发。而其他的问题我们会先放在一边,比如性能问题。。。不管怎样,在目前为止,确定问题都已经被设计成了合理的领域对象模型将会对整个程序提供巨大的好处。

数据库模型和领域对象模型

虽然数据库模型和领域对象模型很相像,甚至获取的结果都一样,但事实上是完全不同的。在构建一个数据库模型时,我们更关注的事那些数据的结构和访问时的性能问题。而构建领域对象时,性能问题就变得比较棘手,其实任何面向对象模型都有这个问题。事实上,如果能在设计数据库时就根据领域对象来设计是最好的。如果性能的确是一个问题的话,我的建议是先构建完领域对象,然后再出于性能的考虑对其进行修改。

领域对象模型关系

通常会碰到一个常见的误区,构建一些专门用来描述领域对象模型之间关系的领域对象。究其原因是为了表达数据库中多对多的关系。DOM中的关系描述应该被设计成一个更面向对象的方式,通常是一个领域对象中包含其他一个或一组领域对象。一个常见的错误就是根据数据库来设计DOM,关于详细我们将在今后“领域对象模型关系”这一章中详细讨论。

是否要封装行为?

就目前为止我们还没在任何领域对象中讨论过封装行为,虽然在一开始的定义中有提到。事实上,在构建时,这是一个可选的选项(但很重要)。因为我们更倾向于将业务行为抽离出去,放入service层,这不仅能减小对象的体积,也使得对象更易被重用,维护也更方便。看起来很完美,不是吗?但其实并不完全如此。在一些情况下,将行为设计在领域对象中会有很多好处。在spring的JPetStore代码示例中就有个很好的例子:用户可以获得一个购物车,并且购物车中有一些所购物品。当用户准备订购这些物品时,系统会生成一张订单,但是请注意,光生成一张空订单是不符合业务条件的,订单中还得有订单条目(即购物车中的物品)。将购物车中的物品转换为订单条目是一种特殊行为:1. 它只与领域对象有关。2. 它对其他模块无任何依赖。于是我们很自然的会想到在Order对象中创建一个initOrder()方法,也许有2个参数:账户和购物车。所有根据购物车生成订单的行为都可以被封装在这个方法内,鉴于这个方法只和对象打交道,也不存在何时调用的问题,将其放入领域对象中也不会造成复用问题。所以结论出来了,当业务行为仅与领域模型有关,应该将其放入DOM中。


Spring博客应用实例

下面让我们看看DOM模型在spring博客中的实例应用。首先请看下图的领域对象图。

12-1:博客发布相关的DOM设计

12-2: 博客用户和角色相关的DOM设计

虽然这个DOM不是很复杂,但是它的确展现了DOM的特点,别急,接下来的三章我们还将继续讨论。

Spring博客DOM的继承

Spring博客的核心就是发布博文。发布分2种类型:即发布博文,和发布评论。所以将发布操作中相同的部分抽象成一个接口如下:

//Listing 12-3. The BlogPosting Interface
package com.apress.prospring3.springblog.domain;

import java.util.Date;
import java.util.Set;
import com.apress.prospring3.springblog.domain.Attachment;

public interface BlogPosting {
	public String getBody();

	public void setBody(String body);

	public Date getPostDate();

	public void setPostDate(Date postDate);

	public String getSubject();

	public void setSubject(String subject);
}
 

然而,为了避免不必要的代码重复(必须分别对博文发布和评论发布进行实现),我们创建一个抽象类AbstractBlogPosting用来被继承。如下:

//Listing 12-4. The AbstractBlogPosting Class
package com.apress.prospring3.springblog.domain;

// Import statements omitted
public abstract class AbstractBlogPosting implements BlogPosting {
	protected Long id;
	protected String subject;
	protected String body;
	protected Date postDate;
	protected String createdBy;
	protected DateTime createdDate;
	protected String lastModifiedBy;
	protected DateTime lastModifiedDate;
	protected int version;

	public String getBody() {
		return body;
	}

	public void setBody(String body) {
		this.body = body;
	}

	public Date getPostDate() {
		return postDate;
	}

	public void setPostDate(Date postDate) {
		this.postDate = postDate;
	}

	public String getSubject() {
		return subject;
	}

	public void setSubject(String subject) {
		this.subject = subject;
	}
	// Other setter/getter methods omitted
}
 

通过继承这个基类,我们把重复的实现代码都给去掉了。接下来只用关注不同点的实现了:

//Listing 12-5. The Entry Class
package com.apress.prospring3.springblog.domain;

// Import statements omitted
public class Entry extends AbstractBlogPosting {
	private static final int MAX_BODY_LENGTH = 80;
	private static final String THREE_DOTS = "...";
	private String categoryId;
	private String subCategoryId;
	private Set<EntryAttachment> attachments = new HashSet<EntryAttachment>();
	private Set<Comment> comments = new HashSet<Comment>();

	public Entry() {
	}

	public String getShortBody() {
		if (body.length() <= MAX_BODY_LENGTH)
			return body;
		StringBuffer result = new StringBuffer(MAX_BODY_LENGTH + 3);
		result.append(body.substring(0, MAX_BODY_LENGTH));
		result.append(THREE_DOTS);
		return result.toString();
	}

	public String getCategoryId() {
		return this.categoryId;
	}

	public void setCategoryId(String categoryId) {
		this.categoryId = categoryId;
	}

	public String getSubCategoryId() {
		return this.subCategoryId;
	}

	public void setSubCategoryId(String subCategoryId) {
		this.subCategoryId = subCategoryId;
	}

	public Set<EntryAttachment> getAttachments() {
		return this.attachments;
	}

	public void setAttachments(Set<EntryAttachment> attachments) {
		this.attachments = attachments;
	}

	public Set<Comment> getComments() {
		return this.comments;
	}

	public void setComments(Set<Comment> comments) {
		this.comments = comments;
	}
}
 

如上所示,这是一个非常经典的spring程序结构。共同的功能被定义在接口而不是抽象类中,但提供一个抽象类来实现。这样设计的好处是我们可以利用抽象类的默认实现来避免每一个实现类不得不去实现一大堆重复的代码。当然,如果对于Entry类有一个特别的需求话,我们可以直接实现BlogPosting接口。这里要指出的是,要根据接口来定义功能而不是抽象类。还有一点要注意的是,我们没有根据数据库结构来设计程序,换句话说,我们没有定义一张BLOG_POSTING表来储存那些共享的数据(博文和评论)。这么做的原因是我们不想把结构复杂化;而且也体现了不根据数据库来设计领域模型的原则。

Spring博客领域对象模型中的行为

虽然spring博客的例子比较简单,我们还是在领域对象里封装了一些逻辑。因为博文可能会非常长,为了在显示博文列表方便,我们通常只取文章的一些片段。所以我们创建方法Entry.getShortBody()(注意这个方法没有定义在接口和抽象类里),如下所示:

//Listing 12-6. Behavior in the Entry Class
package com.apress.prospring3.springblog.domain;

// Import statements omitted
public class Entry extends AbstractBlogPosting {
	private static final int MAX_BODY_LENGTH = 80;
	private static final String THREE_DOTS = "...";

	public String getShortBody() {
		if (body.length() <= MAX_BODY_LENGTH)
			return body;
		StringBuffer result = new StringBuffer(MAX_BODY_LENGTH + 3);
		result.append(body.substring(0, MAX_BODY_LENGTH));
		result.append(THREE_DOTS);
		return result.toString();
	}
	// Codes omitted
}
 

你可以看到,我们只取前80个字符作为片段。这是一个非常简单的实现,不过用来说明足够了。

领域对象模型关系

在12-1中,我们定义了博文和评论之间的关系以及评论和附件的关系。作为spring博客需求的一部分,我们希望能够在发布博客和发布评论时都能上传附件。在数据库里,我们设计表ENTRY_ATTACHMENT_DETAIL 和 COMMENT_ATTACHMENT_DETAIL。一个常见的错误是创建一个领域对象模型来维护他们之间的关系,而不是使用标准的java特性来表现。当存在一个一对一关系是,你可以在一个DOM中引用另外一个DOM。而对一对多或者多对多则是一个DOM中引用另一个DOM的集合。请参考在12-7中Entry类的一些片段:

//Listing 12-7. Using Set for Domain Object Relationships
package com.apress.prospring3.springblog.domain;

// Import statements omitted
public class Entry extends AbstractBlogPosting {
	private Set<Attachment> attachments = new HashSet<Attachment>(0);

	public Set<Attachment> getAttachments() {
		return this.attachments;
	}

	public void setAttachments(Set<Attachment> attachments) {
		this.attachments = attachments;
	}
	// Codes omitted
}
 

不使用额外的对象而是使用一个set来表现关系。是不是很有效?

领域对象模型总结

在这一章里,我们观察了DOM在Spring博客离得应用,并且我们花费了一些篇幅讨论了领域对象的构建和实现。毫无疑问,这个话题要比我们这里讨论的广泛得多。事实上,如果有时间的话推荐各位可以读一些专门讨论此问题的书籍,比如《Domain-Driven Design: Tackling Complexity in the Heart of Software, Addison-Wesley Professional》。在这我们只是粗略地浏览了问题的表面。不管怎样,也许不使用DOM你的程序也能跑起来,但我们还是强烈建议您的使用,相信它会极大的减小项目的复杂度,后期的维护成本以及更少的BUG。


下一节:类型校验和格式转换 http://wsjjasper.iteye.com/blog/1574823

猜你喜欢

转载自wsjjasper.iteye.com/blog/1574590