WizardWu 編程網

一位台灣的工程師,接觸 .NET 逾十年,近年研究 SQL Server、Performance Tuning、手機應用

博客园 首页 新随笔 联系 订阅 管理

本帖介绍 Prototype Pattern (原型模式),并以一个「人事招聘程序」作为示例来说明。

--------------------------------------------------------
本帖的示例下载点:
https://files.cnblogs.com/WizardWu/090713.zip
第一个示例为 Console Mode (控制台应用程序) 项目,第二个示例为 ASP.NET 网站项目。
执行示例需要 Visual Studio 2008 或 IIS + .NET 3.0,不需要数据库。
--------------------------------------------------------

Prototype Pattern (原型模式)

Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
                                 - Design Patterns: Elements of Reusable Object-Oriented Software

 

原型模式就是以一个既有的原型实例当作范本,利用复制的方式,动态获得这个原型实例的状态以及全部的字段和属性,以此创建一或多个相同的对象,而且不需要知道任何创建的细节。

Prototype 模式打个通俗的比方:假如您在图书馆看到几本自己喜欢的书籍,当看到某些知识点时,想在上面作相关记号,但由于其是图书馆的书,不能在上面乱涂乱画。此时您只好把相关的章节,用复印机把它复印出来,然后在自己复印的纸张上作记号。在 Prototype Pattern 里,Clone 方法就如同此种复印的动作,用户从一个既有的原型实例 (如同图书馆里的书),复印后得到一或多个新的拷贝,不会破坏原本的原型,且用户不必知道原型的内容和格式。

Prototype 也具有一种「展示」的意味,就像是车展上的「原型」车款。当您对某个车款感兴趣时,您可购买相同的车款,而不是车展上展示的那辆车。

在软件设计方面,也常需要进行此种对象复制。例如我们要写一套室内设计软件,软件的操作界面上有一条 Toolbar,用户只要单击 Toolbar 上的 Button,或用鼠标拖曳到设计窗格中,就可创建一个桌子或椅子的副本,并可事后改变它的颜色或位置。当设计师改变设计图中的副本对象时,Toolbar 上的「原型」对象并不会跟着被改变。同样的观念,亦适用于工业设计 CAD 软件、图像处理软件,以及 Visual Studio 等各种软件的设计。


Prototype 模式的重点在于 Clone 方法,它负责复制 (克隆) 出一个新的对象并返回,而不是用 new 运算符和某个类的构造函数去创建实例。在此模式中,派生类如何覆写父类的 Clone 方法将是重点。而 clone 的方式,又可分为「浅拷贝 (shallow copy)」和「深拷贝 (deep copy)」,在介绍这个 Prototype 模式之前,先简单介绍一下这两种拷贝方式的差异 [1], [2], [3], [4] :

  • 浅拷贝; 浅表复制 (shallow copy):对象拷贝时,如果字段是「值类型 (Value Type)」,则直接复制其值 (亦即复制整个字段);若字段为「引用类型 (Reference Type)」,则只复制其「引用 (reference; pointer)」,但不复制引用的字段,亦即若更改了任一个副本对象的某一个「引用类型」字段,则原型正本对象、其他副本对象,也全部会一并更改 (如同本帖的第三个示例 02_Employee / 02_ShallowCopy_fail.aspx.cs),也就是说正本和所有的副本,都指向了内存的同一个位置。
  • 深拷贝; 深层复制 (deep copy):不论对象的字段为「值类型」或「引用类型」,都会完整地复制,而且这些字段和属性都是完全独立的。在深拷贝中,所有的对象都是重复的。

另补充,.NET 的类型系统,分为「值类型」、「引用类型」两种,其对象在内存中的存储方式不同,如下:

  • 值类型:只需要一段单独的内存,用于存储实际的数据在「栈 (Stack)」里,例如:int、byte、float、double、bool、struct、enum、char、...等类型。
  • 引用类型:需要两段内存,第一段存储实际的数据,其总是位于「堆 (Heap)」中;第二段是一个存在「栈」里的引用 (reference; pointer),其指向数据在「堆」中的实际存放位置,例如:object、string、class (包括自定义类)、interface、delegate、array (参考本帖的第三、第四个示例) 等类型。

但 string (字符串) 较特殊。string 虽然是「引用类型」,但却拥有「值类型」的特性。在 Prototype Pattern 及本帖的四个示例中,当透过 MemberwiseClone 方法做「浅拷贝」时,对象的 string 字段仍会被完整地复制,其结果就如同 int 等「值类型」的字段一样。


如下图 1 及下方示例 01_Shell,我们可透过自定义的 Prototype 抽象类,搭配 .NET 最顶层基类 System.Object 的 MemberwiseClone 方法,达成对象的「浅拷贝」,亦即复制某个对象其所有「字段 (field)」的值;但在 .NET 中,亦可舍弃此一自定义抽象类,让图 1 中的 ConcretePrototype1 类、ConcretePrototype2 类,改为实现 .NET 原生的 System.ICloneable 接口,透过实现此接口唯一的一个 Clone 方法,来达成对象的「浅拷贝」或「深拷贝」。


图 1 此图为 Prototype 模式的经典类图

 

01_Shell / Program.cs

 

上方图 1 的 Class Diagram,以及「Shell (壳)」示例中,客户端程序透过抽象类 Prototype 的定义来操作其派生类。先选择一个「原型」实例,亦即 ConcretePrototype1 类或 ConcretePrototype2 类的实例,通过调用它所覆写抽象父类的 Clone 方法,获得一个和它一样、有相同 id 值的新对象,而非透过 new 运算符去创建实例。而拷贝完成后,拷贝的原型样本 (p1、p2),和副本 (c1、c2) 是两个独立的对象,可以独立变化和修改。


但为什么不用 new 运算符加上某个类的构造函数去创建实例,而要再衍生出这种 Prototype Pattern 呢?其中一个原因,是系统设计上,类的种类可能会很多,而难以整合成特定的自定义类时;或类与类之间有大量平行阶层结构,类的数量过多会造成管理上的困难。例如本帖一开始提到的室内设计软件,桌子类和椅子类,又可各分成: 方形的、圆形的、其他各种形状的…,若全部都要写成不同的类,类的数量会很可观。

此外,也是为了避免整个系统中,类与类之间结构的剧烈变化,避免为了重载一个新的函数,导致我们要修改的不是一个类,而是整个继承关系体系里的每一个类。

另一个原因,是用户在用鼠标操作这个室内设计软件时 (运行时期),若要重复创建相同的圆形桌子对象时,用 Prototype Pattern 这种对象复制的方式,由于对象初始化的内容都相同,会比从头用类去创建新的实例要容易,同时能在客户端程序中隐藏对象创建的细节,且在速度和性能上会较优 [15], [17]。


接下来的三个示例,为一个人事招聘系统的部分代码,我们以此为例来实现原型模式。某间公司要招聘「程序员」、「行政文员」两种职务,其中的 Employee 为顶层的抽象类,两个派生类 Developer 和 Typist 必须实现其 Clone 方法。示例执行结果如下图 3。

由于两个派生类 Developer 和 Typist,其成员都是 int 等「值类型」或 string,因此我们在客户端程序 (Page_Load) 中执行「浅拷贝」时,既有的第一个「原型」对象 - dev 和 typist 实例,其所有的字段,会被逐位复制到新对象 devCopy1、devCopy2、typistCopy1 中,且正本对象和副本对象的字段各自独立、存储在内存的不同位置。

此外,我们看到 Developer 第一个「拷贝」出来的对象 devCopy1,它的 Role、PreferredLanguage 字段,都和原型对象 dev 一样,是「资深工程师」和「C#」,而我们可以将 Name 字段改成「李大同」,而不影响原型对象 dev 的 Name 字段。


图 2 Sybase PowerDesigner 12.5 的「反向工程」功能,已可解析 C# 3.0 的 Auto-Implemented Property 语法

 

02_Employee / 01_ShallowCopy.aspx.cs

 

 
图 3 上方示例 01_ShallowCopy.aspx.cs 的执行结果。在两个派生类的 Clone 方法中,用 MemberwiseClone 方法实现了「浅拷贝」


接下来我们要把上方的示例,在派生类 Developer 中,添加一个「引用类型」的数组 int[] intArray。由于数组是「引用数型」,因此在做「浅拷贝」时,原始对象及其复本引用的是同一个对象 (指向内存的同一个位置);当我们在 Page_Load 做完 Developer 的两次「浅拷贝」,再把最后一位程序员「吴宇泽」,他的 intArray 数组的第一个元素,其值从 1 改为 9,此时执行结果如下图 4 ,您会发现其他两位程序员 - 王小明、李大同,他们的 intArray 数组的第一个元素,也都同时被改成 9 了。

 

02_Employee / 02_ShallowCopy_fail.aspx.cs

 

 
图 4 Developer 类添加了一个「引用类型」的数组,造成「浅拷贝」时,只复制了「引用」,却未复制引用的字段


在下方的最后一个示例中,我们要更正前一个示例 02_ShallowCopy_fail.aspx.cs 的错误。我们把前述 Developer 类里面的 Clone 方法,从「浅拷贝」改成「深拷贝」,以搭配数组这个「引用类型」的拷贝;而另一个 Typist 类,由于没有「引用类型」成员,因此不需要更改,Clone 方法仍然延用「浅拷贝」。执行结果如下图 5,我们同样在 Page_Load 做 Developer 的两次拷贝,只不过这两次是「深拷贝」。结果符合我们需求,只有最后一位程序员「吴宇泽」,他的数组的第一个元素,其值从 1 改为 9,其他两位程序员则不受影响。

 

02_Employee / 03_DeepCopy.aspx.cs

 

 
图 5 将 Developer 类中 Clone 方法里的「浅拷贝」改成「深拷贝」,以配合该类中「引用类型」成员的复制


--------------------------------------------------------

Prototype Pattern 适用的情景:

  • 当系统中,某个系列的类,变化和扩展特别频繁的时侯。
  • 当系统应该独立于它的产品创建时。
  • 想对客户端程序,隐藏类的具体内容。
  • 希望依用户的操作,在「执行时期」动态地加载 (dynamic loading) 或动态获得对象的状态。例如本帖一开始提到的室内设计软件,会依使用者鼠标的操作来动态创建副本。
  • 同上一点,当需要的类型不是编译时就能确定的,而是能在运行过程中动态选择的。
  • 当类型本身可枚举的种类非常固定时,例如一家软件公司,只有「主管、程序员、业务员」三种职务,当公司标到大型项目,需要招幕一百个程序员,与其通过某种机制 new 一百个程序员实例,不如通过一个现成的「程序员」原型实例,克隆一百个对象出来。
  • 当一个类的多个实例,他们之间的字段和属性只有些许不同时。
  • 当一个类的实例,只能有几种不同状态组合的其中一种时。
  • 当对象的初始化需要高成本,例如:构造函数的参数、字段数量很多很复杂时。

Prototype Pattern 的优点:

  • 可达到资源优化,避免用 new 创建实例会较消耗资源,且这样做速度也比 clone 对象慢 [15], [17], [18]。
  • 独立性和灵活性高,容易动态加载新功能。
  • 减少类的数量。避免类的种类太多时,其子类数量会迅速地增加。
  • 能在客户端程序中隐藏对象创建的细节。

Prototype Pattern 的缺点:

  • 每一个类都需要配备并覆写 Clone 方法,且撰写时需要做整个系统架构的通盘考量。

Prototype Pattern 的其他特性:

  • 可避免形成多个类与类之间的大量平行 (平级) 阶层结构,在宽度和深度上的扩展。
  • 「浅拷贝」可以提供低成本的对象复制;但通过「序列化 (Serialization)」进行的「深拷贝」代价就比较大,而非低成本的。

 

--------------------------------------------------------

本帖的最后,提供一位 Java 大师 - 结城浩,所绘制的 Prototype Pattern 趣味四格漫画,原地址如下:

Giko 猫谈 DP 四格漫画:
http://www.javaworld.com.tw/jute/post/view?bid=44&id=40932&sty=3&age=0&tpg=1&ppg=1#40932
http://www.hyuki.com/dp/cat_Prototype.html


∧_∧  敲敲敲  ╱
(    )  ∧ ∧ < 等于是利用 copy & paste 来制作实例..恩....。
(    )  (,,゚Д゚)  ╲____________
______ (つ_つ____
|   日∇ ╲|ThinkPad|╲
|      =========  ╲

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
____________

| 喔~、是 Prototype Pattern 吗?
╲ __ __________
  |╱
  ∧_∧       ╱
  ( ・∀・)  ∧ ∧ < 你你是谁? ...有..有什么事嬷你?...
  (  ⊃ )  (゚Д゚;)  ╲____________
________(つ_つ____
|    日∇ ╲|ThinkPad|╲
|       =========  ╲

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
__________

| 利用实例来创建实例..恩
╲ __ ________
  |╱
  ∧_∧       ╱
  ( ・∀・)  ∧ ∧ < 恩..可以那样说...类变成配角.
 (     )  (;゚Д゚)  ╲____________
_____ (つ_つ____
|     日∇╲|ThinkPad|╲
|       =========  ╲

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
__________

| 把类的设计失败, 在实例去修正.恩.
╲ __ ________
  |╱
 ∧_∧       ╱
 ( ・∀・)  ∧ ∧ < 不..不是这样啦..
 (  ⊃ )  (゚Д゚;) ╲____________
_____(つ_つ____
|   日∇ ╲|ThinkPad|╲
|      =========  ╲
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

giko猫.clone()

    ∧ ∧        ┌────────────────
   ( ゚Д゚ )       < giko.clone()
    U  U        └────────────────
     |  |
    U U

    ∧ ∧ ∧      ┌────────────────
   ( ゚Д ゚Д゚ )     < 这这是、
    U  U  U      .└────────────────
     |    |
    U U U

    ∧ ∧∧∧     ┌────────────────
   ( ゚Д゚ ゚Д゚ )    < 到底到底是、
    U U.U  U     └────────────────
     |     |
    .U UU U

    ∧ ∧   ∧ ∧   ┌────────────────
   ( ゚Д゚ >< ゚Д゚ )  < 是 shallow copy 是 shallow copy 吗
    U  U  U  U   └────────────────
     |   ><  |
    .U U   U U

.    ∧ ∧   ∧ ∧  ┌────────────────
    ( ゚Д゚ ) * ( ゚Д゚ ) < 或是 deep copy。或是 deep copy。
    U  U   U  U  └────────────────
     |  .| .* |  |
     U U   .U U

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

--------------------------------------------------------


本帖的内容和四个示例,已过滤掉 Prototype Pattern 非必要的功能和角色,主要目的是让初学者能够快速入门。实务上 Prototype Pattern 还能有很多变化和进阶应用,比如:

  • 整合「工厂模式」,用一个工厂类来单独负责构造的工作 [8], [19]。
  • 通过「序列化 / 反序列化」实现深拷贝 [2], [13], [14], [20]。
  • 客户端程序多通过一个 Prototype manager 去创建 ConcretePrototype 对象,避免客户端程序与 ConcretePrototype 类产生依赖 [4], [5], [14], [15], [20], [21]。

有兴趣深入研究的网友,可参阅下方的「相关文章」和「相关书籍」。

--------------------------------------------------------

相关文章:

[1] .NET深入学习笔记(4) 深拷贝与浅拷贝(Deep Copy and Shallow Copy) - 老徐的博客 - 博客园
http://www.cnblogs.com/frank_xl/archive/2009/02/24/1396903.html

[2] C# Tips 浅拷贝和深拷贝 - SQL SERVER - 数据库-数据仓库 - ZDNetChina中文社区
http://bbs.zdnet.com.cn/thread-1426176-1-1.html

[3] 小议 .NET 中的对象拷贝 - TerryLee's Tech Space - 博客园
http://terrylee.cnblogs.com/archive/2006/01/06/312493.html

(推荐此帖)
[4] C#设计模式(9)-Prototype Pattern - First we try, then we trust - 博客园
http://www.cnblogs.com/zhenyulu/articles/39257.html

[5] Prototype Design Pattern in C# and VB.NET (英文)
http://www.dofactory.com/Patterns/PatternPrototype.aspx

[6] Proxy 模式,作者: caterpillar (繁体中文)
http://caterpillar.onlyfun.net/Gossip/DesignPattern/PrototypePattern.htm
http://www.javaworld.com.tw/jute/post/view?bid=44&id=25500&sty=1&tpg=3&age=-1

[7] 原型模式(ProtoType) - 最简单的 - JavaEye技术网站
http://iwtxokhtd.javaeye.com/blog/361086
http://www.codeweblog.com/prototype-model-prototype/

[8] 设计模式学习笔记五——Prototype模式 - 每天进步一点点,微笑面对全世界! - JavaEye技术网站
http://mybluesky99.javaeye.com/blog/384252

[9] Prototype Design Pattern :: BlackWasp Software Development (英文)
http://www.blackwasp.co.uk/Prototype.aspx

[10] 设计模式学习笔记(六)——Prototype原型模式 - KiddLee - 博客园
http://www.cnblogs.com/kid-li/archive/2006/05/18/403559.html

[11] Prototype pattern - Wikipedia, the free encyclopedia (英文)
http://en.wikipedia.org/wiki/Prototype_pattern

[12] DotNet Framework源代码中的模式(六)——Prototype(原型模式) - Guushuuse _NET - 博客园
http://www.cnblogs.com/guushuuse/archive/2009/05/15/1457951.html

[13] 无废话C#设计模式之五:Prototype - LoveCherry - 博客园
http://www.cnblogs.com/lovecherry/archive/2007/10/06/915535.html

[14] .NET设计模式(6):原型模式(Prototype Pattern) - TerryLee's Tech Space - 博客园
http://www.cnblogs.com/Terrylee/archive/2006/01/16/317896.html

[15] ASP_NET Wiki Architecture Design Patterns (英文)
http://wiki.asp.net/page.aspx/499/prototype-pattern/
http://wiki.asp.net/page.aspx/276/design-patterns/

[16] Implementing Audit / history tracking using Prototype Pattern (英文)
http://www.codeproject.com/KB/aspnet/AuditTracking.aspx

--------------------------------------------------------

相关书籍:

[17] 大话设计模式 (繁体中文翻译本), 程杰 着, 清华大学出版社, 第九章
http://www.cnblogs.com/cj723/archive/2009/03/12/1409359.html

[18] C# 3.0 Design Patterns, Judith Bishop, O'Reilly 出版社, 第五章
http://oreilly.com/catalog/9780596527730/
http://www.oreilly.com.cn/book.php?bn=978-7-111-25080-7

[19] 设计模式 - 基于 C# 的工程化实现及扩展, 王翔 着, 电子工业出版社, 第七章
http://www.china-pub.com/129880

[20] Design Patterns: Elements of Reusable Object-Oriented Software
http://www.amazon.com/exec/obidos/tg/detail/-/0201633612/ref=ase_portlandpatternrA/002-6454224-1040813?v=glance&s=books

[21] Design Patterns 于 Java 语言上的实习应用 (繁体中文翻译本), 结城浩 着, 博硕文化出版社

[22] Head First 设计模式, Freeman, O'Reilly 出版社 (本书似乎未谈到 Prototype Pattern)
http://oreilly.com.cn/book.php?bn=978-7-5083-5393-7

--------------------------------------------------------

posted on 2009-07-13 06:11  WizardWu  阅读(2413)  评论(9编辑  收藏  举报