建造者模式 vs 配置对象

35

建造者模式常用于创建不可变对象,但创建一个建造者需要一些编程开销。所以我想知道为什么不直接使用配置对象。

使用建造者的示例代码如下:

Product p = Product.Builder.name("Vodka").alcohol(0.38).size(0.7).price(17.99).build();

显然,这很易读且简洁,但你需要实现构造器:

public class Product {

    public final String name;
    public final float alcohol;
    public final float size;
    public final float price;

    private Product(Builder builder) {
        this.name = builder.name;
        this.alcohol = builder.alcohol;
        this.size = builder.size;
        this.price = builder.price;
    }

    public static class Builder {

        private String name;
        private float alcohol;
        private float size;
        private float price;

        // mandatory
        public static Builder name(String name) {
            Builder b = new Builder();
            b.name = name;
            return b;
        }

        public Builder alcohol(float alcohol) {
            this.alcohol = alcohol;
            return.this;
        }

        public Builder size(float size) {
            this.size = size;
            return.this;
        }

        public Builder price(float price) {
            this.price = price;
            return.this;
        }

        public Product build() {
            return new Product(this);
        }

    }

}

我的想法是,通过使用一个简单的配置对象来减少代码量,就像这样:

class ProductConfig {

        public String name;
        public float alcohol;
        public float size;
        public float price;

        // name is still mandatory
        public ProductConfig(String name) {
            this.name = name;
        }

}

public class Product {

    public final String name;
    public final float alcohol;
    public final float size;
    public final float price;

    public Product(ProductConfig config) {
        this.name = config.name;
        this.alcohol = config.alcohol;
        this.size = config.size;
        this.price = config.price;
    }

}

使用方法:

ProductConfig config = new ProductConfig("Vodka");
config.alcohol = 0.38;
config.size = 0.7;
config.price = 17.99;
Product p = new Product(config);

这种用法需要更多的代码行,但是同样非常易读。实现方式也更简单,对于不熟悉构建器模式的人来说可能更容易理解。顺便问一下:这种模式有名字吗?

我有没有忽略配置方法中的缺点?


Builder 中,您的 setter 需要 return this; 才能使模式正常工作。 - rsp
谢谢!我应该测试我的代码... - deamon
使用config对象方法,您还可以通过使用幻影类型使事物类型安全。也就是说,只有当设置了所有必要字段时,编译器才允许将config实例传递给原始构造函数。详情请见http://michid.wordpress.com/2008/08/13/type-safe-builder-pattern-in-java/。 - michid
9个回答

20

建造者模式改善了解耦 - 您的产品可以是接口,而唯一知道实现(或某些情况下的实现)的类是建造者。如果建造者也实现了一个接口,则可以将其注入到您的代码中以进一步增加解耦。

这种解耦意味着您的代码更易于维护和测试。


4
在这个例子中,我认为仍然存在耦合。调用者必须知道如何调用所有这些链接方法,如果漏掉其中的一个,则Product没有被正确初始化。产品的更改需要对生成器进行更改,需要对客户端进行更改 - 在我看来这是耦合的。 - djna
1
@djna,建造者模式用于可选参数,否则您必须使用望远镜构造函数。因此,如果您错过了一个方法,这并不是问题。强制性参数放在构建器构造函数中或作为构建方法的参数,以便您可以强制执行它们。 - atamanroman
这可能是使用构建器的最重要的理由,但实际上大家使用它主要不是因为这个原因,而是因为“不可变性”,忘记了构建器是可变的,并且出于“可读性”的考虑,这是非常主观的,仅凭这一点并不能证明其复杂性。 - johnlemon

9

正如已经指出的那样,您正在失去生成器模式的几个优点(new 不够美观、难以维护并且泄漏细节,而生成器则更加清晰)。

然而,我最缺少的一个是生成器模式可以用于提供所谓的“流畅接口”。

而不是这样:

ProductConfig config = new ProductConfig("Vodka");
config.alcohol = 0.38;
config.size = 0.7;
config.price = 17.99;
Product p = new Product(config);

你可以做:

ProductFactory.create()
    .drink("Vodka")
    .whereAlcohoolLevelIs(0.38)
    .inABottleSized(0.7)
    .pricedAt(17.99)
    .build();

并不是每个人都喜欢流畅的接口,但它们绝对是建造者模式的很好应用(所有流畅的接口都应该使用建造者模式,但并非所有建造者模式都是流畅的接口)。
一些优秀的Java集合库,如Google集合,非常自由地和非常好地使用了“流畅接口”。与你的“更易于输入/字符更少”的方法相比,我会选择这些:)

6
配置模式和构建器模式在功能上是等效的。它们都解决了相同的问题 -
- 消除多个构造函数签名的需要 - 允许字段仅在构造期间设置 - 允许消费者仅设置他们关心的值,并针对其他值使用逻辑默认值。
你可以在其中一个模式中想做的任何事情都可以在另一个模式中实现,例如仅允许使用执行验证的方法设置状态,并使用封装逻辑设置状态。唯一的区别就是你喜欢用 "new" 关键字创建对象还是喜欢调用 ".build()" 方法。

4
你尝试用什么模式解决问题?建造者模式用于具有许多(可选)参数的对象,以避免大量不同的构造函数或非常长的构造函数。它还在构造过程中保持对象处于一致状态(与JavaBean模式相对)。建造者和“配置对象”(感觉是一个好名字)之间的区别是,你仍然必须通过构造函数或getter / setter使用相同的参数创建对象。这要么无法解决构造函数问题,要么使配置对象处于不一致状态。配置对象的不一致状态并不会真正伤害它,但你可以将未完成的配置对象作为参数传递。 [Michids 链接到幻影类型似乎解决了这个问题,但这又降低了可读性(new Foo<TRUE,TRUE,TRUE,FALSE,TRUE> 稍微有些糟糕).] 这就是建造者模式的巨大优势:你可以在创建对象之前验证你的参数并且 你可以返回任何子类型(就像一个工厂)。配置对象适用于所有强制性参数集。我之前也看到过在.NET或Java中出现这种模式。

我想要不可变对象,并且我想避免使用许多参数或许多构造函数的构造函数。如果我在实际对象的构造函数中执行验证,则该对象将不会以无效状态创建。在构建器中执行验证的好处在哪里? - deamon
1
感觉更加协调。不必在实际构造函数中检查配置对象,您可以留在生成器类中并在那里执行所有与对象创建相关的操作。 此外,请不要忘记,生成器是一个高级工厂(经过时间证明)。能够返回子类型或接口值得额外的代码行数。但我同意配置对象有时可能是合适的。 - atamanroman

2

我个人认为,建造者模式一开始看起来可以为您提供更干净的代码,因为这些对象实际上被使用。另一方面,没有getter/setter将无法被许多期望驼峰命名的getter/setter的框架广泛使用。我认为这是一个严重的缺点。

我也喜欢getter/setter的原因是你清楚地看到你正在做什么:获取或设置。我觉得用建造者模式会失去一些直观的清晰度。

我知道很多人已经读过一本特定的书,现在建造者模式突然享受了热潮,就像它是新的iPhone一样。然而,我不是早期采用者。只有当它在任何领域,无论是性能、维护还是编码,真正证明节省大量时间时,我才使用“新方法”。

我的亲身经历是,我通常使用getter/setter和构造函数更好。它使我能够将这些POJO用于任何目的。

虽然我看到了您的Config对象的用途,但我也认为它比建造者模式更加繁琐,为什么呢?有什么问题吗?为什么不能用setter?

也许我们需要发明一个WITH子句:例如,假设您有

public Class FooBar() {
    private String foo;

    public void setFoo(String bar) { 
      this.foo = bar; 
    }

    public String getFoo() { 
        return this.foo; 
    }
}

public static void main(String []args) {

 FooBar fuBar = new FooBar();
 String myBar;

 with fuBar {
    setFoo("bar");
    myBar = getFoo(); 
 }
}

啊,我不知道...我认为这可能会导致更快的代码编写,而不需要所有的内部类麻烦。有人和Oracle Java大师联系吗?

使用带有构建器的对象看起来不够简洁,但可以节省构建器构建时间。而且你仍然可以将该类用作常规pojo/bean,可在框架中使用...

你们实际上喜欢这个条款吗,或者你认为它会相当糟糕? 干杯


既然你问了,我认为你的Java语言修改建议不是一个好主意。虽然我理解这在其他语言如Visual Basic中是一个已经存在的构造,但我认为使用流畅的接口/方法链提供了更多的灵活性,同时又不需要对语言本身进行任何更改。 - jacobq

2

你是否考虑过使用builder-builder

我认为带有前缀“With”的构建器更加自然流畅。


1

在我看来,如果你需要进行验证等操作,建造者模式会更加健壮。

在你的情况下,建造者模式可以改为执行以下操作:

Product p = new ProductBuilder("pName").alcohol(0.38).size(0.7).price(17.99).build();

build() 方法可以完成所有必要的验证工作,以确保您的构建器正常运行。构建器模式还具有多种设计优势(其中可能并非所有优势都适用于您的情况)。有关详细信息,请查看此 question


但是验证也可以在Product的构造函数中执行。 - deamon
@deamon 同意,依我看 Builder 更自然,而且使 API 更容易。例如,将 p = new ProductBuilder("name").price()... 与创建配置对象并分配属性等进行比较。 - naikus

0

主要的缺点是它不在Joshua的书中,所以无人能够理解。

你正在使用一个简单的值对象来保存函数(/方法/构造函数)需要的多个参数,这没有任何问题,这已经做了很长时间。只要我们没有命名的可选参数,我们就必须想出这样的解决方法 - 这很遗憾,而不是来自太阳神的一些卓越发明。

真正的区别在于你直接暴露字段。Joshua永远不会有一个公共的可变字段 - 但他编写的API将被数百万人使用,其中大部分都是白痴,而且这些API必须安全地演化几十年,他们可以花费许多人月来设计一个简单的类

我们是谁来模仿他呢?


在值对象中公开字段是完全可以的。如果你读了某些东西(例如Effective Java或者这个线程),那就要么全部读完,要么不读。有人曾经说过,有时将值对象作为参数是可以的。同时也有人说过,为什么工厂和构建器在其他情况下更优秀。这个配置对象与构建器模式完全不同,应用场景也不同。 - atamanroman
有史以来最好的回应,Java 程序员们 :) - johnlemon

-1
你不应该使用公共字段,而应该使用受保护或私有字段。为了访问它们,你应该使用getter和setter来保持封装性。

1
我只是为了让我的示例简短而使用了公共字段,但还是谢谢你。 - deamon
3
实际上,并非总是如此。如果“ProductConfig”不是公共API的一部分,那么它可以像所示的那样简单。即使在《Effective Java第二版》中,J. Bloch也说正常情况下会有这样的值对象而没有任何逻辑,其中getter/setter只会增加一些样板式的负担代码(第14项)。 - Roman
这是一个纯值类。由于验证发生在真实类的构造函数中,并且配置对象不应与其他线程共享,所以我认为这看起来很好。当然,您失去了稍后引入其他验证的能力。 除了我的同名罗马人之外:建造者模式在Effective Java SE(第2项)的第11页。 - atamanroman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接