【问题标题】:What's the point of the Prototype design pattern?原型设计模式的意义何在?
【发布时间】:2012-12-02 23:41:38
【问题描述】:

所以我在学校学习设计模式。今天有人告诉我“原型”设计模式。

我一定错过了什么,因为我看不到它的好处。我在网上看到有人说它比使用new 更快,但这没有意义;在某些时候,无论新对象如何创建,都需要为其分配内存。

这种模式不是和“鸡还是蛋”的问题一样吗?由于原型模式本质上只是克隆对象,因此在某些时候必须自己创建原始对象(即不克隆)。这意味着我需要准备好要克隆的每个对象的现有副本?

谁能解释这种模式的用途是什么?

【问题讨论】:

  • 这个问题可能更适合程序员 StackExchange,因为它更多的是关于编程的概念性问题

标签: design-patterns prototype-pattern


【解决方案1】:

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

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

例如,假设您的程序使用从通过网络检索到的大部分不变信息中解析的数据创建的对象。无需在每次创建新对象时检索数据并重新解析它,原型模式可用于在需要新对象时简单地复制原始对象。

另外,假设对象可能包含占用大量内存的数据,例如表示图像的数据。使用写时复制样式继承可以减少内存,其中显示原始的、未重复的数据,直到代码尝试更改该数据。然后,新数据将屏蔽以引用原始数据。

【讨论】:

  • 吹毛求疵(我给了 +1),但我认为第 1 点措辞有点强硬。它可能减少初始化对象的开销。在许多情况下,复杂的初始化代码是不可避免的,在这种情况下,该模式对您没有任何好处。我也不愿意鼓励克隆对象可以与其他克隆共享数据的想法。典型的想法是克隆应该是独立的,所以除非你小心,否则如果你在克隆之间共享​​>(而不是复制)状态,你很容易打破这种关系。
  • 好答案!只有您的第二个用例违反了 Java API 中建议的约定(“按照约定,此方法返回的对象应该独立于此对象(正在克隆)。”,请参阅docs.oracle.com/javase/7/docs/api/java/lang/Object.html#clone()
  • 第二点是用享元模式解决的,不是原型。
【解决方案2】:

原型模式是一种基于克隆预配置对象的创建模式。这个想法是,您选择一个为默认配置或在某些特定用例的范围内配置的对象,然后克隆该对象并根据您的确切需求进行配置。

当所需的配置繁重时,该模式对于删除一堆样板代码很有用。我认为 Prototypes 是一个预设对象,你可以在其中保存一堆状态作为新的起点。

【讨论】:

  • '在本机不支持此类东西的平台上进行鸭式打字和多重继承'。好点子。但仍然留下了需要克隆原始对象的烦人问题。
  • 那么,回到最初的问题,有什么意义呢?你如何解决鸡/蛋问题? GoF 书描述了 Prototype Manager 的想法,但未能提供有关它如何工作的足够具体细节(如果在运行时向管理器注册了一个类,那么如何避免让管理器的客户对返回的克隆实例?)
  • @Mark,你能解决似乎是问题核心的先有鸡还是先有蛋的问题吗?
  • 原型是“克隆这个对象并做一些小的改动”。确实,您仍然必须创建原始对象。这不是这种创建方法试图解决的问题,而是解决您有一些高度配置的对象可以作为起点的情况。类似于立体声或合成器的预设。
  • 此外,有些语言 (Javascript) 严格来说没有其他方法可以使我们通常认为是“对象”,即与访问者结婚的封装数据的新/单独实例或方法。
【解决方案3】:

这里的许多其他答案都谈到了克隆已配置对象的成本节省,但我想扩展原型模式的另一个“点”。在某些语言中,类被视为一等对象,您可以配置客户端在运行时创建的对象类型,只需向其传递类名即可。在像 C++ 这样的语言中,类不被视为一等对象,原型模式允许您实现相同的效果。

例如,假设我们在一家餐厅有一个Chef,他的工作是制作和提供餐点。假设Chef 薪水过低且心怀不满,所以他做了如下菜:

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);

您可能想知道,使用这种方式,原型模式为您购买了与工厂方法模式相同的东西,那么为什么不直接使用呢?因为工厂方法模式需要创建者类的层次结构来反映正在创建的产品的层次结构;即我们需要一个带有make() 方法的MozzarellaSticksWithKetchupCreator,一个带有make() 方法的HockeyPuckHamburgerWithSoggyFriesCreator,等等。因此,您可以将原型模式简单地视为一种减轻工厂方法模式经常引入的代码冗余的方法。

这个论点来自Design Patterns: Elements of Reusable Object-Oriented Software,又名“四人帮”一书。

【讨论】:

    【解决方案4】:

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

    【讨论】:

      【解决方案5】:

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

      【讨论】:

        【解决方案6】:

        与抽象工厂模式相比,通过使用原型模式,您不必有大的工厂层次结构,只需要有大的产品层次结构。

        【讨论】:

          【解决方案7】:

          如果您有需求,您需要在其中填充或使用包含 Object repeatable 的相同数据

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

          构建一个对象是耗时的[构建一个大对象,通过从数据库中获取数据] 然后使用这个设计模式,因为在这个副本中创建了现有的对象,这个副本将不同于原始对象并且可以像原来的一样使用。

          【讨论】:

            猜你喜欢
            • 2011-05-02
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2011-07-25
            • 1970-01-01
            • 2015-06-29
            • 2016-03-25
            • 2012-07-27
            相关资源
            最近更新 更多