23种设计模式——原型模型

文章目录

    • 1. 概念
    • 2. 特点
    • 3. 实现
      • 3.1 Java中的实现
      • 3.2 深拷贝

1. 概念

原型模式是一种创建型设计模式, 使你能够复制已有对象, 而又无需使代码依赖它们所属的类,即无需关心复杂的复制逻辑。
23种设计模式——原型模型_第1张图片

  • 目的:根据一个已有的对象,创建出另外一个一模一样的对象。

  • 解决的问题:复杂的复制逻辑
    如果你有一个对象, 并希望生成与其完全相同的一个复制品, 你该如何实现呢? 首先, 你必须新建一个属于相同类的对象。 然后, 你必须遍历原始对象的所有成员变量, 并将成员变量值复制到新对象中。

    不错! 但有个小问题。 并非所有对象都能通过这种方式进行复制, 因为有些对象可能拥有私有成员变量, 它们在对象本身以外是不可见的
    23种设计模式——原型模型_第2张图片
    直接复制还有另外一个问题。 因为你必须知道对象所属的类才能创建复制品, 所以代码必须依赖该类。 即使你可以接受额外的依赖性, 那还有另外一个问题: 有时你只知道对象所实现的接口, 而不知道其所属的具体类,比如接口有多个实现类,而你不知道要复制的对象使用的是哪个实现类。


2. 特点

  • 优点
    1. 可以克隆对象, 而无需与它们所属的具体类相耦合。
    2. 可以克隆预生成原型, 避免反复运行初始化代码。
    3. 可以更方便地生成复杂对象(比如有多个属性),使用原型模式就不必一一获取再设置。
    4. 可以用继承以外的方式来处理复杂对象的不同配置。
  • 缺点:克隆包含循环引用的复杂对象可能会非常麻烦(深拷贝)
  • 适用场景
    1. 如果你需要复制一些对象,但这个对象的构建比较复杂,使用原型模式就可以无视这种复杂的逻辑直接复制。

    2. 如果子类的区别仅在于其对象的初始化方式, 那么你可以使用该模式来减少子类的数量。 别人创建这些子类的目的可能是为了创建特定类型的对象。

      在原型模式中, 你可以使用一系列预生成的、 各种类型的对象作为原型。客户端不必根据需求对子类进行实例化, 只需找到合适的原型并对其进行克隆即可。类似模具的作用~


3. 实现

在讲原型模式之前,先模拟一下普通做法是怎么复制一个对象的,在这种做法下,有两个角色:

  • 原型类:被复制的对象
  • 客户端:调用复制方法的一端,复制操作也由客户端实现
/**
 * 客户端
 */
class client{
     
    /**
     * 复制方法
     * @param test 原型对象
     */
    void copy(Test test){
     
        //new一个对象
        Test newTest = new Test();
        //根据原型对象注入属性
        newTest.setValue(test.getValue());
    }
    
	public static void main(String[] args) throws CloneNotSupportedException {
     
	        Test test = new Test();
	        test.setValue(10);
	        
	        Test clone = copy(test);
	        System.out.println(clone.getValue());
	    }
    
}

/**
 * 原型对象
 */
class Test{
     
    /**
     * 原型对象的属性
     */
    private int value;
    
    public int getValue() {
     
        return value;
    }

    public void setValue(int value) {
     
        this.value = value;
    }
}

以上便是普通的复制一个对象方法的实现,例子里面的复制还比较简单,假如原型对象有几十个对象,此时代码就会很繁杂,又假如涉及到一些依赖的接口,此时代码会更复杂,所以下面就需要原型模式出马了。

原型模式将克隆过程委派给被克隆的实际对象。 模式为所有支持克隆的对象声明了一个通用接口, 该接口让你能够克隆对象, 同时又无需将代码和对象所属类耦合。 通常情况下, 这样的接口中仅包含一个 克隆方法。

总结一下,就是原型模式中变成了三个角色

  • 原型类:被复制的对象,要实现复制接口,实现具体的复制操作,原来由客户端实现的复制操作转移到了这里
  • 复制接口:里面一般只有一个复制方法
  • 客户端:调用复制方法的一方,但具体的复制操作不由它实现

所有的类对克隆方法的实现都非常相似。该方法会创建一个当前类的对象, 然后将原始对象所有的成员变量值复制到新建的类中。 你甚至可以复制私有成员变量, 因为绝大部分编程语言都允许对象访问其同类对象的私有成员变量。

支持克隆的对象即为原型。 当你的对象有几十个成员变量和几百种类型时, 对其进行克隆甚至可以代替子类的构造。
23种设计模式——原型模型_第3张图片
其运作方式如下: 创建一系列不同类型的对象并不同的方式对其进行配置。 如果所需对象与预先配置的对象相同, 那么你只需克隆原型即可, 无需新建一个对象。这里讲的对应了第二种使用场景。

3.1 Java中的实现

Java本身就是有对于原型模式的支持,我们都知道每一个类都是Object的子类,而Object又有一个方法叫做clone(),这个就是克隆方法,所以我们创建的每一个类都可以作为原型类。

而Java又给我们提供了一个接口叫做Cloneable,这个就对应了原型模式中的复制接口。这个接口比较特殊,里面其实是没有任何方法的,实现了它其实就是相当于打开了一个开关,这个开关意味着这个类可以被克隆,和Serializable是一样的。

所以我们的做法就是,原型类实现Cloneable接口,重写clone()方法,然后直接调用这个类的clone()方法就可以完成复制。具体代码如下:

/**
 * 客户端
 */
class client {
     
    public static void main(String[] args) throws CloneNotSupportedException {
     
        Test test = new Test();
        test.setValue(10);
        // 直接使用cloe方法获得一个克隆对象
        Test clone = test.clone();
        System.out.println(clone.getValue());
    }
}

/**
 * 原型对象
 */
class Test implements Cloneable{
     
    /**
     * 原型对象的属性
     */
    private int value;

    public int getValue() {
     
        return value;
    }

    public void setValue(int value) {
     
        this.value = value;
    }

    /**
     * 重写clone方法
     * @return 返回克隆的对象
     * @throws CloneNotSupportedException 没有实现克隆接口时就会报这个错误
     */
    @Override
    protected Test clone() throws CloneNotSupportedException {
     
        return (Test)super.clone();
    }
}

3.2 深拷贝

上面的代码就展示了在Java中如何实现原型模式,但其实这样还是存在问题的,有什么问题呢?比如Test这个原型类里面的属性是存在一个对象的,如图:
23种设计模式——原型模型_第4张图片
这个时候按照上面那种方式进行拷贝的话,就会产生一个问题:即所有克隆对象的test2属性其实都是同一个。如下图:
23种设计模式——原型模型_第5张图片
也就是说所有对象都共用了同一个test2,假如不小心修改了数据,则所有对象都会受影响。这时候该怎么办呢?我们只需要在重写clone方法时,也实现对对象属性的克隆。代码如下:

/**
 * 原型对象
 */
class Test implements Cloneable {
     
    /**
     * 原型对象的普通属性
     */
    private int value;
    /**
     * 原型对象的引用类型属性
     */
    private Test2 test2;

    public int getValue() {
     
        return value;
    }

    public void setValue(int value) {
     
        this.value = value;
    }

    public Test2 getTest2() {
     
        return test2;
    }

    public void setTest2(Test2 test2) {
     
        this.test2 = test2;
    }

    /**
     * 重写clone方法
     * @return 返回克隆的对象
     * @throws CloneNotSupportedException 没有实现克隆接口时就会报这个错误
     */
    @Override
    protected Test clone() throws CloneNotSupportedException {
     
        Test test = (Test)super.clone();
        test.setTest2(test.getTest2().clone());
        return test;
    }
}

class Test2 implements Cloneable{
     
    private int value;

    @Override
    protected Test2 clone() throws CloneNotSupportedException {
     
        return (Test2)super.clone();
    }
}

此时克隆出来的对象就不会共用同一个test2了,这种操作也叫做深拷贝,相应的,直接重写的那种方式就叫做浅拷贝。

其实还有一种实现方式,因为上面的实现方式其实就是一种嵌套,把Test2clone()也进行了重写,但假如Test2里也有引用类型的属性呢,这时又继续嵌套,这样代码会很丑。

所以另外一种方式是采用流的形式,直接对这个对象进行了复制,这种方式就无需考虑引用类型的属性里面是否还有依然存在引用类型的属性,推荐使用,代码如下:

public Object deepClone() {
     
	//创建流对象ByteArrayOutputStream bos = null; 
	ObjectOutputStream oos = null; 
	ByteArrayInputStream bis = null; 
	ObjectInputStream ois = null;
	
	try {
     
		//序列化
		bos = new ByteArrayOutputStream(); 
		oos = new 	ObjectOutputStream(bos);
		oos.writeObject(this); //当前这个对象以对象流的方式输出
		//反序列化
		bis = new ByteArrayInputStream(bos.toByteArray()); 
		ois = new ObjectInputStream(bis);
		DeepProtoType copyObj = (DeepProtoType)ois.readObject();
		return copyObj;
	} catch (Exception e) {
     
		return null;
	} finally {
     
		//关闭流 try {
     
		bos.close();
		oos.close();
		bis.close();
		ois.close();
		} catch (Exception e2) {
     
			System.out.println(e2.getMessage());
		  }
		}	
	}
}

你可能感兴趣的:(设计模式)