使用 Builder 模式的一些常见的、真实的例子是什么?它给你买了什么?为什么不直接使用工厂模式?
13 回答
以下是争论在 Java 中使用模式和示例代码的一些原因,但它是设计模式中的四人组所涵盖的构建器模式的实现。在 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);
这里的问题是,因为对象是通过多次调用创建的,所以它可能在构建过程中处于不一致的状态。这也需要付出很多额外的努力来确保线程安全。
更好的选择是使用构建器模式。
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();
这导致代码易于编写且非常易于阅读和理解。在此示例中,可以修改 build 方法以在将参数从构建器复制到 Pizza 对象后检查参数,如果提供了无效的参数值,则抛出 IllegalStateException。这种模式很灵活,将来很容易向其中添加更多参数。仅当构造函数的参数超过 4 或 5 个时,它才真正有用。也就是说,如果您怀疑将来可能会添加更多参数,那么这可能是值得的。
我从Joshua Bloch的《 Effective Java, 2nd Edition》一书中大量借鉴了这个主题。要了解有关此模式和其他有效 Java 实践的更多信息,我强烈推荐它。
考虑一家餐馆。“今天的饭菜”的创建是一种工厂模式,因为你告诉厨房“给我今天的饭菜”,厨房(工厂)根据隐藏的标准决定生成什么对象。
如果您订购定制比萨,则会出现构建器。在这种情况下,服务员告诉厨师(建造者)“我需要一个比萨饼;加奶酪、洋葱和培根!” 因此,构建器公开了生成的对象应具有的属性,但隐藏了如何设置它们。
恕我直言,构建器和工厂之间的主要区别在于,当您需要做很多事情来构建对象时,构建器很有用。例如想象一个 DOM。您必须创建大量节点和属性才能获得最终对象。当工厂可以在一个方法调用中轻松创建整个对象时,使用工厂。
使用构建器的一个示例是构建 XML 文档,例如,我在构建 HTML 片段时使用了此模型,我可能有一个构建器用于构建特定类型的表,它可能具有以下方法(未显示参数):
BuildOrderHeaderRow()
BuildLineItemSubHeaderRow()
BuildOrderRow()
BuildLineItemSubRow()
然后这个构建器会为我吐出 HTML。这比通过大型程序方法更容易阅读。
.NET StringBuilder 类是构建器模式的一个很好的例子。它主要用于通过一系列步骤创建字符串。执行 ToString() 得到的最终结果始终是一个字符串,但该字符串的创建会根据使用的 StringBuilder 类中的函数而有所不同。总而言之,基本思想是构建复杂对象并隐藏其构建方式的实现细节。
对于多线程问题,我们需要为每个线程构建一个复杂的对象。该对象表示正在处理的数据,并且可以根据用户输入而改变。
我们可以改用工厂吗?是的
为什么我们没有?我猜生成器更有意义。
工厂用于创建具有相同基本类型(实现相同接口或基类)的不同类型的对象。
构建器一遍又一遍地构建相同类型的对象,但构建是动态的,因此可以在运行时更改。
我一直不喜欢 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");
现在我们有了一个简洁的类,它管理自己的初始化,并且与构建器完成几乎相同的工作,除了它更优雅。
当您有很多选择要处理时,您会使用它。想想像 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);
在浏览 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;
}
}
在前面的答案(双关语)的基础上,一个出色的真实示例是Groovy内置的对Builders
.
构建器的另一个优点是,如果你有一个工厂,你的代码中仍然存在一些耦合,因为工厂要工作,它必须知道它可能创建的所有对象。如果添加另一个可以创建的对象,则必须修改工厂类以包含他。这也发生在抽象工厂中。
另一方面,使用构建器,您只需为这个新类创建一个新的具体构建器。导演类将保持不变,因为它在构造函数中接收构建器。
此外,还有许多口味的builder。Kamikaze Mercenary's 给了另一个。
/// <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);
}
}
我在本土消息传递库中使用了构建器。库核心从线路接收数据,并使用 Builder 实例收集数据,然后,一旦 Builder 决定它已获得创建 Message 实例所需的一切, Builder.GetMessage() 正在使用从收集的数据构造消息实例金属丝。
当我想在我的 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 的代码,您会看到这如何使它更易于操作。