首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

域对象模型的构建

域对象模型 (DOM) 是从我们的问题域中抽象、建模后得到的一系列类。在本章的这一小部分内容中,我们实在无法对DOM的概念进行全面讲述,因此我们推荐您阅读以下书籍,以便对域对象模型有个全面的了解:《Patterns of Enterprise Application Architecture (Addison-Wesley, 2002)一书,作者为Martin Fowler或者《 Domain Driven Design: Tackling Complexity in theHeart of Software ,作者 Eric Evans (Addison-Wesley, 2003)。尽管我们不会深入探讨这个模式中的诸多细节,我们还是向您展示了为什么我们选择为SpringBlog应用建立一个域对象模型,以及我们是如何完成域对象模型的构建过程。

Spring 与域对象模型

本书定位于一本Spring技术书籍,但是您也许会惊奇的发现我们在一些与Spring并无直接关联的主题上投入了大量篇幅。对于我们基于Spring构建的应用而言,唯一始终没有为Spring所管理的对象,就是域对象。这其中的缘由在于,Spring并没有必要涉及域对象的操作与管理。通常,我们通过new操作符创建域对象的实例,尽管需要的时候我们也可以借助Spring创建新的实例,不过,每次需要实例的时候都调用BeanFactory.getBean()显得有些过于追求技术细节,特别是当域对象并无法利用依赖注入所带来的便捷时,更是如此。域对象一般只与域对象模型之外发生有限的关联,并且无需太多的配置。

这里,读者也许会产生好奇,为什么我们如此关注域对象模型?答案非常简单,域对象模型对应用的许多组成部分都产生了影响,无论这些组成部分是不是由Spring进行管理,对域对象模型的正确理解,是使得项目成功的重要因素。

域对象模型(DOM) != 值对象(Value Object)

理解DOM模式的一个关键点在于,它与值对象(通常也成为数据传输对象[Data Transfer Object])模式并不等同。值对象模式的诞生,在于克服传统EJB规范中所有EJB调用都是远程调用的缺陷。为一个EJB进行状态设定往往意味着多次调用,每次调用均为远程方式。通过使用值对象,对象状态可以在一次远程调用中被封装后集中传递,这样就减少了多次远程调用的性能开销。

注意:按照正规的划分,值对象模式与数据传输对象模式并非等价Martin Fowler将值对象定义为“小型简单对象,类似货币或者日期范围,他们的等同性参照并不是基于实例唯一性。”这里就产生了一些困惑,J2EE核心模式分类中,对于数据传输对象模式的许多示例都以术语“值对象”进行描述。本节中,我们将值对象和数据传输对象视为等同,但是我们实际在讨论的,应该是数据传输对象模式。

域对象模型是应用问题域的一个基于对象的展现形式,为了让程序员可以站在对象的层面针对问题领域进行编码,而不是针对计算机中的对象进行编码。值对象只是单纯的对状态进行封装,而对于域对象而言,可以将状态和行为都进行封装 (尽管你也可以选择不在域对象中包含行为)

域对象与值对象的另外一个关键性差异在于,值对象的结构是由远程数据传输的需求所驱动,而域对象是为了表达真实世界中的概念而构建的模型,它与应用的基础结构无关。正如我们稍后将探讨的一样,我们相信并不存在一个固定迅捷的域对象建模法则,我们必须选择一个适合应用和功能的粒度级别。

应用可以同时包含域对象和值对象。在这种情况下,值对象被用于衔接业务层和数据访问层。这些值对象常常被转换为域对象,并被传递到表现层用于界面生成。站在我们的角度,这种方式并不值得推荐。借助Spring,数据访问框架如此强大,很简单就可以完成将数据直接映射到域对象的任务。然而,这种方式有时却会带来一些问题,比如当我们的域对象模型与底层数据存储结构迥异的情况下。在稍后的章节“域对象建模”中,我们将对这一问题进行深入讨论。

为何要创建域对象模型?

域对象模型的创建需要一些前期工作以便确定域对象,并为这些对象建立代码形式的表现形式。可是,这些前期工作为我们程序节约的时间以及规避的错误,远远超出了我们的付出,我们将发现,一个好的域对象模型将有助于更加简单的解决业务问题,同时,我们可以借此面向问题编码,而不是面向机器编码。好的对象模型可以有助于顺利的将业务需求转换为应用。

域对象建模

域对象建模有相当多的方法和途径。一些实践提倡让底层数据存储驱动我们的对象模型,而又有些实践说“让业务领域驱动对象模型”。在实践过程中,我们发现这两种方式的一个中间点,就是域对象模型,既可以简单的运用又可以很好的达到我们的目标。

对于只有五六个数据库表的小型应用而言,为每个表建立一个域对象往往更为简单。尽管这些对象并非严格的域对象——他们的创建并非由问题域所驱动,而是数据结构——对于这样一个小型应用而言已经足够。实际上,在许多小型应用中,域建模过程的结果就是与数据库结构完全对应的对象模型。

对于更大规模的应用,我们需要在真实世界的问题域以及底层数据结构之间进行更深层次的思考。当我们为一个应用构建域对象模型时,我们通常集中在下面三个重点:

l       问题域如何构成

l       域对象将如何被使用

l       底层数据存储是如何构建的

我们所寻求的域对象模型应该是与理想模型尽可能接近,同时不会对数据存储的性能造成太大影响,不会对使用域对象的代码造成过大冲击。

典型地,域对象模型是相当细粒度的,对于单个逻辑概念,我们往往需要多个类进行描述。例如,对于采购系统中的订单。典型情况下,一份订单由一个单独的Order对象以及若干个OrderLine对象(每个OrderLine对象描述了订单中的一项商品)进行描述。试图用一个对象描述订单将导致不必要的臃肿,更不要说实现上带来的难度。当可能使得域对象模型更加简单易用时,我们应该始终寻找机会减小域对象的粒度。

同时我们也会发现我们的域对象模型包含了数据存储中所没有的一些对象。例如,对于一个典型的采购系统而言,其中包含了购物篮的概念,可能是由CartCartItem对象所描述。除非我们需要在用户Session中将这些内容持久化,否则这些域对象并没有对应的数据库表存储结构。记住,我们并不是在简单的构建一个面向对象的数据库表现形式,我们是在对业务领域进行建模。这一点无论如何强调都不过分。我们看到太多项目中,基于由数据存储结构衍生的假冒域对象模型进行构建,不可避免的,这些项目陷于原本可以通过一个良好的域对象模型而避免的抽象不足的困境之中。

我们发现,一个可靠的域对象模型来源于我们对问题域的审视,确认域内的对象,并找出这些对象与应用需求最匹配的自然粒度。尽管我们必须同时考虑域对象的使用与底层数据存储,但是我们并不喜欢这些对我们的域对象模型造成潜在的影响。

非常重要的一点,记住域对象模型的目标在于创建一系列有助于我们和其他开发者在最接近问题域的抽象层次上进行应用构建的类。通常,在构建域对象模型时,我们把其他的影响作为次要因素加以考量。如果发现由于我们的域对象模型造成了性能上的困难,尽管进行调整,但是我们并不推荐一开始就进行这项工作。必须坚信我们的域对象模型就是要受到质疑的。我们并不希望由于错误的观念导致域对象模型所带来的好处受到削弱。

数据库建模与域对象建模

尽管数据库建模与域对象建模非常相似,但是这两者最终的结果很少一致,实际上,我们很少希望发生这样的事情。进行数据库建模时,我们寻求最高效、且最能保持数据一致性的数据结构形式。当进行域对象建模时,性能固然重要,但是构建一个更易于组织逻辑关系的API显得同样重要。通常,我们发现数据库建模中,应该使用最适合数据库特点的建模方式。而域对象建模,至少,采用最适合域对象建模的方式,如果发现性能瓶颈的话,我们可以稍后进行修改。

域对象关系建模

在域对象模型中,常见的错误就是域对象模型由数据库设计所驱动,构建域对象是为了展现与其他域对象之间的关联。这来自于一个事实:对于数据库的多对多关系,必须存在一个中间表完成关联关系的表述。域对象模型中的关联关系应该构建在一个更加OOP的风格之上,域对象本身维护着对其他域对象的引用或者相关的一系列对象列表。

从数据库中读取域对象数据的过程中,一个常见的错误,就是保证所有相关的域对象都必须同时从数据库中读取出来,事实上并非这样。稍后的章节“域对象关联”将对这个问题进行更深层次的探讨。

域对象中是否要封装行为?

并没有任何人强迫我们必须在域对象中对行为进行封装;实际上,我们可以选择在域对象中仅仅包含问题域中的状态展现。大多数情况下,我们发现,将业务逻辑封装在与域对象协同工作的那些服务对象中,会比直接把这些逻辑封装在域对象中更好。典型地,我们把所有在于对象之外,与组件交互的逻辑放入服务对象中。通过这种方式,我们降低了域对象模型与应用逻辑组件之间的耦合关系。这就使得域对象模型可以在更广泛的场景中加以使用,同时,往往我们会发现域对象模型可以在其他应用中重用,以解决同样领域中的问题。

我们倾向于将行为封装在域对象模型之中的情况,就是逻辑仅仅与域对象之间的交互相关。Spring中的jPetStore 示例提供了一个极佳的示例,我们可以将其映射到我们的采购系统示例之中。在这个场景中,用户拥有一个购物篮,以Cart对象以及一系列CartItem对象描述。当用户准备购买购物篮中的商品并创建一份订单时,应用必须建立与模型中CartCartItem对应的Order对象以及相应的数个OrderLine对象。这是一个行为应被封装于域对象模型之中的一个典型示例。从CartOrder的转变是单纯的域对象操作,而与应用中的其他组件没有任何依赖关系。在jPetStore中,Order类拥有一个initOrder()方法,它接受两个参数Account Cart。为Account对象所表示的用户以及针对该用户,根据Cart对象创建Order的逻辑都在这个方法中表达。

与所有建模相关的事情一样,对于何时将逻辑封装在域对象中,何时将其封装于服务对象中,并没有什么固定迅捷的法则。我们必须避免将逻辑封装在域对象之后,导致域对象与其他域对象之外的应用组件相耦合的现象出现。这样,我们就保证了域对象模型尽可能可以得到重用。除此之外,仅与域对象模型相关的逻辑在理论上应该被封装在域对象模型之中,这样无论何时使用域对象模型,逻辑也可以同时得到重用。

SpringBlog域对象模型

由于SpringBlog非常简单,域对象模型也同样非常简单。图11-1 描述了SpringBlog的域对象模型。

11-1. DOM in SpringBlog

尽管这是一个过于简单的域对象模型,它也突出体现了我们之前探讨过的几个关键点。这些将在下面三个章节中进行讨论。

SpringBlog域对象模型中的继承

SpringBlog的核心在于“发布”的概念。“发布”包括两种类型:文章条目,blog的最高层发布内容。以及评论,对特定blog文章条目的评论。尽管SpringBlog并未包含安全性方面的功能,我们仍然需要保证只有Blog拥有者可以创建文章条目,而匿名访客可以创建评论。

我们决定在一个接口BlogPosting中对常见的“发布”特性进行定义,参见代码清单 11-4Entry Comment 对这个接口提供了实现。

代码清单 11-4. BlogPosting接口

package com.apress.prospring.domain;

 

import java.util.Date;

import java.util.List;

 

public interface BlogPosting {

 

   public List getAttachments();

   public void setAttachments(List attachments);

 

   public String getBody();

   public void setBody(String body);

 

   public Date getPostDate();

   public void setPostDate(Date postDate);

 

   public String getSubject();

   public void setSubject(String subject);

}

 

然而,这导致了不当的代码重复现象。Entry Comment 都各自实现了BlogPosting。为了解决这个问题,我们引入了AbstractBlogPosting类,而由EntryComment类对其进行扩展。AbstractBlogPosting的内容请参见代码清单 11-5

代码清单 11-5. AbstractBlogPosting接口

package com.apress.prospring.domain;

 

   import java.util.Date;

   import java.util.List;

 

public abstract class AbstractBlogPosting implements BlogPosting {

 

   protected String subject;

 

   protected String body;

 

   protected Date postDate;

 

   protected List attachments;

 

   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;

   }

 

   public List getAttachments() {

      return attachments;

   }

 

   public void setAttachments(List attachments) {

      this.attachments = attachments;

   }

 

}

 

通过扩展这个基类,我们移除了EntryComment中的所有BlogPosting实现细节,减少了重复代码。作为示例,代码清单 11-6 展示了Entry 类的代码。

代码清单 11-6. Entry

package com.apress.prospring.domain;

 

public class Entry extends AbstractBlogPosting {

 

private static final int MAX_BODY_LENGTH = 80;

 

   private static final String THREE_DOTS = "...";

 

   private int entryId;

 

   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 toString() {

      StringBuffer result = new StringBuffer(50);

      result.append("Entry { id="};

      result.append(entryId);

      result.append(", subject=");

      result.append(subject);

      result.append(" )");

 

      return result.toString();

   }

 

   public int getEntryId() {

      return entryId;

   }

 

   public void setEntryId(int entryId) {

      this.entryId = entryId;

   }

}

 

Spring以及本书示例SpringBlog中,这种模式得到了广泛应用。通用功能在接口中而不是抽象类加以定义,,但是我们针对此接口提供了一个默认的抽象实现。这样做的原因在于,在尽可能的时候,我们可以通过使用这个抽象类实现代码重用,如Entry Comment,这样避免了每个类都必须直接实现BlogPosting接口。另一方面,当我们遇到Entry类必须由Foo类扩展而来的新需求时,我们也可以在Entry类中直接实现BlogPosting接口。这里需要注意的重点是,我们不应该在抽象类中对通用功能进行定义,因为这对我们的集成结构造成了极大限制。作为替代,在接口中定义通用功能,并以一个抽象基类作为默认实现。这样我们可以尽可能的利用继承的优势,而不是对继承层次的无谓约束。

值得注意的一点是,继承树并没有反映到数据库。也就是说,我们并没有专门建立一个BlogPosting表来存储共享数据。而是两个表:EntryComment,分别存储对应的实体数据。这样做的主要原因是我们并不认为像SpringBlog这样规模的应用需要如此复杂的结构,再加上这个示例本身就是用于演示域对象模型与数据库结构之间的差异。定义这个继承层次的主要原因,除了设计优良性考虑之外,也是为了允许SpringBlog可以对EntryComment中的通用数据进行操作,而无需进行特别区分。第七章中的obscenity filter也是一个很好的例子。

SpringBlog中的域行为

尽管SpringBlog域模型非常简单,我们仍然需要在域模型中封装一些逻辑。因为一个Blog发布项的主体可能会非常冗长,当显示Blog发布项列表时,我们需要建立某种机制对其进行摘录。出于这个原因,我们编写了如代码清单 11-7所示的Entry.getShortBody() 方法:

代码清单 11-7. Entry类中的行为

package com.apress.prospring.domain;

 

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();

   }

   /* omitted for clarity */

}

 

这里我们可以看见,为了构造一个缩减的发布项主体,我们截取了发布项主体的前80个字符,并在其之后简单的以省略号结尾。这是一个非常简单的实现,但是它体现了域对象模型中逻辑封装的一个典型场景。

域对象关联

在图11-1展示的域对象模型中,注意到我们在EntryAttachmentCommentAttachment之间定义了一个关联。作为SpringBlog需求的一部分,我们需要在这两种类型的发布项中对文件上传和存储提供支持。在数据库中,我们定义了一个表用于存储附件:attachments.。然后将附件与某个发布项或者评论相关联,此外我们还有两张表: entryattachments commentattachments。我们看到,一个常见的错误就是人们通过创建域对象对这些关联进行建模,而不是通过Java的标准特性将这些对象进行关联。当我们在数据库中有一个一对一关系时,我们可以通过在域对象模型中创建一个对其他对象进行引用的对象完成描述。对于一对多或者多对多关系,使用Java Collection将简化这些复杂关系的表述,同时也非常易于编码。代码清单 11-8, AbstractBlogPosting类的片断摘录,展示了我们如何通过一个List来保存发布项的Attachment对象。

代码清单 11-8. 使用List表述域对象关联

package com.apress.prospring.domain;

 

import java.util.List;

 

public abstract class AbstractBlogPosting implements BlogPosting {

 

   protected List attachments;

 

   public List getAttachments() {

      return attachments;

   }

 

   public void setAttachments(List attachments) {

      this.attachments = attachments;

   }

}

 

我们使用了一个简单的List来对一对多关系进行建模,而非通过额外的对象进行表示。除了减少编码量之外,这个方法也可以防止域对象模型被不必要的类所污染,同时也使得常见的Java概念如Iterator在关联遍历操作中能充分发挥威力。

DOM的标准化(Canonicalization和内存考量

在进行对象建模时,需要考虑的方面之一就是这些对象可能占用的内存资源。通常在我们的应用中,同一个类同一时间会有多个实例在运行。往往同一时间,有多个域对象实例对同一个逻辑实体进行表述。许多情况下,我们无法避免这种情况的出现,但是在某些场景中,我们可以通过阻止创建同一个域对象的多个实例来避免这一情况的出现,这种技术被称为:标准化。

注意标准化 模式常常被归为Typesafe Enum模式。

当我们对这项技术进行探讨之前,我们首先讨论一下这种情况适用的场景。再次回到我们的采购系统中。其中有个域对象Product。可能同一时间有多个用户在查看同一产品信息。通常,这将导致围绕同一个物理产品的多个Product实例被同时创建。我们的虚构采购系统销售10,000种不同的产品类型,这个数字使得标准化变得不切实际,另外,我们还可以看到,采购系统中还有另外一个域对象ShippingCompany,它描述了提供产品的公司(供应商)。我们的系统仅仅包括了三个供应商,那么JVM中在某一时段就可能出现这些供应商的多个实例。这种固定的小数目数据使得ShippingCompany适用于标准化技术。标准化的基本实现方式是通过将类的构造方法标记为私有,同时把所有可能出现的类实例定义为public static final。代码清单 11-9中展示了这样一个实例:

代码清单 11-9. 标准化域对象

package com.apress.prospring.ch11.domain;

 

public class ShippingCompany {

 

   public static final ShippingCompany UPS = new ShippingCompany(1, "UPS");

 

   public static final ShippingCompany DHL = new ShippingCompany(2, "DHL");

 

   public static final ShippingCompany FEDEX = new ShippingCompany(3, "FEDEX");

 

   private final int id;

 

   private final String name;

 

   private ShippingCompany(int id, String name) {

      this.id = id;

      this.name = name;

   }

 

   public int getId() {

      return this.id;

   }

 

   public String getName() {

      return this.name;

   }

  

   public static ShippingCompany fromInt(int id) {

      if (id == UPS.id) {

         return UPS;

      } else if (id == DHL.id) {

         return DHL;

      } else if (id == FEDEX.id) {

         return FEDEX;

      } else {

         return null;

      }

   }

}

 

这里我们可以看到三个以public static final方式构建的ShippingCompany实例。构造方法被声明为private类型,因而外部类不可能创建更多的ShippingCompany实例。fromInt() 方法并非必须,而是我们由hibernate继承而来。当将标准化对象由数据库加载时,fromInt() 才会产生作用。本章稍后我们将从数据访问的角度来对标准化技术进行探讨。

当我们拥有大批量对象需要进行Canonicalize时,比如说我们的应用中需要对Country对象进行大量操作,我们可能会发现,缓存可能是比标准化更好的解决方案。本章中我们并不准备围绕缓存技术进行讨论。如需要进一步了解,可以参阅《Expert One-on-One J2EE Development without EJB 作者 Rod Johnson Juergen Hoeller (Wrox, 2004)

域对象模型综述

本节中,我们对SpringBlog中的域对象模型进行了探讨,同时我们也花费了一些时间讨论域对象建模和实现的基本技术。无疑这个主题比我们这里所讨论的更加庞大,实际上,目前已经有一系列书籍对这个主题进行了深入介绍。这里我们只是浮光掠影,而我们着重探讨的,则在于为什么要构建一个域对象模型,以及与SpringBlog应用相关的几个常见问题。

诚然,即使无需构建域对象模型我们也可以构建出我们的应用程序,不过,根据我们的经验,这样的工作将对降低整体的复杂性、降低维护成本、减少错误有着极大的帮助。

 

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论