什么时候使用构建器模式?[关闭]


530

什么是一些常见的现实世界的例子使用Builder模式的?它能买到什么?为什么不只使用工厂模式?


stackoverflow.com/questions/35238292/...提到了一些API,这些API使用生成器模式
阿里Fattahi

AaronTetha的回答确实很有帮助。这是与这些答案有关的全文
暗黑破坏神

Answers:


262

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

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

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

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

在Wikipedia上查看Builder模式


1020

以下是在Java中使用模式和示例代码的一些原因,但这是在设计模式中由四人组成的Builder模式的实现。在Java中使用它的原因也适用于其他编程语言。

如Joshua Bloch在《有效Java,第二版》中所述

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

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

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模式,您可以在其中调用带有必需参数的构造器,然后在之后调用任何可选的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,第二版》一书中大量借用了这个主题。要了解有关此模式和其他有效Java实践的更多信息,我强烈建议您使用它。


24
与原始的GOF构建器不同吗?因为没有导演班。在我看来,这几乎就像是另一种模式,但我同意它非常有用。
罗莎

194
对于此特定示例,删除布尔参数并说出更好的方法不是更好new Pizza.Builder(12).cheese().pepperoni().bacon().build();

46
这看起来更像是Fluent接口,而不是构建器模式
罗伯特·哈维

21
@Fabian Steeg,我认为人们对漂亮的布尔型设置方法反应过度,请记住,这些设置方法不允许运行时更改:Pizza.Builder(12).cheese().pepperoni().bacon().build();,如果您只需要一些意大利辣味香肠,则需要重新编译代码或使用不必要的逻辑披萨。至少您还应该提供参数化版本,例如最初建议的@Kamikaze Mercenary。Pizza.Builder(12).cheese(true).pepperoni(false).bacon(false).build();。再说一次,我们从不进行单元测试,对吗?
egallardo

23
@JasonC对,不可变的披萨有什么用?
Maarten Bodewes 2014年

325

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

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


尼丁用这个问题的另一个答案扩展了厨房类比。
2012年

19

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


9
我认为那不是建造者模式。StringBuilder只是字符数组类(即字符串)的另一种实现,但是由于字符串是不可变的,因此它考虑了性能和内存管理。
查尔斯·格雷厄姆

23
就像Java中的StringBuilder类一样,它绝对是构建器模式。请注意,这两个类的append()方法如何返回StringBuilder本身,以便b.append(...).append(...)在最终调用之前可以进行链接toString()。引文: infoq.com/articles/internal-dsls-java
波尔

3
@pohl Ya我认为这不是真正的构建器模式,我想说的只是一个流畅的界面。
Didier A.

“注意这两个类的append()方法如何返回StringBuilder本身”,不是构建器模式,而是一个流畅的接口。只是通常,构建器也将使用流畅的界面。生成器不必具有流畅的界面。
bytedev

但是请注意,与同步的StringBuffer不同,StringBuilder本质上是不同步的。
艾伦·

11

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

我们可以改用工厂吗?是

我们为什么不呢?我猜Builder更有意义。

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

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


9

当您有很多选项要处理时,可以使用它。考虑一下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);

3
我忘了阅读“ if”地图实现了构建器模式,并惊讶地看到那里的结构。.:)
sethu 2012年

3
:) 对于那个很抱歉。在许多语言中,返回自身而不是返回void是很常见的。您可以在Java中完成此操作,但这不是很常见。
达斯汀

7
映射示例仅是方法链接的示例。
nogridbag 2013年

@nogridbag实际上它将更接近方法级联。尽管它以模拟级联的方式使用链接,所以显然它是链接,但从语义上讲,它表现为级联。
Didier A.

9

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

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

@Tetha,可能有一家意大利人经营的餐厅(框架),提供比萨饼。为了准备比萨饼,意大利佬(Object Builder)使用带有比萨饼基础层的Owen(Factory)。

现在,印度人接管了意大利人的餐厅。印度餐厅(框架)服务器使用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;
   }
}

7

我始终不喜欢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对象?快速答案:我不能,因为我需要使用构建器。我们走了一圈...
David Barker

1
总而言之,Builder旨在构建不可变的对象,并具有在不改变产品类别的情况下改变未来构建方式的能力
Mickey Tin

6
嗯?您是否在回答我的某处读到了说什么是Builder的?这是对“何时使用构建器模式?”顶部问题的一种替代观点,该问题基于对模式无数次滥用的经验,在这种情况下,简单得多的方法可以更好地完成工作。如果您知道何时以及如何使用它们,那么所有模式都是有用的-首先就是记录这些模式!如果模式被过度使用或更糟(被滥用),那么它将成为代码上下文中的反模式。las ...
Pavel Lechev 2015年

6

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

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

此外,还有很多建筑商的口味。神风雇佣兵给出了另一个。


6
/// <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
您可以通过两种方法改善答案:1)将其设为SSCCE。2)解释这如何回答问题。
james.garriss 2015年


3

我在本地消息库中使用了builder。库的核心是从网络接收数据,并通过Builder实例进行收集,然后,一旦Builder确定已经拥有创建Message实例所需的一切,Builder.GetMessage()便会使用从收集到的数据构造消息实例。线。



2

当我想为我的XML使用标准XMLGregorianCalendar来反对Java中的DateTime编组时,我听到了很多关于使用它的繁重和繁琐的评论。我试图在xs:datetime结构中转换XML字段,以管理时区,毫秒等。

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

由于我在哪里工作,我无法不合法地在线共享它,但这是客户如何使用它的示例。它抽象了细节并过滤了XMLGregorianCalendar的一些实现,这些实现较少用于xs:datetime。

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

授予此模式更多的是过滤器,因为它将xmlCalendar中的字段设置为未定义,因此将其排除在外,它仍然“构建”它。我已经轻松地向构建器添加了其他选项,以创建xs:date和xs:time结构,并在需要时操纵时区偏移量。

如果您曾经看过创建和使用XMLGregorianCalendar的代码,那么您将看到它如何使它变得更容易操作。


0

在单元测试类时,可以使用一个很好的现实世界示例。您使用sut(受测系统)构建器。

例:

类:

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

在这种情况下,为什么需要一个生成器,而不是只向CustomAuthenticationService类中添加setter ?
Laur Ivan 2015年

@LaurIvan是个好问题!也许我的示例有点差劲,但是假设您无法更改CustomAuthenticationService类,那么构建器将是提高单元测试阅读率的一种有吸引力的方法。而且我认为create getters和setters将公开您的字段,仅用于测试。如果您现在想更多地了解Sut Builder,则可以阅读有关Test Data Builder的信息,这与uts几乎相同。
拉斐尔·米切利
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.