Aaron
  • 3.5k

设计模式之原型模式

熟悉JavaScript的同学对于原型的概念可能并不陌生,今天要说的原型模式和这个差不多是类似的,但是还是存在一些不同。原型模式的核心思想是,通过拷贝指定的原型实例(对象),创建跟该对象一样的新对象。简单理解就是克隆指定对象

所有的原型类都必须有一个通用的接口,使得即使在对象所属的具体类未知的情况下也能复制对象。 原型对象可以生成自身的完整副本, 因为相同类的对象可以相互访问对方的私有成员变量。

什么是原型模式

原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。 —— 节选自百度百科

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

原型模式用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或者相似的新对象。在这里,原型实例指定了要创建的对象的种类。用这种方式创建对象非常搞笑,根本无需知道对象创建的细节。比如我们作为程序员经常使用复制粘贴大概就是这个道理。

但是直接复制还有另外一个问题。因为你必须知道对象所属的类才能创建复制品,所以代码必须依赖该类。 即使你可以接受额外的依赖性,那还有另外一个问题:有时你只知道对象所实现的接口, 而不知道其所属的具体类, 比如可向方法的某个参数传入实现了某个接口的任何对象。

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

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

原型模式优缺点

原型模式是一种创建型设计模式,原型模式允许一个对象再创建另外一个可定制的对象,根本无需知道任何如何创建的细节,工作原理是:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它们自己来实施创建。

如果子类的区别仅在于其对象的初始化方式,那么你可以使用该模式来减少子类的数量。别人创建这些子类的目的可能是为了创建特定类型的对象。如果你需要复制一些对象,同时又希望代码独立于这些对象所属的具体类,可以使用原型模式。原型模式为客户端代码提供一个通用接口,客户端代码可通过这一接口与所有实现了克隆的对象进行交互,它也使得客户端代码与其所克隆的对象具体类独立开来。

优点
  1. 简化创建新对象的过程并提高效率
  2. 可动态获取对象运行时的状态
  3. 原始对象变化相应克隆对象也会发生变化
缺点
  1. 对已有类修改时,需要修改源码,违背了开闭原则
  2. 需要为每一个类都配置一个clone方法
  3. 当实现深拷贝时,需要编写较为复杂的代码,并且当对象之间存在多层嵌套的引用时,为了实现深拷贝,每一层对象对象的类都必须支持深拷贝,实现起来比较麻烦,增加了程序的复杂程度

示例

原型模式的主要角色如下:

  1. 原型:接口将克隆方法进行声明。在绝大多数情况下,其中只会有一个名为clone的方法
  2. 具体原型:类将实现克隆方法。除了将原始对象的数据复制到克隆体中之外,该方法有时还需处理克隆过程中的极端情况,例如克隆关联对象和梳理递归依赖等等
  3. 客户端:可以复制实现了原型接口的任何对象

类图如下所示:

image

代码示例:

interface Prototype {
    clone():Prototype;
}

class Dog implements Prototype {
    public name: string;
    public birthYear: number;
    public sex: string;
    public presentYear: number;
    constructor() {
        this.name = "lili";
        this.birthYear = 2015;
        this.sex = "男";
        this.presentYear = 2018;
    }

    public getDiscription(): string {
        return `狗狗叫${this.name},
                性别${this.sex},
                ${this.presentYear}年,
                ${this.presentYear-this.birthYear}岁了`;
    }

    // 实现复制
    public clone(): Prototype {
        return Object.create(this);
    }
}

// 使用
const dog = new Dog();
console.log(dog.getDiscription());
dog.presentYear = 2020;
const dog1 = Object.create(dog);
console.log(dog1.getDiscription());

总结

原型并不基于继承, 因此没有继承的缺点。另一方面,原型需要对被复制对象进行复杂的初始化。 工厂方法基于继承, 但是它不需要初始化步骤。有时候原型可以作为备忘录模式的一个简化版本, 其条件是你需要在历史记录中存储的对象的状态比较简单, 不需要链接其他外部资源, 或者链接可以方便地重建。

阅读 1.4k

推荐阅读