MapperRegistry 是工厂方法的变形? ——探索Domain Model系列(上)

2024-04-17 01:18

本文主要是介绍MapperRegistry 是工厂方法的变形? ——探索Domain Model系列(上),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

来源:博客园 1-2-3.cnblogs.com  http://www.cnblogs.com/1-2-3/archive/2007/10/15/917762.html 

      “请问我从这儿出发应该走哪条路呢?”
      “这多半看你要去哪儿。”猫说。
      “我不太介意去哪儿——”爱丽斯答道。
      “那你走哪条路都无所谓。”猫说。
      “——只要我最后能到一个地方就可以了。”爱丽斯补充说。
      “哦,当然,”猫说,“只要你走得够远,你一定可以做到的。”
                                         ——《爱丽斯漫游奇境》

摘要

本文通过由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类有两个可能的修改理由——业务逻辑发生变化时和数据库发生变化时——这是我们不愿看到的。我们需要进行一次重构,将访问数据库的职责从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. 不但在计算机领域,在其它非计算机领域也广泛使用了分层的思想,请举出几例。

参考文献

[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 拼音输入法键入。

这篇关于MapperRegistry 是工厂方法的变形? ——探索Domain Model系列(上)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/910423

相关文章

MySQL 表空却 ibd 文件过大的问题及解决方法

《MySQL表空却ibd文件过大的问题及解决方法》本文给大家介绍MySQL表空却ibd文件过大的问题及解决方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考... 目录一、问题背景:表空却 “吃满” 磁盘的怪事二、问题复现:一步步编程还原异常场景1. 准备测试源表与数据

python 线程池顺序执行的方法实现

《python线程池顺序执行的方法实现》在Python中,线程池默认是并发执行任务的,但若需要实现任务的顺序执行,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋... 目录方案一:强制单线程(伪顺序执行)方案二:按提交顺序获取结果方案三:任务间依赖控制方案四:队列顺序消

SpringBoot通过main方法启动web项目实践

《SpringBoot通过main方法启动web项目实践》SpringBoot通过SpringApplication.run()启动Web项目,自动推断应用类型,加载初始化器与监听器,配置Spring... 目录1. 启动入口:SpringApplication.run()2. SpringApplicat

使用Java读取本地文件并转换为MultipartFile对象的方法

《使用Java读取本地文件并转换为MultipartFile对象的方法》在许多JavaWeb应用中,我们经常会遇到将本地文件上传至服务器或其他系统的需求,在这种场景下,MultipartFile对象非... 目录1. 基本需求2. 自定义 MultipartFile 类3. 实现代码4. 代码解析5. 自定

Python文本相似度计算的方法大全

《Python文本相似度计算的方法大全》文本相似度是指两个文本在内容、结构或语义上的相近程度,通常用0到1之间的数值表示,0表示完全不同,1表示完全相同,本文将深入解析多种文本相似度计算方法,帮助您选... 目录前言什么是文本相似度?1. Levenshtein 距离(编辑距离)核心公式实现示例2. Jac

C#高效实现Word文档内容查找与替换的6种方法

《C#高效实现Word文档内容查找与替换的6种方法》在日常文档处理工作中,尤其是面对大型Word文档时,手动查找、替换文本往往既耗时又容易出错,本文整理了C#查找与替换Word内容的6种方法,大家可以... 目录环境准备方法一:查找文本并替换为新文本方法二:使用正则表达式查找并替换文本方法三:将文本替换为图

SQL Server 查询数据库及数据文件大小的方法

《SQLServer查询数据库及数据文件大小的方法》文章介绍了查询数据库大小的SQL方法及存储过程实现,涵盖当前数据库、所有数据库的总大小及文件明细,本文结合实例代码给大家介绍的非常详细,感兴趣的... 目录1. 直接使用SQL1.1 查询当前数据库大小1.2 查询所有数据库的大小1.3 查询每个数据库的详

Java实现本地缓存的四种方法实现与对比

《Java实现本地缓存的四种方法实现与对比》本地缓存的优点就是速度非常快,没有网络消耗,本地缓存比如caffine,guavacache这些都是比较常用的,下面我们来看看这四种缓存的具体实现吧... 目录1、HashMap2、Guava Cache3、Caffeine4、Encache本地缓存比如 caff

Java 中编码与解码的具体实现方法

《Java中编码与解码的具体实现方法》在Java中,字符编码与解码是处理数据的重要组成部分,正确的编码和解码可以确保字符数据在存储、传输、读取时不会出现乱码,本文将详细介绍Java中字符编码与解码的... 目录Java 中编码与解码的实现详解1. 什么是字符编码与解码?1.1 字符编码(Encoding)1

Python Flask实现定时任务的不同方法详解

《PythonFlask实现定时任务的不同方法详解》在Flask中实现定时任务,最常用的方法是使用APScheduler库,本文将提供一个完整的解决方案,有需要的小伙伴可以跟随小编一起学习一下... 目录完js整实现方案代码解释1. 依赖安装2. 核心组件3. 任务类型4. 任务管理5. 持久化存储生产环境