首页 > 代码库 > 设计模式学习05—原型模式

设计模式学习05—原型模式

一、动机与定义

     之前学习原型模式一直以为原型模式目的是为了方便的创建相同或相似对象,用复制对象的方式替换new的方式,还研究了深克隆和浅克隆。最近仔细看了GOF的设计模式,发现原型模式的本意并不仅仅是复制对象这么简单。
     复制对象确实是一方面,当我们需要大量相似,甚至相同对象的时候,除了一个个的new之外,还可以根据一个原型,直接复制出更多的对象。但是如果把原型模式认为只是复制对象这么简单就错了。
     创建型模式主要讲如何创建对象,通常包含何时创建,谁来创建,怎么创建等。GOF书里面写的意图是,用原型实例指定创建对象的种类,并且通过拷贝这些原型对象创建新的对象。也就是说原型模式应该理解成先指定好要创建的对象种类,也就是指定对象类型,再通过拷贝方式创建对象。
     此时再看原型模式定义就更清晰了:用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象。
     原型模式的目的是创建对象,通过复制的方式来创建对象,复制只是一种方式、一种方法、或者说一种手段,而不是目的,重点在创建对象上,而不是复制上,如果把重点放到复制对象上,那就变成了研究深克隆,浅克隆这些东西,他们仅仅是创建对象的一种方法而已,没有这些克隆,我们自己实现克隆也完全可以。

二、结构与类图

     原型模式类图如下:
     
     Prototype:声明一个复制自身的接口,可以是接口或类,其实复制自身不一定非要所有状态必须一样,后面会有提到。     
     ConcretePrototype:被复制的类,实现了复制自身的操作。
     Client:让一个原型复制自身,从而创建一个新对象。
     但是在Java中提供了一个本地clone方法来高效克隆对象,为了让所有类能够使用通用的clone方法而且不受单一继承的影响,就把clone放到到了Object中,如果想使用,只需要实现Cloneable这个标记接口就行。这个只能说Java提供了非常便利的方法让我们直接使用原型模式(相似的还有很多,比如观察者java.util.Observer等)。而Java默认提供的这个clone只会克隆基本类型和String,也就是通常所说的浅克隆,要是所有对象都克隆,就要自己实现深克隆了。

三、适用场景及效果(优缺点)

     先说我之前的理解,之前一直觉得原型模式有两个重点,一是通过复制代替new了,所以适用场景通常是new一个对象比较困难、代价大,复制方便简单时使用;二是复制对象能保存对象状态,所以需要保留对象状态时使用。通常有下面几个适用场景:
     1、创建对象成本较大时,直接复制一个对象就非常简单。这个成本指很多方面,如时间花费较长、资源占用较多、初始化过程繁琐等等。
     2、系统需要对象状态时,比如某个对象有10个状态(或者说属性),我需要一个和这个对象9个状态相同,只有1个状态不同时,原型模式非常方便(有些情况重新创建可能甚至无法获取从前的状态了),举个例子,我要创建一个资源访问配置,配置好了资源位置、读取方式、缓存大小、访问权限,访问密码等等,当访问另一个资源时,只有位置不同,其他的完全相同,此时就可以直接复制一个对象,然后修改一下资源位置状态即可。
     3、一个对象对应多个修改者的情况,当一个对象供多个修改者使用时,比如多线程修改一个对象时,可以考虑使用原型模式创建多个相同对象供调用者使用,从而避免资源竞争,加锁等(这个要看你的需求,如果目的是共享资源访问,就不能这样了)。

     其实这些也没问题,但是和GOF说的还有一定差距,GOF说了4种适用场景:
     1、当一个系统应该独立于它的产品创建、构成和表示时,要使用Prototype模式。这句话范围很广泛,其实依赖倒置原则下的一个解决办法,调用者不能依赖具体,只能依赖抽象,但是调用者不知道具体产品是哪个时怎么办,工厂模式是直接向工厂要一个对象,建造者是向导演类要一个对象,单例是向单例类要一个对象,而原型模式是从一个已有的对象上要对象,要一个和你一模一样的对象。
     2、当要实例化的类是在运行时刻指定时,例如,通过动态装载。这个指需要动态创建对象类型,如开发游戏中的地面类型,有草地、雪地、水泥地等等,程序先创建好各种土地的原型,运行时根据参数来判断具体要创建哪种类型土地,找到类型对应的原型后,复制即可。
     3、为了避免创建一个与产品类层次平行的工厂类层次。这个就是有时候可以代替抽象工厂模式,不是不需要工厂和工厂方法了,而是把工厂方法放到自己的clone方法中,把自身当成工厂了,这样就能减少工厂类。虽然clone方法名字是复制自身,其实不一定就非得复制一个状态完全一模一样的对象,有时候只要对象类型一样即可,也不一定就要复制一个新的对象,有时候把自己return出去也是可以的(和单例结合使用)。
     4、当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。这个是解决“只有固定的几类,每类对象可能有多个时”创建对象的情况,比如中国象棋中的棋子,只有固定7类(车马象士将炮兵),不会新增其他类的,每类有多个对象(兵有5个,炮有2个等),我们可以先把每种棋子都初始化好(7类棋子),用到的时候复制一个自己就行了。
//棋子
public class Piece {

    // 类型
    private int type;
    // 名字
    private String name;
    // 状态
    private String state;
    // 颜色
    private String color;

    // 其他属性......
}
     当然,如果种类固定,数量也固定,那可以考虑使用泛型了。
     在说说使用后的效果,也就是优点:
     1、和工厂、建造者模式一样,封装底层实现,依赖抽象产品,隐藏了具体的产品,减少代码复杂度(类少了)等等,这里就不详细说了。
     2、运行时刻增加和删除产品,这个指用户使用不同产品时,无需创建新的产品类,可以使用一个属性状态等来控制对象类型,如上面例子中的棋子,就没必要为每种棋子创建一个类,只需要一个type就能控制是哪类棋子,调用者使用时,可以自行动态创建和减少产品类。也减少了类数量,简化了代码。不过这个要看实际情况,对象种类都特别相似的情况下才可以。
     3、改变值以指定新的对象,和上面类似,只需要修改产品属性值就能创建一个新的产品,非常方便。
     4、改变结构以指定新的对象,这个就是直接一个深克隆创建一个完全新的产品,直接使用,非常方便。比如做汽车轮胎,需要4个,创建1个轮胎成本较大,但是创建1个后,复制4个就很简单了。
     5、减少子类的构造,前面也说了,相比工厂等方法,减少了工厂类,clone方法直接放到了产品里面,新增一个子类,不用增加工厂等,简化了代码。
     6、用类动态配置应用,我们可以将一些配置都放到一个对象中,使用时对外提供这个配置对象的副本即可,这样动态修改配置对象后,不影响旧的对象使用,而新的使用到配置的地方可以用新的配置。
     缺点:
     1、原型模式缺点也是显而易见的,所有子类都必须实现clone方法,新设计的程序还好,原始程序改造时就麻烦了,逐个给所有类加一个clone方法太难了。
     2、无法克隆问题,克隆对象的方式看起来很好,但是有个问题,有些对象不能克隆怎么办,有些对象克隆起来太复杂怎么办,比如java中的属性使用了final关键字,不能2次赋值,就没办法实现深度克隆。还有一种情况,内部属性之间是相互引用,循环引用的,此时要实现克隆就非常困难。
     3、深度克隆需要复杂的代码,这个没啥好说的。

四、模式扩展

     调用者直接复制原型有个缺点,调用者的角色不明确,他需要直到要用哪个原型,原型对象和复制出来的产品之间也可能无法区分(大部分时候不用区分),所以有一种方式引入了原型管理器来管理原型,维护已有原型的清单,调用者使用时会向原型管理器发出请求,获取原型,这样调用者就不需要知道具体原型了,原型和复制出来的产品也可以区分了。
     这个是相比其他创建型模式来说的,没有工厂,没有单例,没有导演类,原型模式只需要一个产品原型,就能复制出其他产品,所以非常灵活,调用者可以在运行时动态增加和删除产品,而不用先写一堆工厂,单例,建造者这样的类。