建造者设计模式:为什么需要一个指挥者?

48

最近我接触了建造者设计模式。看起来不同的作者用“建造者模式”指代不同的变体,所以让我描述一下我所询问的模式。

我们有一个用于创建 产品 的算法,即不同类型的对象。在足够抽象的层面上,对于所有产品类型,算法都是相同的,但每种产品类型需要算法的每个抽象步骤的不同实现。例如,我们可能有以下制作蛋糕的算法:

 1. Add liquids.
 2. Mix well.
 3. Add dry ingredients.
 4. Mix well.
 5. Pour batter into baking pan.
 6. Bake.
 7. Return baked cake.
不同的蛋糕需要不同的步骤实现,例如使用什么液体/干配料、搅拌速度以及烘焙时间等等。
该模式要求按照以下方式进行。为每个产品创建一个具体的建造者类,其中包括上述各步骤的实现。所有这些类都派生自抽象建造者基类,本质上是一个接口。例如,我们将有一个抽象基类CakeBaker,其中包含纯虚方法AddLiquid(),MixLiquids()等。具体的蛋糕师将是具体子类,例如:
class ChocolateCakeBaker : public CakeBaker {
public:
   virtual void AddLiquids()
   {
        // Add three eggs and 1 cup of cream
   }

   virtual void AddDryIngredients()
   {
       // Add 2 cups flour, 1 cup sugar, 3 tbsp cocoa powder,
       // 2 bars ground chocolate, 2 tsp baking powder
   }
      ...
      ...
};

LemonCitrusCakeBaker将成为CakeBaker的子类,但在其方法中使用不同的配料和数量。

不同种类的蛋糕也将类似地成为抽象的Cake基类的子类。

最后,我们有一个类来实现抽象算法。这就是导演。在烘焙的例子中,我们可以称之为ExecutiveBaker。该类将接受(来自客户端的)具体构建器对象,并使用其方法以创建并返回所需的产品。

我的问题是:为什么需要将导演与抽象构建器分开?为什么不将它们合并为单个构建器抽象基类,使原始抽象构建器的公共方法受保护(并且具体子类像以前一样覆盖这些方法)?

7个回答

34

建造者模式的核心部分涉及抽象建造者和其子类(具体建造者)。根据GoF的设计模式,导演仅在产品的某个部分需要构建时“通知”建造者,这可以由客户端完美地完成。

Java API中的StringBuilder类是没有相应导演的建造者的一个例子 - 通常是客户端类“指导”它。

此外,在Effective JavaCreating and Destroying Java Objects中,Joshua Bloch建议使用建造者模式,并且他不包括导演。


这是否意味着在MVC应用程序中,控制器(实际上是控制器的一个方法,在我的特定情况下)可以成为“导演”? - Yugo Amaryl

32

GoF版本的建造者模式中没有不带指导者的建造者。这其中有一个不同的点,但我将会进一步解释。

建造者模式的重点是为你提供创建相同对象的多种方式。建造者应该只有构建对象不同部分的方法,但算法——这些函数执行的方式——应该是指导者关心的。如果没有指导者,每个客户端都需要准确知道构建工作的方式。但是有了指导者,客户端只需要知道在特定情况下使用哪个建造者。

因此,我们有两个部分:

  1. 建造者,逐个创建对象的各个部分。需要注意的重要事项是,为此它保留了已创建对象的状态。
  2. 指导者,控制建造者函数的执行方式。

现在回到我之前提到的重点。建造者模式的一部分在其他情况下也很有用,并且已经被不同的供应商用于不同目的,而没有指导者。这样使用的一个具体例子是Doctrine Query Builder

这种方法的缺点是,当构建器开始构建对象时,它变得有状态,如果客户端在对象创建后没有重置构建器,则另一个客户端或多次使用的同一客户端可能会获取先前创建的对象的部分。因此,Doctrine使用工厂模式来创建构建器的每个实例。
希望这能帮助那些正在搜索相关内容的人。

你可以直接应用CQS并使构建器为只读。因此,您无需担心状态是否已修改。 - Mihai Bratulescu

16

如果你将制造者和导演分开,你就记录了从一组零部件组装产品的不同责任(导演)和创建部件的责任(制造者)。

  • 在制造者中,您可以更改如何构建部件。在您的情况下,您可以决定AddLiquid()应该添加奶油还是牛奶。
  • 在导演中,您可以更改如何组装这些部件。在您的情况下,通过使用AddChocolate()而不是AddFruits(),您会获得不同的蛋糕。

如果你需要这种额外的灵活性,我建议重新命名(因为在制造者中使用baker似乎是制造者的工作而不是组装部件的工作)。

class LightBakingSteps : public BakingSteps {
public:
    virtual void AddLiquids()
    {
        // Add milk instead of cream
    }

    virtual void AddDryIngredients()
    {
        // Add light sugar
    }

    ...
};

class ChoclateCakeBaker : public CakeBaker {
public:
     Cake Bake(BakingSteps& steps)
     {
         steps.AddLiquieds();
         steps.AddChocolate();      // chocolate instead of fruits
         return builder.getCake();
     }
}

2
但是这种分离的优势是什么呢?如果我想要不同的组装协议,我需要有不同的指导者,每个指导者都针对特定(抽象)构建器进行了定制。这些指导者没有任何可以抽象的共同功能,除了可能不太有用的MakeSomething()。您始终可以将任何任务分解为更小的子任务和责任,并且始终可以向任何模式添加另一级“管理器”或“指导者”。我认为,这种模式的作者可能正在沿着您概述的思路思考,但我看不出其中的价值。 - Ari
1
@Ari,你可能看不到编程模式的价值,因为编程模式的价值与程序所涉及的领域密切相关。为什么每个蛋糕都需要特定的面包师呢?如果把领域拿走,那么所有的东西都只是屏幕上的噪音和随机字符。这就是我在回答中所说的意思。 - Arnis Lapsa

4

Builder知道如何执行特定的步骤。

Director知道如何使用Builder步骤组装整个东西。

他们一起工作。

我能看到这种模式唯一的脆弱之处是客户端能够直接调用Builder方法而不需要Director - 这可能会带来一些问题和不连贯性(例如不调用Init方法,该方法是整个算法的一部分)。


3
假设你想制作一款不需要干性成分的蛋糕。你可以通过向Director添加新方法或创建另一个Director来实现这一目标。这将保护你免受继承复杂性的影响,同时也会使你的代码更加灵活。

0

模式的缺点是它们用技术术语污染了我们对业务领域的理解,使我们的焦点变得模糊。

在我看来,蛋糕和制作蛋糕的知识之间存在过多的耦合。这些可以通过在我们的代码中引入蛋糕配方的概念(更像是从现实世界借鉴,通过业务领域设计我们的模型)来解耦。配方将包含成分和烘焙步骤(只是一个步骤名称,不是实际实现,因为配方不会烤蛋糕),描述如何制作蛋糕。我们的面包师会有一个BakeCake(recipe)方法,以及根据烘焙步骤的一堆较小的方法,例如混合、添加成分等。

请注意,如果您需要一般性地建模厨师,而不仅仅是蛋糕师傅,您还需要将制作烘焙品的知识与面包师本身分离开来。这可以通过引入厨师具备技能的概念来实现。


这更像是一种模式的替代方案,而不是对其进行解释。我有误解吗? - Ari
@Ari 确实是这样。从技术上讲,我并没有回答问题。只是想提醒一下,模式本身并不那么重要。就像口语一样 - 你必须至少了解基础知识,但最终 - 重要的是你想要表达什么。 - Arnis Lapsa

0

我同意你的观点。我认为另一种方法是CakeBaker应该有一个GetCake()方法,返回一个蛋糕(Cake类),以及MakeCake()方法,其中算法将运行。这很好,但另一方面也有一个负责任的分离。考虑抽象构建者和特定构建者仅作为蛋糕部件的构建者,而Director则是经理或设计师,其责任是组装和生产蛋糕。


1
责任分离并没有让我信服。在有些情况下,分离是有意义的;在其他情况下则不然。我认为这不是一个模式的一部分,而是一个正交的决策。 - Ari
学习设计模式时,我尝试理解其原则而非简单地照搬。此外,在实践中,我经常将它们进行组合。因此,不必像文档中那样编写相同数量的类或接口,而是让代码更加灵活和可修改。 - Arseny

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