【问题标题】:When would you use the Builder Pattern? [closed]你什么时候会使用建造者模式? [关闭]
【发布时间】:2010-09-24 14:48:57
【问题描述】:

有哪些常见的现实世界的例子使用建造者模式?它给你买了什么?为什么不直接使用工厂模式?

【问题讨论】:

标签: java design-patterns builder


【解决方案1】:

以下是争论在 Java 中使用该模式和示例代码的一些原因,但它是 Design Patterns 中的四人帮所涵盖的构建器模式的实现。您在 Java 中使用它的原因也适用于其他编程语言。

正如 Joshua Bloch 在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 个参数,它就变得难以记住所需的参数的顺序以及在给定情况下您可能需要的特定构造函数。

伸缩构造器模式的一个替代JavaBean模式,您可以在其中调用具有强制参数的构造器,然后在之后调用任何可选的设置器:

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

这里的问题是,由于对象是通过多次调用创建的,因此它在构造过程中可能处于不一致的状态。这也需要大量额外的努力来确保线程安全。 p>

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

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

【讨论】:

  • 与原来的 GOF builder 不同吧?因为没有导演班。对我来说似乎几乎是另一种模式,但我同意它非常有用。
  • 对于这个特定的例子,删除布尔参数并能够说new Pizza.Builder(12).cheese().pepperoni().bacon().build();
  • 这看起来更像Fluent Interface,而不是builder pattern
  • @Fabian Steeg,我认为人们对更好看的布尔设置器反应过度,请记住,这些设置器不允许运行时更改:Pizza.Builder(12).cheese().pepperoni().bacon().build();,你需要重新编译你的如果您只需要一些意大利辣香肠比萨饼,请编写代码或有不必要的逻辑。至少您还应该提供参数化版本,例如最初建议的@Kamikaze Mercenary。 Pizza.Builder(12).cheese(true).pepperoni(false).bacon(false).build();。再说一次,我们从不进行单元测试,对吗?
  • @JasonC 对,不可变的披萨到底有什么用?
【解决方案2】:

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

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

【讨论】:

【解决方案3】:

构建器和工厂恕我直言的主要区别在于,当您需要做很多事情来构建对象时,构建器很有用。例如想象一个 DOM。您必须创建大量节点和属性才能获得最终对象。当工厂可以在一个方法调用中轻松创建整个对象时,使用工厂。

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

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

然后这个构建器会为我吐出 HTML。这比通过大型程序方法更容易阅读。

查看Builder Pattern on Wikipedia

【讨论】:

    【解决方案4】:

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

    【讨论】:

    • 我不认为那是建造者模式。 StringBuilder 只是字符数组类(即字符串)的另一种实现,但它考虑了性能和内存管理,因为字符串是不可变的。
    • 绝对是builder模式,Java中的StringBuilder类也是如此。注意这两个类的 append() 方法是如何返回 StringBuilder 本身的,因此可以在最终调用 toString() 之前链接 b.append(...).append(...)。引用:infoq.com/articles/internal-dsls-java
    • @pohl 是的,我认为这不是真正的构建器模式,我会说这更像是一个流畅的界面。
    • “注意这两个类的 append() 方法如何返回 StringBuilder 本身”这不是 Builder 模式,这只是一个流畅的接口。它只是经常生成器也会使用流利的界面。 Builder 不一定要有流畅的界面。
    • 但是注意StringBuilder本质上是不同步的,不像StringBuffer是同步的。
    【解决方案5】:

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

    我们可以改用工厂吗?是的

    我们为什么不呢?我猜生成器更有意义。

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

    构建器一遍又一遍地构建相同类型的对象,但构建是动态的,因此可以在运行时进行更改。

    【讨论】:

      【解决方案6】:

      我一直不喜欢 Builder 模式,因为它笨拙、突兀并且经常被经验不足的程序员滥用。只有当您需要从需要初始化后步骤的某些数据中组装对象时,这种模式才有意义(即,一旦收集了所有数据 - 对其进行处理)。相反,在 99% 的情况下,构建器只是用于初始化类成员。

      在这种情况下,最好在类中简单地声明withXyz(...) 类型设置器并让它们返回对自身的引用。

      考虑一下:

      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”类首先能够提供 XMLable 产品,以及如何更改它以生成 JSONable 对象?快速回答:我不能,因为我需要使用构建器。我们绕了一圈……
      • total bs , Builder 旨在构建不可变对象,并且能够在不涉及产品类的情况下改变未来的构建方式
      • 嗯?您是否在我的答案中某处读到了 Builder 的设计用途?它是顶部问题“你什么时候使用 Builder 模式?”的另一种观点,它基于无数次滥用该模式的经验,其中更简单的东西可以更好地完成工作。如果您知道何时以及如何使用它们,所有模式都是有用的——这就是首先记录模式的重点!当模式被过度使用或更糟 - 滥用 - 然后它成为代码上下文中的反模式。唉...
      【解决方案7】:

      当您有很多选择要处理时,您会使用它。想想 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);
      

      【讨论】:

      • 我忘记阅读“如果”映射实现了构建器模式,并惊讶地看到那里的构造.. :)
      • :) 抱歉。在许多语言中返回 self 而不是 void 是很常见的。你可以用java做,但不是很常见。
      • 地图示例只是方法链接的示例。
      • @nogridbag 实际上更接近方法级联。虽然它以模拟级联的方式使用链接,所以它显然是链接,但在语义上它表现为级联。
      【解决方案8】:

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

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

      @Tetha,可以有一家意大利人经营的餐厅(框架),提供披萨。为了准备比萨,意大利人(对象生成器)使用欧文(工厂)和比萨基(基类)。

      现在印度人从意大利人手中接管了餐厅。印度餐厅 (Framework) 提供 dosa 而不是披萨。为了准备 dosa 印度人(对象构建器)使用煎锅(工厂)和 Maida(基类)

      如果你看场景,食物是不同的,食物的准备方式是不同的,但在同一家餐厅(在同一框架下)。餐厅的建造方式应该能够支持中国、墨西哥或任何菜肴。框架内的对象构建器有助于插入您想要的美食。例如

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

      【讨论】:

        【解决方案9】:

        在前面的答案(双关语)的基础上,一个出色的真实示例是 Groovy 内置对 Builders 的支持。

        Groovy Documentation 中查看Builders

        【讨论】:

          【解决方案10】:

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

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

          此外,builder 有很多种。 Kamikaze Mercenary's 给了另一个。

          【讨论】:

            【解决方案11】:
            /// <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);
                }
            }
            

            【讨论】:

            • 您可以通过两种方式改进您的答案:1) 使其成为 SSCCE。 2) 解释这是如何回答问题的。
            【解决方案12】:

            我在自制消息库中使用了构建器。库核心从线路接收数据,并使用 Builder 实例收集数据,然后,一旦 Builder 决定它已获得创建 Message 实例所需的一切, Builder.GetMessage() 正在使用从收集的数据构造消息实例电线。

            【讨论】:

              【解决方案13】:

              当我想在我的 XML 中使用标准 XMLGregorianCalendar 来对 Java 中的 DateTime 进行对象编组时,我听到了很多 cmets 关于使用它的重量和繁琐程度。我试图控制 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 的代码,您就会明白这如何使它更易于操作。

              【讨论】:

                猜你喜欢
                • 2010-09-10
                • 2010-09-20
                • 2010-09-06
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多