这是一个有效的Java不可变类和构建器模式的实现吗?

9

建造者模式实现了Cloneable接口并重写了clone()方法,但是与其复制建造者的每个字段不同,不可变类会保留一个私有的建造者克隆。这样可以轻松地返回一个新的建造者,并创建不可变实例的稍微修改副本。

这样我就可以继续前进了。

MyImmutable i1 = new MyImmutable.Builder().foo(1).bar(2).build();
MyImmutable i2 = i1.builder().foo(3).build();

Cloneable接口被认为是有些问题的,但是这是否违反了良好的Java编程实践,这个构造中存在任何问题吗?

final class MyImmutable { 
  public int foo() { return builder.foo; }
  public int bar() { return builder.bar; }
  public Builder builder() { return builder.clone(); }
  public static final class Builder implements Cloneable {
    public Builder foo(int val) { foo = val; return this; }
    public Builder bar(int val) { bar = val; return this; }
    public MyImmutable build() { return new MyImmutable(this.clone()); }
    private int foo = 0;
    private int bar = 0;
    @Override public Builder clone() { try { return (Builder)super.clone(); } catch(CloneNotSupportedException e) { throw new AssertionError(); } }
  }
  private MyImmutable(Builder builder) { this.builder = builder; }
  private final Builder builder;
}
3个回答

6

通常,从构建器构造的类没有任何有关构建器的专业知识。也就是说,不可变类会有一个构造函数来提供foo和bar的值:

public final class MyImmutable {
  public final int foo;
  public final int bar;
  public MyImmutable(int foo, int bar) {
    this.foo = foo;
    this.bar = bar;
  }
}

生成器将是一个独立的类:
public class MyImmutableBuilder {
  private int foo;
  private int bar;
  public MyImmutableBuilder foo(int val) { foo = val; return this; }
  public MyImmutableBuilder bar(int val) { bar = val; return this; }
  public MyImmutable build() { return new MyImmutable(foo, bar); }
}

如果您愿意,您可以在 MyImmutable 构建器中添加一个静态方法来从现有的 MyImmutable 实例开始:
public static MyImmutableBuilder basedOn(MyImmutable instance) {
  return new MyImmutableBuilder().foo(instance.foo).bar(instance.bar);
}

1
我试图想省略一些步骤,避免显式地复制字段。 "basedOn" 方法确实很清晰,但它需要我再次复制字段。也许我太懒了。 - Aksel
1
基于方法的建议很棒 :) - troig

3
我以前没有见过这种方法,但看起来似乎能够很好地工作。
基本上它使得构建器模式相对简单易实现,代价是在运行时产生稍微更高的开销(额外的对象、克隆操作以及访问器函数中可能会或者不会被编译出的间接层级)。
你可能想要考虑一个潜在的变化:如果你把构建器对象本身变成不可变的,那么你就不需要进行防御性克隆了。这可能是一个整体上的胜利,特别是如果你经常构建对象而不是改变构建器的情况下。

1
将构建器设置为不可变的是一个有趣的建议,但也许我应该完全遵循 Joshua Bloch 的优秀示例,停止尝试聪明。好吧,这样说出来有点不对,显然 Joshua Bloch 非常聪明。我的意思是要遵循他优秀书籍中的第二条建议。 - Aksel
1
使用所述方法,从不同的不可变对象生成具有任意数量更改的新不可变对象将需要两个防御性副本。如果没有可变构建器,进行 N 次更改将需要 N 个防御性副本。哪种方法更好取决于要进行的更改数量。 - supercat

3

你的实现与Josh Bloch的《Effective Java 2nd Edition》中详细介绍的实现类似。

有一个争议点是你的build()方法。如果单个构建器创建一个不可变的实例,那么考虑到它的工作已经完成,允许再次使用构建器是否公平?这里需要注意的是,即使您创建了一个不可变对象,构建器的可变性也可能导致一些相当“令人惊讶”的错误。

为了纠正这一点,可以建议build方法创建实例,然后使构建器无法再次构建对象,需要一个新的构建器。尽管模板代码可能看起来很繁琐,但后面可以获得的好处超过目前所需的努力。实现类可以接收一个Builder实例作为构造函数参数,但构建器应该让实例提取所有所需的状态,释放构建器实例并保留对相关数据的最终引用。


1
《Effective Java》指出这样的好处:“一个单独的构建器可以用于构建多个对象。在对象创建之间可以调整构建器的参数以改变对象。” - slim
1
是的,没错。你不希望之前使用的构建器干扰当前正在创建的新对象。 - gpampara

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