原型模式——创建型模式(4)

前言

在前面介绍的三种创建型模式中,有一个共通的特点,就是不管是直接创建还是分步骤地组装创建都是通过第三方对象来完成,比如工厂、生成器。但是在创建型模式中还有一个比较特殊的模式,其不用通过第三方对象来完成对对象的创建,而是通过克隆自己来创建新对象,完成对新的对象的创建工作。这样客户端便可以直接根据原型对象来动态获取新的对象,而无需借助其他的辅助类来生成新的对象呢。类似这样的一种创建型模式,名曰——原型模式!

动机

在实际的软件系统中,由于需求的变化,导致产品类总是动态变化的,而且系统中的产品类都具有一定的等级结构。如果通过工厂方法模式来隔离产品的变化过程,那么与产品类等级结构平行的工厂方法类也会随着产品种类的改变而改变,当产品种类变化较大时,导致工厂方法类变化过大时,这时利用工厂方法模式就显得不是那么合适呢。如何通过隔离封装这种变化?从而使依赖于产品对象的客户端不需要因为产品类的改变而变化呢?接下来,我们就详细讲述原型模式是如何应对这种变化之道。

意图

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

结构图

image

  1. 抽象原型(Prototype)角色:声明一个克隆自身的接口,在java中通过使用Cloneable接口替代,在.net通过ICloneable接口替代。
  2. 具体原型(ConcretePrototype)角色:实现一个克隆自身的操作,也就是实现了Prototype接口。
  3. 客户(Client)角色:让一个原型对象克隆自身从而创建一个新的对象。

代码示例

 1:  public abstract class Prototype{
 2:      public abstract Prototype Clone();
 3:  }
 4:   
 5:  public class ConcretePrototype1 extends Prototype{
 6:      public String name;
 7:      public ConcretePrototype1(String name){
 8:          this.name=name;
 9:      }
10:      public String getName() {
11:          return name;
12:      }
13:      public void setName(String name) {
14:          this.name = name;
15:      }
16:      public ConcretePrototype1 Clone(){
17:          return (ConcretePrototype1) this.clone();
18:      }
19:  }
20:   
21:  public class ConcretePrototype2 extends Prototype{
22:      public String name;
23:      public ConcretePrototype2(String name){
24:          this.name=name;
25:      }
26:      public ConcretePrototype2 clone(){
27:          return this.clone();
28:      }
29:  }
30:  public class Client{
31:      public static void main(String[] args){
32:          ConcretePrototype1 cp1=new ConcretePrototype1("cp1");
33:          ConcretePrototype1 cp1_copy=cp1.Clone();
34:          System.out.println(cp1_copy.getName());
35:   
36:          ConcretePrototype1 cp2=new ConcretePrototype1("cp2");
37:          ConcretePrototype1 cp2_copy=cp1.Clone();
38:          System.out.println(cp2_copy.getName());
39:  }

上述示例代码只是示意性的代码,在IDE中并不能编译通过,在实际的实现过程中,无须自定义抽象原型接口,直接使用java语言中的Cloneable接口或者.net语言中的ICloneable接口即可,但是它却能够比较清楚地说明原型模式的实现和基本使用方法,这就足够呢。由于具体原型对象都实现了抽象原型接口,都可以独立地根据自身对象类型克隆自己,产生新的对象,这也正是对原型模式本质的最好诠释。

现实场景

其实,在我们日常的生活世界里,有很多类似的例子可以抽象归纳为原型模式,比如细胞分裂活动:通过对自身的分裂,产生两个完全一样的子细胞。这个过程与原型模式有异曲同工之妙。还有就是《西游记》里的孙大圣,拨下几根毫毛,叫声“变”,瞬间就将其变化成与自己外表内含一模一样的孙大圣,虽然是神话,但是透过表象,深入本质,抽象过程,同样与原型模式本质相吻合——通过自身来动态克隆创建新的“自己”,应对新对象的创建需求。

在示例代码过程中,在客户端我们只是通过简单直接引用具体原型对象,根据其来创建新的对象,这并非是最佳的编程实践。真实的使用环境下,我们完全可以创建一个原型注册器,这样就可以避免每次都需要创建新的原型实例来克隆自己来产生新的实例对象呢,节省内存,提高效率。而所谓的原型注册器,其实很简单,通过hashMap就可以实现一个简单的原型注册器,key-value的方式来注册和获取原型对象。简述其原理就是通过key来关联相应的原型对象,如果通过key没有引用到对应的原型对象,就注册一个新的原型对象,否则直接返回已经注册过的原型对象,然后再根据该原型对象来克隆创建新的对象即可。

说完对原型模式的“最佳实践”方法,接下来,我们来谈谈java(.net语言类似)中“浅拷贝”与“深拷贝”,之所以要提及它们两个,是因为原型模式中真正实现克隆功能的接口实现中经常需要根据实际情况来选择对拷贝问题的“深浅”问题。所谓的“浅拷贝”,就是利用java对象的clone()方法,将对象中所有的属性进行拷贝,这里拷贝主要是分两种类型,一个是基本数据类型属性的值拷贝,另一个就是引用数据类型的引用拷贝,值拷贝的结果将会产生一个新值,与原值没有任何关联,而引用拷贝的结果是只拷贝了对对象的引用,但是对象中值并未进行拷贝,这样造成的结果就是通过新引用来操作对象与旧引用操作的对象是同样一个对象,因为两者都关联着同一个对象,自然改变的就是同一对象中属性值呢。当然这里,我们完全可以将引用拷贝也看作成一种“值拷贝”,只是拷贝的不是对象的值,而是对对象的引用的拷贝。理解了上面的所谓的“值拷贝”问题,那么对“深拷贝”问题也就不难理解呢。“深拷贝”主要用于解决对引用数据类型的拷贝问题,针对引用数据类型,我们必须通过拷贝其”值“,而不是拷贝对其的引用,换句话来说就是通过创建同样类型的新的对象以及新的属性值(主要指的是引用属性),对象中的所有属性值与原对象中的属性值没有任何关联引用,这样就完成了真正意义上的”深拷贝“。下面是对”深拷贝“的简单java代码诠释:

 1:  class User implements Cloneable {
 2:      String name;
 3:      int age;
 4:      public User(String name,int age){
 5:          this.name=name;
 6:          this.age=age;
 7:      }
 8:      @Override
 9:      public User clone() throws CloneNotSupportedException {
10:              return (User) super.clone();
11:      }
12:  }
13:   
14:  class Account implements Cloneable {
15:      User user;
16:      long balance;
17:      public User getUser(){
18:          return user;
19:      }
20:      public Account(User user,long balance){
21:          this.user=user;
22:          this.balance=balance;
23:      }
24:      @Override
25:      public Account clone() throws CloneNotSupportedException {
26:              Account account = null;
27:              account = (Account) super.clone();
28:              if (user != null) {
29:                      account.user = user.clone();
30:              }
31:              return account;
32:      }
33:  }
34:   
35:  public class Client{
36:      public static void main(String[] args){
37:          Account account=new Account(new User("jackybing", 24), 10000);
38:          Account account2=account.clone();
39:          System.out.println(account.getUser()==account2.getUser());
40:      }
41:  }

 上述代码片的39行处说明了深拷贝的作用,此时,两个account对象中的user对象已经不是同一个对象呢。因为在代码29行处,同样是通过深拷贝的方式来获得user的”克隆体“。注意,User类也实现了cloneable接口,但是实现上却只是使用了”浅拷贝“的方式来完成复制操作,大家需要明确的是,虽然user类的name属性为String类型,但是其是不可改变的对象类型,jvm会将其与值对象一样处理,生成新的值对象。因此,我们可以通过”浅拷贝“的方式来获得User的”克隆体“,这也是为什么代码行39处输出打印的结果为true的原因呢。在这里,就深拷贝多说几句吧。其实深拷贝可以是很复杂的,因为有可能涉及递归式地深拷贝,也就是对象类中的属性本身也是实现了深拷贝操作,这样在克隆最外层对象类里,我们需要递归地将所有引用的对象都进行深拷贝操作,如果递归的过程中,有一个对象类型的属性不支持深拷贝,那么结果将是不可预测的,因为这时就会出现一个对象被多个引用句柄所引用呢,自然对该对象的改变也就不可控呢。所以,在软件系统设计过程中,我们一定要提前规划设计好需要支持深拷贝的对象类,否则中途再修改就比较麻烦呢。如果大家想对java中cloneable接口的使用有一个比较深刻的认识,建议可以参考这篇博文,里面有对深、浅拷贝较为深刻的解读分析,这里就不过多扩展呢。

实现要点

  1. 使用原型注册管理器:当一个系统中原型数目不固定时,可以动态地创建和销毁,这样用户就可以通过它在运行时刻动态获取相应的原型对象用于克隆的目的。
  2. 实现克隆操作:Prototype模式最困难的地方在于正确地实现clone操作,在不同的语言层面有不同的实现技巧,当对象结构中存在循环引用时,情况就变得更加棘手和复杂。如果只是浅拷贝很容易实现,但如果是深拷贝情况就比较复杂化呢。像java语言,我们可以参考上述示例代码来实现简单地深拷贝操作。通过来说浅拷贝操作就足够呢,但是如果克隆一个结构复杂的原型通过需要深拷贝,因为复制对象和原对象必须相互独立,同时还必须保证克隆对象也是原型构件的克隆。
  3. 初始化克隆对象:有时,通过原型对象克隆出来的新对象需要根据实际需要改变克隆对象的一些内部状态,这个时候我们需要在原型对象类中存在相应的设置对象状态的方法。这样一些辅助方法需要在设计之时加以考虑和衡量。

运用效果

  1. 运行时刻增加和删除产品:客户可以通过原型管理(注册)器将一个新的具体产品并入系统中,这比其他创建型模式更加灵活,因为客户可以在运行时刻建立和删除原型对象。
  2. 改变值以指定新对象:对象原型管理器我们可以很方便地获取对象的原型克隆对象,然后通过对应的状态设置方法,改变克隆对象的一些内部状态信息,这样就可以产生新类别(或者说新状态)的对象呢。
  3. 改变结构以指定新的对象:许多应用由部件和子部件来创建对象,通过将子部件作为一个原型增加到可用的原型管理器中,这样由多个子部件(可以进行克隆的子部件)组合生成的复杂部件就可以通过深拷贝,来实现具有不同结构的复杂部件的原型呢。
  4. 减小子类构造:原型模式可以通过克隆一个原型而不是请求一个工厂方法去产生一个新的对旬,根本不需要抽象Creator类层次,但是工厂方法需要产生一个与产品类层次平等的Creator类层次。
  5. 动态配置应用:由于原型模式独立性比较高,可以很容易动态加载新功能到系统中,而不影响原来系统的功能。
  6. 产品类不需要事先确定一个等级结构,因为原型模式可以适用于任何的等级结构。
  7. 原型模式的主要缺陷是每一个prototype子类都必须实现Clone操作,有时在现实系统中这很难实现,尤其是所考虑的类已经存在实现时不难以新增加Clone操作呢,还有就是内部包括一些不支持拷贝或者有循环引用的对象时,实现克隆操作也很困难。

适用性

当一个系统应该独立于它的产品创建、构成和表示时,可考虑使用原型模式,还有就是:

  1. 当要实例化的类是在运行时刻指定时,例如,通过动态装载时,或者
  2. 为了避免创建一个与产品类层次平等的工厂类层次时;或者
  3. 当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们比每次用合适的状态手工实例化该类更方便些。

相关模式

  1. 原型模式与抽象工厂模式:由于原型模式关注的是如何创造出对象实例,方案是通过克隆的方式,而抽象工厂模式关注的是如何创造出产品族,正因为关注点不一样,所以可将两结合起来使用,具体就是通过原型来创建抽象工厂方法所有需要创建的产品族,抽象工厂只需要管理定义好产品族接口即可。
  2. 原型模式和生成器模式:生成器模式关注的是创建的过程,而在构建的过程中,各个子部件的创建工作可能通过原型对象克隆得到。

总结

原型模式的本质是:克隆生成对象。原型模式的核心是创建新的对象实例,至于创建出的对象,其属性是否与原型对象属性一致,这并没有强制规定,但是克隆体必须与原型对象的类型是一样的。其实,程序语言发展到现在,很多已经将原型模式集成到相应的语言实现层面呢,根本不需要我们手工来创建相应的原型接口,相应语言已经为我们提前定义好了相应接口,我们所需要做的就是实现该接口即可,比如java语言中的Cloneable接口。之所以我们学习原型模式,只是为了了解原型模式的本质、来源和适应场景,正所谓”知其然更要知其所以然“嘛!唯有这样, 我们才能透彻理解要将原型模式加入语言层面的源由和目的呢。对原型模式的讲解就讲这么多吧,个人认为原型模式还是比较好理解的,下篇我们将介绍单例模式,敬请期待!

 

posted @ 2012-09-22 20:04  JackyBing  阅读(679)  评论(0编辑  收藏  举报