“请问我从这儿出发应该走哪条路呢?”
“这多半看你要去哪儿。”猫说。
“我不太介意去哪儿——”爱丽斯答道。
“那你走哪条路都无所谓。”猫说。
“——只要我最后能到一个地方就可以了。”爱丽斯补充说。
“哦,当然,”猫说,“只要你走得够远,你一定可以做到的。”
——《爱丽斯漫游奇境》
摘要
本文通过由Active Record模式到Data Mapper模式(使用工厂方法)再到Data Mapper模式(使用MapperRegistry)的一系列重构,探讨模式背后隐藏的思想和面向对象设计原则。本系列的要点是:重要的不是如何做,而是为什么做。
适用读者
基本上,我们猜测本系列的读者是
A. OO达人,但是没看过Martin Fowler的《企业应用架构模式》一书。本系列所探讨的Data Mapper和Ghost等模式都来自《企业应用架构模式》,不过即使您没看过这本书也没关系,我们会对文中涉及到的模式作简要介绍,相信凭借您丰富的OO经验和超强的理解能力,一定能轻松领悟这几个模式。
B. 刚刚看完《企业应用架构模式》,但是对书中的Domain Object和Ghost等模式相当迷惑。太好了!本系列就是为您而写的。愿笔者对模式的思考能给您带来一点帮助和启发。
C. SQL达人,对OO从来不感冒。相信您看了本文一定会说:"切,这么简单的问题用两个SQL语句不就搞定了么?干嘛非要用花哨的OO?"嗯,既然您已经是可以用SQL作任何事情的达人,工作对您还有什么挑战性呢?不如开始尝试一下OO,换换口味吧^_^
D. 刚刚修完C语言的大二学生,OO这东西,听说过没见过。也许您会像中途入场的电影观众那样,有一种前不着村后不着店的感觉。不过没关系,反正您早晚要学这些东东的嘛,不如先看看本文,找找感觉。特别是后面列出来的参考文献,都是最近几年最流行的OO经典,不容错过呦。
E. 娱乐记者。您可能正在Google上搜索"某某明星家中闹鬼事件",一不小心进来了。没关系,即来之则安之。相信本文那些生硬古怪的比喻、弯来绕去的图形一定会让您大呼过瘾。您一定会惊诧于这个世界上居然还有人会为了"分层设计"这种无聊的事情苦思冥想,好像如果让下层访问了上层,世界就会颠倒了似的。
X. 即是OO达人又精通企业应用架构。这篇粗浅的文章可能对您帮助不大,但恳请您在离开之前能指点一二,在下先行谢过了!
Active Record 模式
这是最简单的一种对象模型了。书上是这么描述它的:
一个封装了数据库表或视图中的一条数据的对象,它不但负责对数据库的访问,而且含有业务逻辑。
很明显地,Person类有两个职责:
- 访问数据库,形成对象与数据库之间的映射。
- 封装数据库中的一条数据,并含有业务逻辑。
这违反了“单一职责”这一设计原则。
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?怎能把新来的菜鸟唬得一愣一愣的?
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类层次。
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放到一个单独的层中,这是应用了分层设计的思想。
分层设计
从不同的层次来观察系统,处理不同层次问题的对象被封装到不同的层中。
进行分层设计时,要注意以下几点:
- 层和层之间的耦合应该尽可能地松散。也就是说上层应该尽量通过下层提供的接口使用下层提供的功能和服务。当然只是“尽量”,并不是绝对不能访问具象类。
- 每一层应当只调用下一层提供的功能服务,而不能跨层调用。这一条也不是绝对的。可以根据需要灵活处理。
- 每一层决不能使用上一层提供的功能服务,也就是说,决不能在层与层之间造成双向依赖或循环引用。这一条是必须遵守的。如果违反了这一条,分层设计就没有意义了。
这里的“层”是逻辑概念。不过如果你喜欢,可以使用物理方法来强化“层”的感觉,例如可以将不同的层放入不同的类库中并使用不同的命名空间。
书上的Data Mapper模式
Matin Folwer给出的Data Mapper模式就是使用了分层设计思想。Domain Object是绝对不可以知道Mapper的。
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.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:
{
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. 不但在计算机领域,在其它非计算机领域也广泛使用了分层的思想,请举出几例。
参考文献
[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 拼音输入法键入。