【设计模式】原型模式(Prototype Pattern)

在这里插入图片描述

 

🔥 核心

直接复制已有对象,而不依赖它们所属的类。

对象本身自己复制自己。

 

🙁 问题场景

有一天你在森林中散步时,突然发现不远处泛着蓝光。你凑近过去,发现这个是一架刚刚坠落的UFO。

UFO上已经没有了外星人,看来是已经逃生走了;这架UFO除了刮蹭,也没有任何的损坏——这令你欣喜若狂。如果你可以「复制」出一架UFO,你将成为本世纪最天才的发明家!

你搜遍了整个驾驶舱,终于发现了一张UFO构造图纸。通过这张图纸,你知道了这是一个 UFO 类,以及它的组成部件(成员变量)以及实现功能(成员方法)。思考一下,有了这些信息,你可以复制出一架UFO吗?

很可惜,大概率不能。

这是一个典型的复杂对象的复制问题。你拿到了这张图纸,就是得知了它所属的 以及 类中的信息 。你想根据这张图纸复制一个UFO,就相当于先新建一个相同类的对象,然后遍历原始对象的所有成员变量,并将成员变量值复制到新对象中。

你会遇到两个致命的问题:

一是原始对象可能拥有私有成员变量,它们在对象之外是不可见的。这就像外星人隐藏了一些关键的技术。

二是你所知道的对象所属的类,有可能只是一个接口或者父类,这样的话成员变量甚至成员方法都是不完整的。

怎么办?这架 UFO 真的没法复制类吗?

 

🙂 解决方案

原型模式的关键就在于,复制不是从类从头开始的,而是直接将复制动作委派给原始对象本身。

也就是说,可以被复制的类,都会暴露出一个「克隆方法」,供外部复制它时使用。因为它是对象本身自己复制自己的,所以复制过程并不会缺少任何信息。

事实上,我们在复制对象时,一直使用的都是原型模式——Java的类实现Cloneable接口并实现clone()方法,不正是原型模式的应用吗?即在一个类的内部进行拷贝的动作,并将接口暴露给外部。

再回到你的奇遇——森林里的UFO。

你有办法复制这个 UFO 对象吗?除了暴力新建类并遍历赋值,其实没有更好的法子了。

所以,你只能祈求这架 UFO 自身可以复制自身了。可是,外星人在建造这个UFO时,真的会对外提供原型复制的方法吗?

 

🌈 有趣的例子

例子太多太常见了,alice.clone() 就是原型模式的一个简单且经典的例子。下面,介绍一个原型模式的一个稍复杂的典型应用——原型注册表。

故事发生在100年后,由于生态的自然衰败,地球上的动物几近灭绝。人类建造了一个动物原型库,将每个种类的动物仅仅留下一只个体保存在营养液中,当人们需要这种动物时,就复制出它的一个个体。

下面用 动物抽象类(Animal) 以及 Koala(考拉)Panda(熊猫)Shark(鲨鱼) 举个例子。每种动物的一个个体被放在动物原型库,即 原型注册表(AnimalCache) 中。

 动物抽象类(这个父类中拥有clone方法)
abstract class Animal implements Cloneable {
    
    public String type;

    public String getType() {
        return type;
    }

    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

 考拉
class Koala extends Animal { public Koala() { type = "Koala"; }}
 熊猫
class Panda extends Animal { public Panda() { type = "Panda"; }}
 鲨鱼
class Shark extends Animal { public Shark() { type = "Shark"; }}


 原型注册表(核心逻辑)
class AnimalCache {

    private static Map<String, Animal> animalMap = new HashMap<>();

    public static Animal getAnimal(String typeId) throws CloneNotSupportedException {
        Animal cacheAnimal = animalMap.get(typeId);
        return (Animal) cacheAnimal.clone();
    }

    public static void initCache() {
        animalMap.put("1", new Koala());
        animalMap.put("2", new Panda());
        animalMap.put("3", new Shark());
    }
}
public class PrototypePatternDemo {
    public static void main(String[] args) throws CloneNotSupportedException {

        // 初始化原型注册表
        AnimalCache.initCache();

        // 拷贝出一只考拉
        Animal koala = AnimalCache.getAnimal("1");
        System.out.println(koala.getType());

        // 拷贝出一只熊猫
        Animal panda = AnimalCache.getAnimal("2");
        System.out.println(panda.getType());

        // 拷贝出一只鲨鱼
        Animal shark = AnimalCache.getAnimal("3");
        System.out.println(shark.getType());
    }
}
Koala
Panda
Shark

 

☘️ 使用场景

◾️如果你需要复制一些对象,同时又希望代码独立于这些对象所属的具体类,可以使用原型模式。

这一点考量通常出现在代码需要处理第三方代码通过接口传递过来的对象时。即使不考虑代码耦合的情况,你的代码也不能依赖这些对象所属的具体类,因为你不知道它们的具体信息。

◾️原型模式为客户端代码提供一个通用接口,客户端代码可通过这一接口与所有实现了克隆的对象进行交互,它也使得客户端代码与其所克隆的对象具体类独立开来。

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

◾️在原型模式中,你可以使用一系列预生成的、各种类型的对象作为原型。

客户端不必根据需求对子类进行实例化,只需找到合适的原型并对其进行克隆即可。

 

🧊 实现方式

(1)创建原型接口, 并在其中声明 克隆 方法。如果你已有类层次结构,则只需在其所有类中添加该方法即可。

(2)原型类必须另行定义一个以该类对象为参数的构造函数。构造函数必须复制参数对象中的所有成员变量值到新建实体中。如果你需要修改子类,则必须调用父类构造函数,让父类复制其私有成员变量值。

(如果编程语言不支持方法重载,那么你可能需要定义一个特殊方法来复制对象数据。 构造函数中进行此类处理比较方便, 因为它在调用 new 运算符后会马上返回结果对象。)

(3)克隆方法通常只有一行代码:使用 new 运算符调用原型版本的构造函数。注意,每个类都必须显式重写克隆方法并使用自身类名调用 new 运算符。否则,克隆方法可能会生成父类的对象。

(4)你还可以创建一个中心化原型注册表,用于存储常用原型。

(你可以新建一个工厂类来实现注册表,或者在原型基类中添加一个获取原型的静态方法。该方法必须能够根据客户端代码设定的条件进行搜索。搜索条件可以是简单的字符串,或者是一组复杂的搜索参数。找到合适的原型后,注册表应对原型进行克隆,并将复制生成的对象返回给客户端。最后还要将对子类构造函数的直接调用替换为对原型注册表工厂方法的调用。)

 

🎲 优缺点

  ➕ 你可以克隆对象,而无需与它们所属的具体类相耦合。

  ➕ 你可以克隆预生成原型,避免反复运行初始化代码。

  ➕ 你可以更方便地生成复杂对象。

  ➕ 你可以用继承以外的方式来处理复杂对象的不同配置。

  ➖ 必须要实现Cloneable接口。

  ➖ 克隆包含循环引用的复杂对象可能会非常麻烦。

 

🌸 补充

 Java中的 clone() 方法是浅拷贝还是深拷贝?浅拷贝。

 与JavaScript不同,Java在克隆时很难实现递归克隆,所以深拷贝很困难。但是,你可以通过序列化/反序列化间接实现深拷贝。

 

🔗 参考网站

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值