你何时会使用建造者模式?

606

什么是建造者模式的一些常见且真实的使用示例?它为你带来了什么好处?为什么不只使用工厂模式?


看看我的InnerBuilder,这是一个IntelliJ IDEA插件,它在生成菜单(Alt+Insert)中添加了一个“Builder”操作,可以生成一个内部构建器类,就像《Effective Java》中所描述的那样。https://github.com/analytically/innerbuilder - analytically
http://stackoverflow.com/questions/35238292/design-an-api-with-cascade-function-calls-class-dothis-dothat/35238377#35238377提到了一些使用建造者模式的API。 - Alireza Fattahi
Aaron和Tetha的回答真的很有信息量。这是与这些答案相关的完整文章 - Diablo
13个回答

1136

以下是一些理由,支持在Java中使用建造者模式及示例代码。但这是《设计模式》中Gang of Four所介绍的建造者模式的实现。你在Java中使用它的原因同样适用于其他编程语言。

正如Joshua Bloch在《Effective Java第二版》中所述:

  

当设计类的构造函数或静态工厂有超过几个参数时,建造者模式是一个不错的选择。

我们都曾经遇到过一个类,它有一系列构造函数,每次添加都会增加一个新的可选参数:

Pizza(int size) { ... }        
Pizza(int size, boolean cheese) { ... }    
Pizza(int size, boolean cheese, boolean pepperoni) { ... }    
Pizza(int size, boolean cheese, boolean pepperoni, boolean bacon) { ... }

这被称为Telescoping构造函数模式。 这种模式的问题在于,一旦构造函数有4或5个参数,就会变得难以记住所需的参数顺序以及在给定情况下可能需要哪个特定的构造函数。

你可以使用一个替代方案来避免Telescoping构造函数模式,那就是使用JavaBean模式。在JavaBean模式中,你首先使用必选参数调用构造函数,然后在之后调用任何可选的setter方法:

Pizza pizza = new Pizza(12);
pizza.setCheese(true);
pizza.setPepperoni(true);
pizza.setBacon(true);

问题在于对象是通过多次调用创建的,因此在构建过程中可能处于不一致的状态。这还需要大量额外的工作来确保线程安全。

更好的选择是使用 Builder 模式。

public class Pizza {
  private int size;
  private boolean cheese;
  private boolean pepperoni;
  private boolean bacon;

  public static class Builder {
    //required
    private final int size;

    //optional
    private boolean cheese = false;
    private boolean pepperoni = false;
    private boolean bacon = false;

    public Builder(int size) {
      this.size = size;
    }

    public Builder cheese(boolean value) {
      cheese = value;
      return this;
    }

    public Builder pepperoni(boolean value) {
      pepperoni = value;
      return this;
    }

    public Builder bacon(boolean value) {
      bacon = value;
      return this;
    }

    public Pizza build() {
      return new Pizza(this);
    }
  }

  private Pizza(Builder builder) {
    size = builder.size;
    cheese = builder.cheese;
    pepperoni = builder.pepperoni;
    bacon = builder.bacon;
  }
}

请注意,Pizza是不可变的并且参数值都在一个单独的位置。因为Builder的setter方法返回Builder对象,它们可以链接起来使用

Pizza pizza = new Pizza.Builder(12)
                       .cheese(true)
                       .pepperoni(true)
                       .bacon(true)
                       .build();

这种方法使得代码易于编写,非常易于阅读和理解。 在这个例子中,构建方法可以修改,在从构建器复制参数到Pizza对象后检查参数并如果提供了无效的参数值,则抛出IllegalStateException异常。 这种模式是灵活的,将来可以很容易地添加更多参数。 它只有在构造函数需要4个或5个以上参数时才有用。 话虽如此,如果您怀疑将来可能会添加更多参数,那么一开始使用也许是值得的。

我从Joshua Bloch的书Effective Java, 2nd Edition中大量借鉴了这个主题。要了解更多关于这种模式和其他有效的Java实践,我强烈推荐它。


27
和原始的GOF Builder模式不同?因为没有Director类。对我来说似乎几乎像是另一种模式,但我同意它非常有用。 - Lino Rosa
214
针对这个例子,去掉布尔参数并使用new Pizza.Builder(12).cheese().pepperoni().bacon().build();更加简洁明了。 - Fabian Steeg
52
这似乎更像是"流畅接口"(Fluent Interface)而不是"建造者模式"(Builder Pattern)。 - Robert Harvey
22
@Fabian Steeg,我认为人们对更美观的布尔类型setter 进行了过度反应,请记住这种setter不允许在运行时进行更改:Pizza.Builder(12).cheese().pepperoni().bacon().build();,如果你只需要一些胡椒披萨,那么你需要重新编译代码或者有不必要的逻辑。至少你也应该提供类似于@Kamikaze Mercenary最初建议的带参数版本:Pizza.Builder(12).cheese(true).pepperoni(false).bacon(false).build();。当然,我们从来没有单元测试,是吗? - egallardo
23
@JasonC对,一个不可变的披萨有什么用处呢? - Maarten Bodewes
显示剩余33条评论

348

考虑一家餐厅。"今日菜品"的创建是工厂模式,因为你告诉厨房“给我今日菜品”,而厨房(工厂)根据隐藏的标准决定生成哪个对象。

如果你点一份自定义比萨,建造者模式就会出现。在这种情况下,服务员告诉厨师(建造者)“我需要一份比萨;加入芝士、洋葱和熏肉!”因此,建造者公开了生成的对象应该具有的属性,但隐藏了如何设置它们。


Nitin在这个问题的另一个答案中扩展了厨房类比。 - Pops

285
建造者和工厂模式之间的关键区别在于,我认为建造者在需要对对象进行大量构建时非常有用。例如,想象一下DOM。你必须创建大量的节点和属性才能得到最终的对象。而当工厂可以在一个方法调用中轻松创建整个对象时,则使用工厂。
使用建造者的一个例子是构建XML文档,我曾经在构建HTML片段时使用过该模型,例如,我可能会为构建特定类型的表格创建一个建造者,并且它可能具有以下方法(参数未显示)
BuildOrderHeaderRow()
BuildLineItemSubHeaderRow()
BuildOrderRow()
BuildLineItemSubRow()

这个构造器将会为我生成HTML代码。相比于浏览一个庞大的过程性方法,这样做要容易得多。
请查看维基百科上的建造者模式

20

.NET StringBuilder类是建造者模式的一个很好的例子。它主要用于通过一系列步骤创建字符串。通过调用ToString()方法,您得到的最终结果始终是一个字符串,但该字符串的创建方式取决于StringBuilder类中使用了哪些函数。总之,基本思想是构建复杂对象并隐藏其构建方式的实现细节。


9
我不认为这是建造者模式。StringBuilder 只是字符数组类的另一种实现(即字符串),但它考虑了性能和内存管理,因为字符串是不可变的。 - Charles Graham
24
这绝对是建造者模式,就像Java中的StringBuilder类一样。请注意这两个类的append()方法都返回StringBuilder本身,这样就可以在最后调用toString()之前链接'b.append(...).append(...)'。 引用:http://www.infoq.com/articles/internal-dsls-java - pohl
3
我不认为这真的是建造者模式,我会说这更像是一个流畅接口。 - Didier A.
注意这两个类的append()方法都返回StringBuilder本身,这不是建造者模式,而只是一种流畅接口。只是通常建造者也会使用流畅接口。建造者并不一定要有流畅接口。 - bytedev
但需要注意的是,StringBuilder本质上不是同步的,而StringBuffer是同步的。 - Alan Deep

15

我一直不喜欢生成器模式,因为它往往是笨拙的、显眼的,并且常常被经验不足的程序员滥用。只有当您需要从某些数据中组装对象并需要后初始化步骤(即在收集所有数据后执行某些操作)时,它才有意义。而在99%的情况下,生成器仅用于初始化类成员。

在这种情况下,更好的方法是在类内部声明withXyz(...)类型的setter方法,并使其返回对自身的引用。

考虑以下示例:

public class Complex {

    private String first;
    private String second;
    private String third;

    public String getFirst(){
       return first; 
    }

    public void setFirst(String first){
       this.first=first; 
    }

    ... 

    public Complex withFirst(String first){
       this.first=first;
       return this; 
    }

    public Complex withSecond(String second){
       this.second=second;
       return this; 
    }

    public Complex withThird(String third){
       this.third=third;
       return this; 
    }

}


Complex complex = new Complex()
     .withFirst("first value")
     .withSecond("second value")
     .withThird("third value");

现在我们有一个整洁的单一类,它管理自己的初始化并执行与构建器几乎相同的工作,只是更加优雅。


我刚刚决定将我的复杂XML文档构建为JSON。首先,我如何知道'Complex'类是否能够提供可转换为XML的产品,并且如何更改它以生成可转换为JSON的对象?简短回答:我不能,因为我需要使用构建器。我们又回到了起点... - David Barker
2
完全胡扯,Builder的设计旨在建立不可变对象,并具有在不触及产品类的情况下更改建立方式的能力。 - Mickey Tin
11
嗯?你在我的回答中有没有看到我解释“Builder模式”的设计目的是什么?这是对顶部问题“何时使用Builder模式?”的替代观点,其基于无数滥用模式的经验,其中更简单的方法可以更好地完成工作。所有模式都有用,只要你知道何时以及如何使用它们-这也是首次记录模式的全部意义!当模式被过度使用或更糟糕-被误用时,它就会成为您的代码环境中的反模式。唉... - Pavel Lechev

12

对于一个多线程的问题,我们需要为每个线程建立一个复杂的对象。该对象表示正在处理的数据,可能根据用户输入而变化。

我们能使用工厂吗?是的。

为什么我们没有用工厂呢?我想建造者更有意义。

工厂用于创建相同基本类型的不同对象(实现相同接口或基类)。

建造者反复构建相同类型的对象,但是构建过程是动态的,因此可以在运行时进行更改。


10

在学习 Microsoft MVC 框架时,我想到了建造者模式。我在 ControllerBuilder 类中发现了这个模式。该类返回控制器工厂类,然后用于构建具体控制器。

我认为使用建造者模式的好处在于,可以创建自己的工厂并将其插入到框架中。

@Tetha,这里有一个由意大利人经营的餐厅(框架),提供披萨。为了准备披萨,意大利人(对象构建器)使用烤箱(工厂)和披萨底(基类)。

现在印度人接管了这家餐厅,餐厅(框架)提供的是印度煎饼。为了准备煎饼,印度人(对象构建器)使用平底锅(工厂)和面粉(基类)。

如果你看一下这种情况,食物是不同的,准备食物的方式也不同,但是它们在同一个餐厅(同一个框架)内。餐厅应该被建造成可以支持任何菜系,如中国菜、墨西哥菜等。框架内的对象构建器可以插入你想要的菜系,例如:

class RestaurantObjectBuilder
{
   IFactory _factory = new DefaultFoodFactory();

   //This can be used when you want to plugin the 
   public void SetFoodFactory(IFactory customFactory)
   {
        _factory = customFactory;
   }

   public IFactory GetFoodFactory()
   {
      return _factory;
   }
}

10

当需要处理大量选项时,您可以使用它。考虑像 jmock 这样的东西:

m.expects(once())
    .method("testMethod")
    .with(eq(1), eq(2))
    .returns("someResponse");

感觉更加自然,而且...

还有XML构建、字符串构建等等。想象一下如果java.util.Map像一个构建器一样具有put方法,你就可以像这样做:

Map<String, Integer> m = new HashMap<String, Integer>()
    .put("a", 1)
    .put("b", 2)
    .put("c", 3);

3
我忘了阅读“if”映射实现建造者模式,看到那里的构造函数时感到惊讶。 :) - sethu
3
很抱歉。在许多语言中,返回 self 而不是 void 是很常见的。你可以在 Java 中这样做,但这并不是很普遍的做法。 - Dustin
7
地图示例只是方法链的一个例子。 - nogridbag
@nogridbag 它实际上更接近于方法级联。虽然它使用了链式调用来模拟级联,因此显然是链式调用,但从语义上看它的行为类似于级联。 - Didier A.

8

7
另一个建造者的优点是,如果您有一个工厂,代码中仍然存在一些耦合,因为为了Factory能够工作,它必须知道所有可能创建的对象。如果您添加另一个可能被创建的对象,则必须修改工厂类以包括该对象。这也发生在抽象工厂中。
与之相反,使用建造者只需为此新类创建一个新的具体生成器即可。指导者类将保持不变,因为它在构造函数中接收生成器。
另外,还有许多种建造者的风格。Kamikaze Mercenary(亦称“神风雇佣兵”)提供了另一种建造者的风格。

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