关于java:你什么时候使用Builder模式?

When would you use the Builder Pattern?

使用构建器模式的一些常见的真实例子是什么?它给你买了什么?为什么不使用工厂模式呢?


下面是在Java中使用模式和示例代码的一些理由,但它是由设计模式中的四族所覆盖的生成器模式的实现。在Java中使用它的原因也适用于其他编程语言。

正如Joshua Bloch在有效Java中所描述的那样,第二版:

The builder pattern is a good choice when designing classes whose constructors or static factories would have more than a handful of parameters.

我们都曾在某个时刻遇到过一个类,其中包含一系列构造函数,每次添加都会添加一个新的选项参数:

1
2
3
4
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:

1
2
3
4
Pizza pizza = new Pizza(12);
pizza.setCheese(true);
pizza.setPepperoni(true);
pizza.setBacon(true);

这里的问题是,因为对象是通过几个调用创建的,所以在构造过程中可能处于不一致的状态。这也需要额外的努力来确保线程安全。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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是不可变的,参数值都在一个位置。因为构建器的setter方法返回构建器对象,所以它们可以被链接。

1
2
3
4
5
Pizza pizza = new Pizza.Builder(12)
                       .cheese(true)
                       .pepperoni(true)
                       .bacon(true)
                       .build();

这就产生了易于编写、易于阅读和理解的代码。在本例中,可以修改build方法以在参数从生成器复制到pizza对象后检查参数,如果提供的参数值无效,则抛出illegalStateException。这种模式是灵活的,很容易在将来添加更多的参数。只有当一个构造函数的参数超过4或5个时,它才真正有用。也就是说,如果您怀疑将来可能会添加更多的参数,那么首先可能是值得的。

我从Joshua Bloch的第二版《有效Java》中大量借用了这个主题。为了更好地了解这种模式和其他有效的Java实践,我强烈推荐它。


考虑一家餐馆。"今天的饭"的创建是一个工厂模式,因为你告诉厨房"得到今天的饭",而厨房(工厂)根据隐藏的标准决定要生成什么对象。

如果您订购定制的比萨饼,将显示构建器。在这种情况下,服务员告诉厨师(建筑商)"我需要一个比萨饼,加上奶酪、洋葱和培根!"因此,生成器公开生成的对象应该具有的属性,但隐藏了如何设置这些属性。


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

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

1
2
3
4
BuildOrderHeaderRow()
BuildLineItemSubHeaderRow()
BuildOrderRow()
BuildLineItemSubRow()

然后这个构建器会为我输出HTML。这是更容易阅读,然后通过一个大型的过程方法。

查看维基百科上的构建器模式。


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


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

我们可以用工厂代替吗?是的

为什么我们没有?我想建设者更有道理。

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

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


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

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

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

现在印度佬从意大利佬手中接管了这家餐馆。印度餐厅(framework)的服务器是dosa而不是pizza。为了准备Dosa Indian Guy(Object Builder)使用煎锅(工厂)和Maida(基类)

如果你看场景,食物是不同的,食物的准备方式是不同的,但在同一个餐厅(在同一个框架下)。餐厅的建造方式应能支持中国菜、墨西哥菜或任何菜肴。框架内的对象生成器可以方便地插入您想要的菜肴。例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
   }
}


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

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

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

还有XML构建、字符串构建和许多其他功能。想象一下,如果以东十一〔0〕是一个建设者。你可以这样做:

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


基于前面的答案(pun的意图),一个很好的现实例子是groovy对Builders的内置支持。

  • 使用groovy的MarkupBuilder创建XML
  • 使用groovy的StreamingMarkupBuilder创建XML
  • 摆动生成器
  • SwingXBuilder

请参见groovy文档中的构建器


我一直不喜欢构建器模式是一种笨拙、突兀、经常被经验不足的程序员滥用的模式。它是一种模式,只有当您需要从一些需要后初始化步骤的数据中组装对象时才有意义(即一旦收集了所有数据,就用它做点什么)。相反,在99%的时间里,生成器只是用来初始化类成员。

在这种情况下,最好只在类内声明withXyz(...)类型的setter,并使它们返回对自身的引用。

考虑一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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");

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


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/// <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);
    }
}


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

另一方面,对于生成器,您只需为这个新类创建一个新的具体生成器。Director类将保持不变,因为它在构造函数中接收构建器。

此外,还有许多建筑风格。神风雇佣兵给了另一个。


我在自制的信息库中使用了builder。库核心从网络中接收数据,并使用Builder实例收集数据,然后,一旦Builder确定它已获得创建消息实例所需的一切,builder.getMessage()将使用从网络中收集的数据构建消息实例。


签出InnerBuilder,一个INTERLIJ思想插件,它为生成菜单添加了一个"生成器"动作(ALT+INSERT),它生成了一个内置的Builder类,如在有效Java中描述的

https://github.com/analytically/innerbuilder


当我想使用标准XMLGRGORIGANCANDARAR使我的XML在爪哇中对DATETIME进行编组时,我听到了很多关于使用它的重量和繁琐的评论。我试图控制xs:datetime结构中的XML字段以管理时区、毫秒等。

因此,我设计了一个实用程序来从公历或java.util.date构建一个xmlGregorian日历。

因为我在哪里工作,我不允许在没有法律的情况下在网上共享它,但是这里有一个客户如何使用它的例子。它提取细节并过滤一些不太用于xs:datetime的xmlGregorianCalendar实现。

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

假设这个模式更多的是一个过滤器,因为它将xmlcalendar中的字段设置为未定义的,所以它们被排除在外,它仍然"构建"它。我已经轻松地将其他选项添加到生成器中,以创建xs:date和xs:time结构,并在需要时操作时区偏移。

如果您曾经看到过创建和使用xmlGregorianCalendar的代码,那么您将看到这是如何使操作更加容易的。


一个很好的现实例子是在单元测试类时使用。您使用SUT(测试中的系统)构建器。

例子:

班级:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;

}

测试:

1
2
3
4
5
6
7
8
9
10
    [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生成器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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();
    }
}