探索Domain Model系列
 

2009-07-13 作者:景春雷 来源:景春雷的blog

 

MapperRegistry 是工厂方法的变形?

摘要

本文通过由Active Record模式到Data Mapper模式(使用工厂方法)再到Data Mapper模式(使用MapperRegistry)的一系列重构,探讨模式背后隐藏的思想和面向对象设计原则。本系列的要点是:重要的不是如何做,而是为什么做。

适用读者

基本上,我们猜测本系列的读者是
A. OO达人,但是没看过Martin Fowler的《企业应用架构模式》一书。本系列所探讨的Data Mapper和Ghost等模式都来自《企业应用架构模式》,不过即使您没看过这本书也没关系,我们会对文中涉及到的模式作简要介绍,相信凭借您丰富的OO经验和超强的理解能力,一定能轻松领悟这几个模式。

注:OO,指 Object Oriented,“面向对象”的缩写。

B. 刚刚看完《企业应用架构模式》,但是对书中的Domain Object和Ghost等模式相当迷惑。太好了!本系列就是为您而写的。愿笔者对模式的思考能给您带来一点帮助和启发。
C. SQL达人,对OO从来不感冒。相信您看了本文一定会说:"切,这么简单的问题用两个SQL语句不就搞定了么?干嘛非要用花哨的OO?"嗯,既然您已经是可以用SQL作任何事情的达人,工作对您还有什么挑战性呢?不如开始尝试一下OO,换换口味吧^_^
D. 刚刚修完C语言的大二学生,OO这东西,听说过没见过。也许您会像中途入场的电影观众那样,有一种前不着村后不着店的感觉。不过没关系,反正您早晚要学这些东东的嘛,不如先看看本文,找找感觉。特别是后面列出来的参考文献,都是最近几年最流行的OO经典,不容错过呦。
E. 娱乐记者。您可能正在Google上搜索"某某明星家中闹鬼事件",一不小心进来了。没关系,即来之则安之。相信本文那些生硬古怪的比喻、弯来绕去的图形一定会让您大呼过瘾。您一定会惊诧于这个世界上居然还有人会为了"分层设计"这种无聊的事情苦思冥想,好像如果让下层访问了上层,世界就会颠倒了似的。
X.  即是OO达人又精通企业应用架构。这篇粗浅的文章可能对您帮助不大,但恳请您在离开之前能指点一二,在下先行谢过了!

Active Record 模式

这是最简单的一种对象模型了。书上是这么描述它的:

An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data.
一个封装了数据库表或视图中的一条数据的对象,它不但负责对数据库的访问,而且含有业务逻辑。

很明显地,Person类有两个职责:
    - 访问数据库,形成对象与数据库之间的映射。
    - 封装数据库中的一条数据,并含有业务逻辑。

这违反了“单一职责”这一设计原则。

Single Responsibility Principle
A class should have only one reason to change.
单一职责原则
每个类应该只有一个需要进行修改的理由。
 

我们的Person类有两个可能的修改理由——业务逻辑发生变化时和创建/持久化对象的方法、策略发生变化(例如想在优化性能时采用LazyLoad)时——这是我们不愿看到的。我们需要进行一次重构,将访问数据库的职责从Person中分离出去。

重构,将访问数据库的职责从Person中分离出去

我们进行一次Extract Class重构,将find()、insert()、update()和delete()这四个函数从Person类转移到PersonMapper类中。

Client 代码会像这个样子:
    Person p1 = Person.find(101);
    p1.salary = p1.salary * 1.20;
    p1.update();

为我们的程序添加两个类

我们的数据库中还有一个DEPARTMENT表,它与PERSON表是一对多的关系。

 

表中的数据如下:

 PERSON表  

ID

FIRST_NAME

LAST_NAME

SALARY

DEPARTMENT_ID

101

Neena

Kochhar

17000

20

102

Lex

De Haan

17000

20

103

Alexander

Hunold

9000

20

105

David

Austin

4800

40

106

Valli

Pataballa

4800

40

107

Diana

Lorentz

4200

50


 DEPARTMENT表  

ID

DEPARTMENT_NAME

LOCATION

MANAGER_ID

20

Marketing

LN Shenyang

102

40

Human Resources

Beijing

105

50

Shipping

Shanghai

107


让我们在程序中添加一个Department类和一个DepartmentMapper类。

 

每当我们看到像Person和Department这种相似的类,就会情不自禁地想(强迫症?)是否存在着重复的代码可以被提炼出来。
我们需要为Person和Department添加一个抽象的父类,因为
    - 这样可以为Client代码提供一个抽象的访问接口,符合针对接口编程的设计原则。
    - 减少重复代码,以后修改代码将会更容易。
    - 子类得到了简化,新增子类将更容易。
    - 可读性更好(当然是对OO达人而言,OO菜鸟会变得更迷惑)。
    - 要是不弄个抽象、接口什么的,怎能显得我们的设计有够OO?怎能把新来的菜鸟唬得一愣一愣的?

 
Design Principle
Program to an interface, not an implementation.
设计原则
针对接口编程,而不要针对实现编程。
 

提炼超类,重构成工厂方法模式

让我们作一个Extract Superclass重构,提炼出Person和Department类的超类DomainObject 以及 PersonMapper和 DepartmentMapper类的超类AbstractMapper。它们都是抽象类。



第一件工作是创建一个抽象类DomainObject,让Person和Deparment继承它。然后将insert()、update()、delete()和find()函数提升到DomainObject中。
不过这里有一个问题。Person#update() 里写的是“new PersonMapper().update(this);”,而Department#update() 里写的是“new DepartmentMapper().update(this);”。如果把update()函数提升到DomainObject类中,该如何创建合适的Mapper对象呢?
解决方法是让“创建合适的Mapper”的工作仍然由子类负责,DomainObject负责其余的工作。为此,我们需要在DomainObject中创建一个抽象函数createMapper(),DomainObject#find()等函数调用这个抽象函数完成工作。
我们还添加了一个抽象类AbstractMapper作为访问各个具体的Mapper类对象的接口。

现在Client代码就可以针对接口编程了:
    IList<DomainObject> dirtyObjects = new List<DomainObject>();
    dirtyObjects.Add(person1);  // person1 is an instance of Person
    dirtyObjects.Add(person2);  // person2 is an instance of Person
    dirtyObjects.Add(department1);  // department1 is an instance of Department
    // update all dirty objects.
    foreach(DomainObject dirty_object in dirtyObjects)
    {
        dirty_object.update();
    }

现在,我们的类结构已经是一个标准的工厂方法模式了。“工厂方法”就是createMapper()函数,产品是AbstractMapper类层次。

 
The Factory Method Pattern
Difines an interface for creating an object, but lets subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.
工厂方法模式
定义一个用于创建对象的接口,让子类决定创建哪一个类的实例。工厂方法模式让创建对象的工作延迟到子类去进行。
 

为什么说Domain Object不应该知道Mapper?

现在,我们的设计已经使用了经典的工厂方法模式,还存在什么问题么?为什么Matin Folwer在书中多次强调Domain Object不应该知道Mapper?这是因为在大型的信息系统中,业务逻辑可能会很多很复杂。相应地,Domain Object类层次的结构也会很复杂。Domain Object操心自己的事儿就已经很累了,我们不希望它还要操心自己的持久化问题——一心不可二用嘛。换句话说,我们希望将Domain Object放到一个单独的中,这是应用了分层设计的思想。

 
注:在本系列中,所谓“类A知道类B”中的“知道”与“调用、使用、引用、访问、创建、依赖”都是一个意思。

分层设计
 
分层
从不同的层次来观察系统,处理不同层次问题的对象被封装到不同的层中。


 

进行分层设计时,要注意以下几点:
    - 层和层之间的耦合应该尽可能地松散。也就是说上层应该尽量通过下层提供的接口使用下层提供的功能和服务。当然只是“尽量”,并不是绝对不能访问具象类。
    - 每一层应当只调用下一层提供的功能服务,而不能跨层调用。这一条也不是绝对的。可以根据需要灵活处理。
    - 每一层决不能使用上一层提供的功能服务,也就是说,决不能在层与层之间造成双向依赖或循环引用。这一条是必须遵守的。如果违反了这一条,分层设计就没有意义了。

这里的“层”是逻辑概念。不过如果你喜欢,可以使用物理方法来强化“层”的感觉,例如可以将不同的层放入不同的类库中并使用不同的命名空间。



书上的Data Mapper模式

Matin Folwer给出的Data Mapper模式就是使用了分层设计思想。Domain Object是绝对不可以知道Mapper的。

 
A layer of Mappers (473) that moves data between objects and a database while keeping them independent of each other and the mapper itself.
Mappers层负责在objects和database之间移动数据。它可使objects 和 database互不依赖,并且objects 和 database也不依赖于Mapers。

 

Person不依赖于Person Mapper,Persson Mapper 依赖于Person,这是由Mapper层到domain object层的单向依赖,符合分层思想。Mapper层是domain object层的上层。Default.aspx.cs依赖于Person Mapper和Person,所以表现层位于Mapper层和domain object层之上。

 

这个图可能与直觉不符。我们通常总是在说“表现层、业务逻辑层、持久化层”,好像与数据库相关的东西就应该在最下面才对。如果您觉得不能相信自己的眼睛,就请再仔细地想一下这个问题,因为分层思想即是本篇的重点又是下篇的基础,一定要想清楚才行。

将Factory Method版的DataMapper重构成分层模式的DataMapper

也就是说,要让DomainObject类层次不依赖于Mapper类层次。方法是删除DomainObject中依赖于Mapper的insert()、update()、delete()、find()和createMapper()函数。

 

很好,现在DoaminObject类层次不依赖于Mapper了,可是“创建合适的Mapper”的工作要交给谁来作呢?交给Client么?如果交给Client来作的话,会像这样:
IList<DomainObject> dirtyObjects = new List<DomainObject>();
dirtyObjects.Add(person1); // person1 is an instance of Person
dirtyObjects.Add(person2); // person2 is an instance of Person
dirtyObjects.Add(department1); // department1 is an instance of Department
// update all dirty objects.
foreach(DomainObject dirty_object in dirtyObjects)
{
    if(dirty_object is Person)
    {
        new PersonMapper().update(dirty_object);
    }
    else if(dirty_object is Department)
    {
        new DepartmentMapper().update(dirty_object);
    }
    else
    {
        thow new Exception("should never reach here!");
    }
}

这又违反了“要针对接口编程,而不要针对实现编程”这一设计原则(涂黄颜色的部分即是典型的针对实现编程)。想一想,当我们需要添加一个新的domain object和mapper的时候,会有数不清的Client代码需要修改。要是遗漏了一处,也只有在运行到那段Client代码的时候才会报错,那真是噩梦啦。
解决方法是,将“创建合适的Mapper”的工作要交给一个单独的具有全局唯一访问点的单件类MapperRegistry类来作。

重构,将创建Mapper实例的代码移动到MapperRegistry中

我们需要作一个Extract Class重构,将创建Mapper实例的代码移动到MapperRegistry中。



Web程序中的MapperRegistry的Example:
 
public class MapperRegistry 
{
  
private IDictionary<Type, AbstractMapper> mappers = new Dictionary<Type, AbstractMapper>();
  
public static MapperRegistry instance
  {
    
get
    {
       MapperRegistry result = System.Web.HttpContext.Current.Session["MapperRegistrySingleton"] 
as MapperRegistry;

       
if (result == null)
       {
         System.Web.HttpContext.Current.Session["MapperRegistrySingleton"] = 
new MapperRegister();
         result = System.Web.HttpContext.Current.Session["MapperRegistrySingleton"] 
as MapperRegistry;
       }

       
return result;
    }
  }
  
private MapperRegistry()
  {
    mappers.Add(
typeof(Person), new PersonMapper());
    mappers.Add(
typeof(Department), new DepartmentMapper());
  }
  
public AbstractMapper getMapper(Type t) 
  {
    
return mappers[t];
  }
  
public PersonMapper person
  {
    
get { return getMapper(typeof(Person)) as PersonMapper; }
  }
  
public DepartmentMapper department
  {
    
get { return getMapper(typeof(Department)) as DepartmentMapper; }
  }
} // class MapperRegistry


Client代码会变成这样:
IList<DomainObject> dirtyObjects = new List<DomainObject>();
dirtyObjects.Add(person1); // person1 is an instance of Person
dirtyObjects.Add(person2); // person2 is an instance of Person
dirtyObjects.Add(department1); // department1 is an instance of Department
// update all dirty objects.
foreach(DomainObject dirty_object in dirtyObjects)
{
  MapperRegistry.instance.getMapper(typeof(dirty_object)).update(dirty_object);
}

工厂方法 VS Registry

现在已经很清楚了,Registry和工厂方法实在是有着异曲同工之妙。我个人觉得工厂方法模式更自然一些,不过为了分层设计的需要,不得不用Registry代替工厂方法。使用Registry,在添加新的domain object和mapper的时候,有时会忘记在MapperRegistry中Add它们。
天杀的,我是每次都会忘记啦!
在第100次看到Exception的时候,我终于明白,靠人的自觉性不犯错误,简直就是天真的理想。

思考题

1. 分层设计不但被软件设计广泛使用,在计算机的其它领域(硬件、网络等)也有广泛的应用,请举出一些例子。
2. 不但在计算机领域,在其它非计算机领域也广泛使用了分层的思想,请举出几例。

分层模式下的Lazy Load

摘要

阅读本文并探索
    - 为什么Lazy Initialization只适用于ActiveRecord模式。
    - 芝麻饼公司的Boss是否应该批准降低成本的议案。
    - 为什么DomainObject会遭遇“巧妇难为无米之炊”的尴尬?
    - 如何用依赖倒置原则解除DomainObject的尴尬处境。
    - 如何使用泛型接口简化Value Holder(这个可是书上没有的哦)。
本文将探讨在分层模式下实现Lazy Load所遭遇的困难与迷思,并重点探索模式背后隐藏的思想和设计原则。文章的最后将对书上给出的三种Lazy Load作一个简单的分析和比较。

Lazy Load简介

对于Lazy Load,书上有一个相当精辟的定义。

 

An object that doesn't contain all of the data you need but knows how to get it.
一个对象,虽然它尚未包含所有你需要的数据,但是知道如何去获取这些数据。

 

对于把数据从数据库装载到内存中的工作,一种方便的设计是,在你装载了想要的对象的同时,将相关联的对象也一并装载进来。这使得使用这些对象的其它开发者的工作变得更容易——否则他们不得不自己显式地装载他们需要的那些对象。

可是,如果你对此作一个逻辑上的推论,就会发现,装载一个对象的效果往往是同时装载了大量的相关联的对象——而当你实际上仅需要其中的几个对象时,这样作有时就会影响性能。

Lazy Load暂时中断装载其它关联对象的工作,只在对象中放置一个记号,这样就可只在需要使用关联对象的时候才装载它们。就像许多人所知道的,如果你因为懒惰而没有干某件工作,那么当这件工作变成了其它人的职责而你完全不需要再去作的时候,你就胜利了。(以上三段为原文翻译)

需要注意的是Lazy Load往往不能提高性能反而会降低性能(感谢怪怪)。这是因为所谓的ripple loading(为得到100条数据而执行100个select语句而不是执行一个select语句得到这100条数据)对性能伤害极大,以至于Fowler建议一开始的时候不要用Lazy Load,只有当系统确实因为装载了过多无用的数据而导致性能很差时,才有的放矢地使用Lazy Load提高性能。这也给我们的设计提出了一个要求:可以以很小的代价在正常装载数据和Lazy Load之间切换。另,由于使用Lazy Load的时机并不是本文的重点,这里就不详谈了。

书中共给出了四种实现Lazy Load的方法,分别是lazy initialization, virtual proxy, value holder, 以及 gost。
让我们先来看看lazy initialization。

 
注:“书”指的是Martin Fowler写的《企业应用架构模式》一书。

Lazy Initialization

这是最简单的实现Lazy Load的方法。
lazy initialization的核心代码像这样:

class Department...
  public Person getManager() {
    if(manager == null) {
      manager = new PersonMapper().findById(managerId);
    }
    return manager;
  }
}

 
注:本文将沿用上篇给出的数据库结构和数据。另外,本文与上篇联系十分紧密,强烈建议先看上篇,再看本篇。

为什么说lazy initialization只适用于ActiveRecord模式

这是因为Data Mapper模式使用了分层设计,DataSource层是DomainObject层的上层,DomainObject是不允许知道Mappers的(关于这一点在上篇中有详细论述),而上面的那个代码中使用了PersonMapper,这个是绝对不允许的。

 

教条主义?

的确,我们写代码是为了完成工作,而不是为了遵守什么活见鬼的设计原则!设计原则往往只是提供一个 guideline ——大方向不能搞错,但是可以根据需要灵活处理。不过这一次不同,因为下层不能访问上层的原则是分层设计中最重要的一个而且是唯一一个绝对不能违反的原则。因为层之间的双向依赖对设计的伤害极大:
    - 它使各层的职责不再清晰。
    - 它令各层不再能够被独立地修改。
    - 它使可读性变差。你将无法把层作为一个有清晰边界的系统来单独阅读,你的注意力必须在各层之间漂移。
    - 它令单元测试更加困难。
总之,分层带来的全部好处都会被它消磨殆尽。

但是lazy initialization的简单性太诱惑了,特别是当你被其它三种lazy load搞得晕头转向的时候。我们不禁要问:可不可以只在实现Lazy Load的时候小小地违反一下原则呢?在回答这个问题之前,不妨先来听个小故事。

芝麻饼公司的故事

芝麻胡同®是国际知名的芝麻饼生产商。在一次经营会议上,该公司的总经理提出了一个降低成本的议案。
“根据公司现在的生产规程,每张芝麻饼上应放置100粒芝麻。如果我们将这一数量降低到97粒,顾客是察觉不到的——这是我们经过长期的科学实验发现的。如果从下个月开始实施,那么,年终就可节约成本3000万美元,而利润亦会有相应的提高。”
总经理指着幻灯片上那动人心魄的鲜红的向上箭头,眼中似乎也放出了异样的光彩。
如果你是董事长,是否应该同意这一议案呢?
粗看起来,这个议案似乎不错——即降低了成本,又没有损害产品质量(虽然实际上是损害了,但是顾客察觉不到可以约等于没有损害^_^)。但是,问题在于,没有人知道芝麻数量的底线是多少。没准过了一段时间,又会有个什么销售经理提出一个“有效降低成本议案”:
“根据公司现在的生产规程,每张芝麻饼上应放置97粒芝麻。如果我们将这一数量降低到90粒,顾客是察觉不到的——这是我们经过长期的科学实验发现的。如果从下个月开始实施,那么,年终就可节约成本7000万美元......”
最后,芝麻饼恐怕就要变成烧饼了。

 
注:这个小故事改编自温伯格的《咨询的奥秘》。

是芝麻饼还是烧饼

同样,我们也不知道违反分层设计原则的底线到底是什么——既然实现Lazy Load可以违反原则,那么实现UnitOfWork的时候是不是也可以违反呢?最后依赖将失去控制,分层也名存实亡了。所以,我们决定要在不违反分层设计原则的前提下实现Lazy Load

巧妇难为无米之炊

再来看一下Lazy Load的定义:一个对象,虽然它尚未包含所有你需要的数据,但是知道如何去获取这些数据。很明显,要实现Lazy Load,必须让domain object知道如何去获取数据,但是根据分层设计原则,domain object又不允许知道mappers,天哪,这岂不是正应了那句巧妇难为无米之炊

抽象,我们的老朋友

解决这个问题的方法是:从mappers里把domain object需要的功能服务抽象出来,成为一个窄接口,然后让domain object依赖这个窄接口。具体点说,Department类需要使用PersonMapper类的findById()函数,但是我们又不想让Department类知道PersonMapper类,这时就可把PersonMapper类的“根据id从数据库中装载并创建一个Person对象”的功能抽象出来,成为一个接口:

public interface IValueLoader
{
  object load(long id);
}

然后Department类就可以只依赖这个接口,而不需要依赖PersonMapper了。

 

下面这张图包含了全部的源代码:

 

这样domain object既没有违反原则去依赖上层,又得到了想要的功能服务。虽然增加了一个接口之后程序变得稍稍有些复杂,但是对于一个复杂的大型系统来说,用增加一点复杂性来换取对依赖的可控性,还是很值得的。这个招数被Martin Fowler命名为Separated Interface(476)模式。

 
Separated Interface Pattern  (by 1-2-3)
在使用了分层设计的系统中,下层不应使用上层提供的功能服务。当下层必须使用上层提供的功能服务时,可以将这些功能服务抽象出来成为一个窄接口,然后让下层依赖这个窄接口。Separated Interface模式将上层和下层之间的双向依赖转变成上下两层对一个窄接口的单向依赖。
 

在Separated Interface模式背后,隐藏着一个十分重要的设计原则,它非常著名,却因高深莫测的定义而难以被初学者理解,它就是依赖倒置原则

依赖倒置原则

 
Dependency Inversion Principle
Depend upon abstractions. Do not depend upon concret classes.
依赖倒置原则
要依赖抽象。不要依赖实现类。
 

这听起来和“要针对接口编程,不要针对实现编程”似乎差不多。的确,两个设计原则都告诉我们应该依赖抽象——这个OO与生俱来的能力与万变不离其宗的原则。不过仅仅说“要依赖抽象”也太过抽象了,感觉与老子的那句“玄之又玄,众眇之门”有得一拼。其实,依赖倒置原则的特别之处并不在原则本身,而在于它的效果——将双向依赖转变成单向依赖
另外,在这里我不想解释“倒置”或“反转”的意思,因为我发现这些词除了时髦好听、令人印象深刻外,对问题的理解并无太大帮助,反而容易分散人们的注意力。

模板方法模式和工厂方法模式也是依赖倒置原则的应用

我们都知道,继承关系本身就是一种由子类到超类的很强的依赖关系。在下图中,我们通过显式地画出这个依赖关系来强化这一概念。

 

在下图中,因为DomainObject的whoAmI()函数引用了Person类和Department类,又增加了DomainObject对Person类和Department类的依赖。

 


现在,依赖倒置原则就可以派上用场了。
首先,要搞明白谁在上层谁在下层。子类Person和Department由于继承了DomainObject,所以可以随意使用DomainObject中定义的函数,这是与生俱来的依赖关系,我们无法消除,所以Person和Department是DomainObject的上层,DomainObject处于下层。那么,下层类DomainOjbect对上层类的依赖就是不好的东西了,我们要消除它。
下一个问题就是,下层类使用了上层类那些功能服务?答案是“判断对象是Person还是Department”。
接着,我们就可以把这个功能服务抽象出来,成为DomainObject的抽象函数getType(),DomainObject直接依赖这个抽象函数;而Person和Department则要实现这个抽象函数。

 


由此可见,所谓“抽象出来的窄接口”不一定非得是interface,也可以是抽象函数,还可以是delegate(书上的Ghost模式就是一个很好的例子)。

重构,提炼类

回到Department的定义中,可以看到一处潜在的重复代码(下面涂黄颜色的部分)。

public class Department...
{
  public Person getManager() {
    if (_manager == null) {
      _manager = _loader.load(managerId) as Person;
    }
    return _manager;
  }
}

以后只要每添加一个Lazy Load,就必须重复一遍这个代码,所以应该把它提炼出来。在作了一个Extract Class重构之后,这段代码被提炼到一个新的类ValueHolder中(被修改的部分涂了蓝色)。

 

使用泛型

现在我们的Lazy Load解决方案已经和书上的Value Holder模式基本差不多了,但是仍有一处坏味道如芒刺在背,让人浑身不自在,这就是Person#getManager()函数中的向下转型(downcasting)操作(下面代码中涂了黄颜色的部分就是向下转型操作)。

public class Department...
{
  public Person getManager()
  {
    return _manager.getValue(managerId) as Person;
  }
}

 
向下转型三宗罪  (by 1-2-3)
向下转型,其罪有三:
    一)破坏了抽象。接口就像魔术师手中的那块黑布,将实现细节隐藏在下边;而向下转型就像一个顽皮的孩子,非要掀开这块布看个究竟不可。
    二)必须知道额外的信息。例如getValue()的返回值类型是Object,那么调用这个函数的人怎么能知道应该把这个Object转型成Person还是转型成Women?抑或必须转型成List?
    三)必须等到运行期才能知道转型是否成功。你可能说我只要每次写了向下转型的代码之后立即就运行程序测试一下就可以了。我相信你足够勤奋,而且你的所有同事也都和你一样勤奋。但是,如果有一天写getValue()函数的那个人修改了函数真正返回的对象的类型呢?他未必会逐个通知所有调用了getValue()的人吧,再说他也未必就知道到底有多少人调用了这个函数。
 

在 .net framework2.0 中新增的泛型功能可以很好地解决这个问题,而且使用起来也很简单,只要把IValueLoader和ValueHolder中的“object”换成“T”就可以了。下面列出源代码,修改的部分涂了黄颜色。

public interface IValueLoader<T>
{
  T load(long id);
}

public class ValueHolder<T>
{
  private IValueLoader<T> _loader;
  private T _value;

  public ValueHolder(IValueLoader<T> loader)
  {
    _loader = loader;
  }

  public T getValue(long id)
  {
    if (_value == null)
    {
      _value = _loader.load(id);
    }
    return _value;
  }
}

而Department和PersonMapper也要做些修改。

public class Department...
{
  private ValueHolder<Person>
_manager;
  public void setManager(ValueHolder<Person>
arg)
  {
    _manager = arg;
  }
  public Person getManager()
  {
    return _manager.getValue(managerId);
  }
}

public class PersonMapper : ..., IValueLoader<Person>

{
  public Person load(long id)
  {
    return findById(id);
  }
}

一对多的情况

例如,想统计每个部门的员工数量,需要为Department增加如下代码。

public Class Department...
{
  private ValueHolder<IList<Person>> _employees;
  public void setEmployees(ValueHolder<IList<Person>> arg)
  {
    _employees = arg;
  }
  public IList<Person> getEmployees()
  {
    return _employees.getValue(id);
  }
  public int employeeCount
  {
    get { return getEmployees().Count; }
  }
}

修改DepartmentMapper。

public class DepartmentMapper...
{
  public DomainObject findById(long id)
  {
    Department result = load from db by id;
    result.setManager(new ValueHolder<Person>(PersonMapper()));
    result.setEmployees(new ValueHolder<IList<Person>>(new PersonMapper()));
    return result;
  }
}

让 PersonMapper 实现 IValueLoader<IList<Person>> 接口。

public class PersonMapper : ..., IValueLoader<Person>, IValueLoader<IList<Person>>
{
  public IValueLoader<IList<Person>> load(long id)
  {
    return findByDepartmentId(id);
  }

}

这样就搞定了。

说再见之前

这篇有些长,所以首先感谢您耐心的读到这里。上面那个例子与书上的Value Holder模式十分接近,只不过加上了泛型而已。在附录中,我对书上的三种Lazy Load模式作了一个非常简单的分析和比较,并为每种模式绘制了一幅UML类图,希望能对您有所助益。

附录 书上给出的三种Lazy Load浅析

Virtual Proxy

优点
    - 对domain object没有侵入性,透明度为100%。
    - 在普通装载和Lazy Load之间切换只需要修改Mapper中的一行代码。
缺点:
    - 在静态类型语言里,实现一个domain object的virtual proxy非常麻烦,所以此方法最好只用于需要返回domain object的集合的情况。
UML图:



Value Holder

优点:
    - 既可以返回单个domain object,也可以返回domain object的集合。
缺点:
    - 对domain object有侵入性,当需要由普通装载转为Lazy Load时,必须修改domain object。
    - 需要对ValueHolder返回的结果作向下转型操作。
UML图:



Ghost

优点:
    - 既可以返回单个domain object,也可以返回domain object的集合。
    - 当由普通装载转为Lazy Load时对DataSource层的改动非常小。但是前提是在项目一开始的时候就要预料到将来使用Ghost的可能性非常大而在一开始的时候就把DataSource层设计成支持Ghost的形式。
    - 当由普通装载转为Lazy Load时对Domain Object层的改动非常小,前提是你的项目愿意并且能够支持AOP。
    - 这是最Lazy的Lazy Load,你可以在一开始的时候让所有的domain object都是ghost。
缺点:
    - 对Mappers有一定的侵入性。如果你的项目一开始的时候没有考虑要使用Ghost,并且设计得内聚度不是很高的话,可能需要进行一番重构才能比较容易支持Ghost。
    - 在Mappers#AbstractFind()里调用CreateGhost()的确可以很方便地一下子把所有domain object都变成ghost,但是如果我们只想Lazy Load部分domain object,这招就不灵了。
    - 对domain object的侵入性非常大,每个属性都要加一个Load()函数。当然如果你的项目愿意并且能够支持AOP就没什么问题。
UML图:

参考文献

[Fowler POEAA]
Fowler, Patterns of Enterprise Application Architecture. Addison-Wesley, 2003.
影印版:企业应用架构模式(影印版)。中国电力出版社,2004。
[Fowler Refactoring]
Fowler, Refactoring: Improving the Design of Existing Code. Addison-Wesley, 1999.
影印版:重构——改善既有代码的设计(影印版)。中国电力出版社,2003。
[Fowler UML]
Fowler et al, UML Distilled: A Brief Guide to the Standard Object Modeling Language(Sencond). Addison-Wesley, 2000.
中文版:徐家福 译,UML 精粹(第2版)标准对象建模语言简明指南。清华大学出版社,2002。
[Freeman et al]
Freeman et al, Head First Design Patterns. O’Reilly, 2004.
影印版:深入浅出设计模式(英文影印版)。东南大学出版社,2005。
[王咏武 王咏刚]
王咏武 王咏刚, 道法自然:面向对象实践指南。电子工业出版社,2004。

工具箱

那个太极小图标来自《Head First Design Patterns》,用FireWorks 6.0和GIMP 2.2作了一些处理。UML图使用Visio 2003+Pavel Hruby's UML2.0 模板绘制。图片上使用了手写字体方正静蕾简体。文字部分使用Google 拼音输入法键入。


火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织