原型设计模式的目的是什么?

78

我正在学习设计模式。今天我了解到了“原型”设计模式。

可能是我漏看了什么,因为我没有看到它的好处。我在网上看到有人说它比使用 new 更快,但这不太合理;无论如何创建新对象,都需要分配内存。

这个模式难道不会遇到“先有鸡还是先有蛋”的问题吗?由于原型模式本质上只是克隆对象,在某个时候必须首先创建出原始对象(即非克隆对象)。这意味着我需要已经准备就绪要克隆的每个对象的现有副本吗?

有人能解释一下这个模式的用途吗?


5
这个问题更适合发布在程序员 StackExchange 上,因为它更多是关于编程概念的问题。 - WebChemist
在https://softwareengineering.stackexchange.com/questions/179489/whats-the-point-of-the-prototype-design-pattern/345315上也有转发。 - Doc Brown
8个回答

57

原型模式有一些好处,例如:

  • 它消除了初始化对象的(可能昂贵的)开销
  • 它简化并可以优化多个相同类型对象大部分数据相同的使用情况

例如,假设您的程序使用从网络检索的大部分不变信息解析的数据创建对象。与每次创建新对象时检索数据和重新解析数据不同,可以使用原型模式来在需要新对象时简单地复制原始对象。

还假设该对象可能具有使用大量内存的数据,例如表示图像的数据。可以通过使用写时复制风格继承来减少内存使用,其中显示原始未复制的数据,直到代码尝试更改该数据。然后,新数据将掩盖对原始数据的引用。


9
有点挑剔(我给了一个+1),但我认为第一点措辞有点过于强硬。它可能会降低初始化对象的开销。在许多情况下,无法避免复杂的初始化代码,这种情况下该模式对你没有任何好处。此外,我也不愿意鼓励克隆对象共享数据的想法。通常的想法是克隆应该是独立的,因此,除非你小心地将状态复制(而不是共享)到克隆体之间,否则很容易破坏它们之间的关系。 - Adam Parkin
好的回答!只有您的第二个用例违反了Java API建议的约定(“按照惯例,此方法返回的对象应该是独立于被克隆的对象的(即正在进行克隆的对象)。”,请参见https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html#clone()) - robert
1
第二个问题是通过使用享元模式来解决的,而不是原型模式。 - AFP_555

41

原型模式是一种基于克隆预配置对象的创建模式。其思想在于选择一个已经配置好默认或接近某个特定用例的对象,然后克隆此对象并根据您的确切需求进行配置。

该模式对于消除大量样板代码非常有用,当所需的配置会很繁琐时。我将原型看作预设对象,您可以将其保存为新起点的一堆状态。


1
“在不支持此类功能的平台上,使用鸭子类型和多重继承是个好主意。” 不错。但仍然存在需要原始对象进行克隆的烦人问题。 - user1905391
2
那么,回到最初的问题,有什么意义呢? 如何解决鸡和蛋的问题?《GoF》书中描述了原型管理器的概念,但未能提供足够具体的细节,说明如何可能工作(如果一个类在运行时向管理器注册,则如何避免管理器的客户端对返回的克隆实例进行显式转换?) - Adam Parkin
@Mark,你能解决这个问题的核心——鸡生蛋还是蛋生鸡的问题吗? - jaco0646
1
原型是“克隆此对象并进行微小更改”的意思。虽然你仍然需要创建原始对象,但这种创建方法并不是为了解决这个问题,而是针对你有一些高度配置的对象可以作为起点的情况。就像立体声或合成器的预设一样。 - Mark Pauley
只有这个让我明白原型模式实际上是用来做什么的。 - NuttLoose
显示剩余2条评论

15
许多其他回答都谈到了克隆已配置对象的成本节约,但我想扩展原型模式的另一个“要点”。在某些语言中,类被视为一等对象,您可以通过简单地传递类名来配置客户端在运行时创建的对象类型。在像C++这样的语言中,其中类不被视为一等对象,原型模式允许您实现相同的效果。
例如,假设我们在餐厅里有一位厨师,他的工作是制作和提供餐点。 假设这位厨师低薪不满,因此他制作以下菜肴:
class Chef {
    public:
        void prepareMeal() const {
            MozzarellaSticksWithKetchup* appetizer = new MozzarellaSticksWithKetchup();
            // do something with appetizer...

            HockeyPuckHamburgerWithSoggyFries* entree = new HockeyPuckHamburgerWithSoggyFries();
            // do something with entree...

            FreezerBurnedIceCream* dessert = new FreezerBurnedIceCream();
            // do something with dessert...
        }
};

假设现在我们想把 Chef 变成一个炫耀的名人厨师。这意味着他/她必须在 prepareMeal()new 不同的菜肴。我们希望修改该方法,以便可以将由 Chef new 的菜肴类型作为参数指定。在其他支持类作为一等对象的语言中,我们可以简单地将类名作为参数传递给方法。但在 C++ 中不能这样做,所以我们可以从原型模式中受益:

class Appetizer {
    public:
        virtual Appetizer* clone() const = 0;
        // ...
};

class Entree {
    public:
        virtual Entree* clone() const = 0;
        // ...
};

class Dessert {
    public:
        virtual Dessert* clone() const = 0;
        // ...
};

class MozzarellaSticksWithKetchup : public Appetizer {
    public:
        virtual Appetizer* clone() const override { return new MozzarellaSticksWithKetchup(*this); }
        // ...
};

class HockeyPuckHamburgerWithSoggyFries : public Entree {
    public:
        virtual Entree * clone() const override { return new HockeyPuckHamburgerWithSoggyFries(*this); }
        // ...
};

class FreezerBurnedIceCream : public Dessert {
    public:
        virtual Dessert * clone() const override { return new FreezerBurnedIceCream(*this); }
        // ...
};

// ...and so on for any other derived Appetizers, Entrees, and Desserts.

class Chef {
    public:
        void prepareMeal(Appetizer* appetizer_prototype, Entree* entree_prototype, Dessert* dessert_prototype) const {
            Appetizer* appetizer = appetizer_prototype->clone();
            // do something with appetizer...

            Entree* entree = entree_prototype->clone();
            // do something with entree...

            Dessert* dessert = dessert_prototype->clone();
            // do something with dessert...
        }
};

请注意,clone()方法创建派生类型的实例,但返回指向父类型的指针。这意味着我们可以通过使用不同的派生类型来更改被创建的对象的类型,而客户端将不会知道差异。现在,这种设计使得我们能够在运行时配置Chef--我们原型的客户端--以制作不同类型的菜肴:

Chef chef;

// The same underpaid chef from before:
MozzarellaSticksWithKetchup mozzarella_sticks;
HockeyPuckHamburgerWithSoggyFries hamburger;
FreezerBurnedIceCream ice_cream;
chef.prepareMeal(&mozzarella_sticks, &hamburger, &ice_cream);

// An ostentatious celebrity chef:
IranianBelugaCaviar caviar;
LobsterFrittataWithFarmFreshChives lobster;
GoldDustedChocolateCupcake cupcake;
chef.prepareMeal(&caviar, &lobster, &cupcake);

或许你会想,既然原型模式和工厂方法模式可以达到相同的效果,为什么不直接使用工厂方法模式呢?因为工厂方法模式需要创建者类的层次结构来镜像被创建产品的层次结构;也就是说,我们需要一个 MozzarellaSticksWithKetchupCreator 类和一个 make() 方法, 一个 HockeyPuckHamburgerWithSoggyFriesCreator 类和一个 make() 方法等等。因此,你可以将原型模式简单地看作是缓解工厂方法模式经常引入的代码冗余的一种方式。

这个论点摘自《设计模式:可复用面向对象软件的基础》(即“四人组”书)。


7
如果你想创建一个对象,但不想经过昂贵的对象创建流程,例如进行网络或数据库调用,那么可以使用原型模式。只需创建对象的副本并在其上进行更改即可。

2

使用原型模式完全取决于你的问题。在大多数情况下,克隆和创建新对象没有任何区别。 但是如果你在构造函数或设置属性时执行了一些复杂或耗时的操作,并且必须执行复杂和耗时的操作,则原型模式将对我们有所帮助。因为从旧实例复制对象到新实例更容易,而且性能更高(深度克隆)。因此,该模式更适用于状态长时间不变的对象。 在使用此模式之前,请彻底分析你的问题。


1

如果您有这样的需求,需要填充或使用包含可重复对象的相同数据

但是

从现有对象构建不可能,例如[使用网络流构建对象],或者

构建一个对象很耗时[通过从数据库获取数据构建大型对象],那么请使用此设计模式。在此模式中,创建了一个现有对象的副本,该副本与原始对象不同,并且可以像原始对象一样使用。


0
原型设计模式产生一个可克隆的对象,在以下情况下可以使用:
1)当创建对象的成本很高,并且对象持有的数据不会过时或过时的数据可以被减轻时。对象持有的数据可能是从数据库查询的,或通过HTTP请求获取的,或通过长时间运行的CPU进程计算的,在这种情况下,克隆一个不可变的对象是有益的,但不会导致数据过时。
2)有时候可能无法以编程方式重新创建对象,比如状态对象或快照或DOM结构,您也无法对原始对象进行变异,因为它是共享的,您也无法合理地持久化该对象,在这种情况下,克隆一个不可变的对象是有益的,但不会导致数据过时。
3)您可能希望在不同的环境中对数据集进行精确复制以进行测试,或者当您需要为对象属性设置默认值时,克隆将对此有所帮助。

-1
与抽象工厂模式相比,使用原型模式可以避免具有大型工厂层次结构的问题,只需拥有一个大型产品层次结构即可。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接