你什么时候使用Builder模式?

时间:2008-11-30 05:41:23

标签: java design-patterns builder

使用Builder Pattern的常见真实示例是什么?它给你带来了什么?为什么不使用工厂模式?

15 个答案:

答案 0 :(得分:959)

以下是一些争论在Java中使用模式和示例代码的原因,但它是由设计模式中的Gang of Four所涵盖的Builder模式的实现。您在Java中使用它的原因也适用于其他编程语言。

约书亚布洛赫在Effective Java, 2nd Edition中说:

  

在设计构造函数或静态工厂具有多个参数的类时,构建器模式是一个不错的选择。

我们在某个时刻遇到了一个带有构造函数列表的类,其中每个添加都添加了一个新的选项参数:

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

这称为伸缩构造函数模式。此模式的问题是,一旦构造函数长度为4或5个参数,就会变得难以记住所需的参数的顺序以及在给定情况下您可能想要的特定构造函数。

Telescoping构造函数模式的一个替代 JavaBean模式,您可以使用必需参数调用构造函数,然后在以下情况下调用任何可选的setter:

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

这里的问题是,因为对象是在多次调用中创建的,所以在构建过程中它可能处于不一致状态。这还需要花费很多额外的精力来确保线程安全。

更好的选择是使用构建器模式。

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实践的更多信息,我强烈推荐它。

答案 1 :(得分:308)

考虑一家餐馆。创造“今日餐”是一种工厂模式,因为你告诉厨房“给我今天吃饭”,厨房(工厂)根据隐藏的标准决定生成什么对象。

如果您订购自定义披萨,则会显示构建器。在这种情况下,服务员告诉厨师(建筑师)“我需要一个披萨;加入奶酪,洋葱和培根!”因此,构建器公开生成的对象应具有的属性,但隐藏如何设置它们。

答案 2 :(得分:241)

构建器和工厂IMHO之间的关键区别在于,当您需要执行大量操作来构建对象时,构建器非常有用。例如,想象一下DOM。您必须创建大量节点和属性才能获得最终对象。当工厂可以在一次方法调用中轻松创建整个对象时,使用工厂。

使用构建器的一个示例是构建XML文档,我在构建HTML片段时使用了此模型,例如我可能有一个构建特定类型的表的构建器,它可能具有以下方法 (参数未显示)

BuildOrderHeaderRow()
BuildLineItemSubHeaderRow()
BuildOrderRow()
BuildLineItemSubRow()

然后,此构建器将为我吐出HTML。这比阅读大型程序方法更容易阅读。

查看Builder Pattern on Wikipedia

答案 3 :(得分:18)

.NET StringBuilder类是构建器模式的一个很好的例子。它主要用于通过一系列步骤创建字符串。你在ToString()上得到的最终结果总是一个字符串,但该字符串的创建因StringBuilder类中使用的函数而异。总而言之,基本思想是构建复杂的对象并隐藏其构建方式的实现细节。

答案 4 :(得分:9)

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

我们可以使用工厂吗?是

为什么不呢?我想,生成器更有意义。

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

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

答案 5 :(得分:9)

在浏览Microsoft MVC框架时,我想到了构建器模式。我在ControllerBuilder类中遇到了这个模式。这个类是返回控制器工厂类,然后用它来构建具体的控制器。

我在使用构建器模式时看到的优点是,您可以创建自己的工厂并将其插入到框架中。

@Tetha,可以有一家由意大利人经营的餐厅(Framework)供应比萨饼。为了准备披萨意大利人(Object Builder)使用Owen(工厂)和披萨基地(基类)。

现在印度人从意大利人那里接过餐馆。印度餐厅(框架)服务器dosa而不是披萨。为了准备dosa印度人(对象建设者)使用煎锅(工厂)与迈达(基类)

如果你看情景,食物是不同的,食物的准备方式是不同的,但在同一个餐厅(在相同的框架下)。餐厅应该以这样的方式建造,它可以支持中国,墨西哥或任何美食。框架内的对象构建器有助于插入您想要的烹饪类型。例如

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

答案 6 :(得分:8)

当你有很多选择要处理时,你可以使用它。想想像jmock这样的事情:

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

感觉更自然,而且......可能。

还有xml构建,字符串构建和许多其他东西。想象一下,如果java.util.Map作为建设者。你可以做这样的事情:

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

答案 7 :(得分:6)

在之前的答案(双关语)的基础上,Groovy构建了一个优秀的现实世界示例,支持Builders

请参阅Builders

中的Groovy Documentation

答案 8 :(得分:5)

构建器的另一个优点是,如果你有一个Factory,你的代码中仍然存在一些耦合,因为要使Factory工作,它必须知道它可能创建的所有对象 。如果添加另一个可以创建的对象,则必须修改工厂类以包含他。这也发生在抽象工厂中。

另一方面,使用构建器,您只需为此新类创建新的具体构建器。导演类将保持不变,因为它在构造函数中接收构建器。

此外,还有许多口味的建设者。 Kamikaze Mercenary`s给出了另一个。

答案 9 :(得分:5)

/// <summary>
/// Builder
/// </summary>
public interface IWebRequestBuilder
{
    IWebRequestBuilder BuildHost(string host);

    IWebRequestBuilder BuildPort(int port);

    IWebRequestBuilder BuildPath(string path);

    IWebRequestBuilder BuildQuery(string query);

    IWebRequestBuilder BuildScheme(string scheme);

    IWebRequestBuilder BuildTimeout(int timeout);

    WebRequest Build();
}

/// <summary>
/// ConcreteBuilder #1
/// </summary>
public class HttpWebRequestBuilder : IWebRequestBuilder
{
    private string _host;

    private string _path = string.Empty;

    private string _query = string.Empty;

    private string _scheme = "http";

    private int _port = 80;

    private int _timeout = -1;

    public IWebRequestBuilder BuildHost(string host)
    {
        _host = host;
        return this;
    }

    public IWebRequestBuilder BuildPort(int port)
    {
        _port = port;
        return this;
    }

    public IWebRequestBuilder BuildPath(string path)
    {
        _path = path;
        return this;
    }

    public IWebRequestBuilder BuildQuery(string query)
    {
        _query = query;
        return this;
    }

    public IWebRequestBuilder BuildScheme(string scheme)
    {
        _scheme = scheme;
        return this;
    }

    public IWebRequestBuilder BuildTimeout(int timeout)
    {
        _timeout = timeout;
        return this;
    }

    protected virtual void BeforeBuild(HttpWebRequest httpWebRequest) {
    }

    public WebRequest Build()
    {
        var uri = _scheme + "://" + _host + ":" + _port + "/" + _path + "?" + _query;

        var httpWebRequest = WebRequest.CreateHttp(uri);

        httpWebRequest.Timeout = _timeout;

        BeforeBuild(httpWebRequest);

        return httpWebRequest;
    }
}

/// <summary>
/// ConcreteBuilder #2
/// </summary>
public class ProxyHttpWebRequestBuilder : HttpWebRequestBuilder
{
    private string _proxy = null;

    public ProxyHttpWebRequestBuilder(string proxy)
    {
        _proxy = proxy;
    }

    protected override void BeforeBuild(HttpWebRequest httpWebRequest)
    {
        httpWebRequest.Proxy = new WebProxy(_proxy);
    }
}

/// <summary>
/// Director
/// </summary>
public class SearchRequest
{

    private IWebRequestBuilder _requestBuilder;

    public SearchRequest(IWebRequestBuilder requestBuilder)
    {
        _requestBuilder = requestBuilder;
    }

    public WebRequest Construct(string searchQuery)
    {
        return _requestBuilder
        .BuildHost("ajax.googleapis.com")
        .BuildPort(80)
        .BuildPath("ajax/services/search/web")
        .BuildQuery("v=1.0&q=" + HttpUtility.UrlEncode(searchQuery))
        .BuildScheme("http")
        .BuildTimeout(-1)
        .Build();
    }

    public string GetResults(string searchQuery) {
        var request = Construct(searchQuery);
        var resp = request.GetResponse();

        using (StreamReader stream = new StreamReader(resp.GetResponseStream()))
        {
            return stream.ReadToEnd();
        }
    }
}

class Program
{
    /// <summary>
    /// Inside both requests the same SearchRequest.Construct(string) method is used.
    /// But finally different HttpWebRequest objects are built.
    /// </summary>
    static void Main(string[] args)
    {
        var request1 = new SearchRequest(new HttpWebRequestBuilder());
        var results1 = request1.GetResults("IBM");
        Console.WriteLine(results1);

        var request2 = new SearchRequest(new ProxyHttpWebRequestBuilder("localhost:80"));
        var results2 = request2.GetResults("IBM");
        Console.WriteLine(results2);
    }
}

答案 10 :(得分:5)

我总是不喜欢Builder模式,因为它是一种笨拙,突兀,经常被经验不足的程序员滥用的东西。它是一种只有在需要从一些需要后初始化步骤的数据中组装对象时才有意义的模式(即,一旦收集了所有数据 - 用它做一些事情)。相反,在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");

现在我们有一个整洁的单一类来管理它自己的初始化,并且与构建器完成相同的工作,除了它更优雅。

答案 11 :(得分:3)

我在自家发布的消息库中使用了构建器。库核心正在从线路接收数据,用Builder实例收集它,然后,一旦Builder确定它已经拥有了创建Message实例所需的一切,Builder.GetMessage()正在使用从中收集的数据构建一个消息实例。丝。

答案 12 :(得分:2)

当我想使用XML的标准XMLGregorianCalendar来对象在Java中对DateTime进行编组时,我听到很多关于如何使用它的重量和繁琐的评论。我试图控制xs:datetime结构中的XML字段来管理时区,毫秒等。

所以我设计了一个实用程序,用于从GregorianCalendar或java.util.Date构建XMLGregorian日历。

由于我在哪里工作,我不允许在没有合法的情况下在线分享,但这里有一个客户如何使用它的例子。它抽象了细节并过滤了一些较少用于xs:datetime的XMLGregorianCalendar实现。

XMLGregorianCalendarBuilder builder = XMLGregorianCalendarBuilder.newInstance(jdkDate);
XMLGregorianCalendar xmlCalendar = builder.excludeMillis().excludeOffset().build();

当然,这个模式更像是一个过滤器,因为它将xmlCalendar中的字段设置为未定义,因此它们被排除,它仍然“构建”它。我已经轻松地向构建器添加了其他选项来创建xs:date和xs:time结构,并在需要时操作时区偏移。

如果您曾见过创建和使用XMLGregorianCalendar的代码,您会看到这样做更容易操作。

答案 13 :(得分:2)

查看InnerBuilder,这是一个IntelliJ IDEA插件,可以添加一个&#39; Builder&#39;对生成菜单(Alt + Insert)的操作,生成内部构建器类,如Effective Java

中所述

https://github.com/analytically/innerbuilder

答案 14 :(得分:0)

一个伟大的现实世界的例子是在单元测试你的类时使用。您使用sut(System Under Test)构建器。

示例:

类别:

public class CustomAuthenticationService
{
    private ICloudService _cloudService;
    private IDatabaseService _databaseService;

    public CustomAuthenticationService(ICloudService cloudService, IDatabaseService databaseService)
    {
        _cloudService = cloudService;
        _databaseService = databaseService;
    }

    public bool IsAuthorized(User user)
    {            
        //Implementation Details
        return true;

}

测试:

    [Test]
    public void Given_a_User_With_Permission_When_Verifying_If_Authorized_Then_Authorize_It_Returning_True()
    {
        CustomAuthenticationService sut = new CustomAuthenticationServiceBuilder();
        User userWithAuthorization = null;

        var result = sut.IsAuthorized(userWithAuthorization);

        Assert.That(result, Is.True);
    }

sut Builder:

public class CustomAuthenticationServiceBuilder
{
    private ICloudService _cloudService;
    private IDatabaseService _databaseService;

    public CustomAuthenticationServiceBuilder()
    {
        _cloudService = new AwsService();
        _databaseService = new SqlServerService();
    }

    public CustomAuthenticationServiceBuilder WithAzureService(AzureService azureService)
    {
        _cloudService = azureService;

        return this;
    }

    public CustomAuthenticationServiceBuilder WithOracleService(OracleService oracleService)
    {
        _databaseService = oracleService;

        return this;
    }

    public CustomAuthenticationService Build()
    {
        return new CustomAuthenticationService(_cloudService, _databaseService);
    }

    public static implicit operator CustomAuthenticationService (CustomAuthenticationServiceBuilder builder)
    {
        return builder.Build();
    }
}