建造者模式和继承

85

我有一个对象层次结构,随着继承树的加深而变得越来越复杂。它们都不是抽象的,因此它们的所有实例都具有更或多或少复杂的目的。

由于参数数量相当高,我想使用建造者模式来设置属性,而不是编写几个构造函数。由于我需要考虑所有排列组合,所以继承树中的叶子类将具有伸缩构造函数。

在我的设计过程中遇到问题时,我在这里浏览了一些答案。首先,让我给你举一个简单的、浅显的例子来说明问题。

public class Rabbit
{
    public String sex;
    public String name;

    public Rabbit(Builder builder)
    {
        sex = builder.sex;
        name = builder.name;
    }

    public static class Builder
    {
        protected String sex;
        protected String name;

        public Builder() { }

        public Builder sex(String sex)
        {
            this.sex = sex;
            return this;
        }

        public Builder name(String name)
        {
            this.name = name;
            return this;
        }

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

public class Lop extends Rabbit
{
    public float earLength;
    public String furColour;
    
    public Lop(LopBuilder builder)
    {
        super(builder);
        this.earLength = builder.earLength;
        this.furColour = builder.furColour;
    }

    public static class LopBuilder extends Rabbit.Builder
    {
        protected float earLength;
        protected String furColour;

        public LopBuilder() { }

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

        public Builder furColour(String colour)
        {
            this.furColour = colour;
            return this;
        }

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

现在我们有一些代码可以使用了,想象一下我想要构建一个 Lop
Lop lop = new Lop.LopBuilder().furColour("Gray").name("Rabbit").earLength(4.6f);

这个调用无法编译,因为最后一个链接的调用无法解析,Builder没有定义方法earLength。因此,这种方式要求所有调用按特定顺序链接,这非常不实际,尤其是在深层次树中。

现在,在寻找答案的过程中,我发现了Subclassing a Java Builder class,它建议使用Curiously Recursive Generic Pattern。然而,由于我的层次结构不包含抽象类,所以这个解决方案对我不起作用。但是,这种方法依赖于抽象和多态性来实现,这就是为什么我认为我不能将其适应于我的需求。

我目前采用的方法是覆盖层次结构中超类Builder的所有方法,然后简单地执行以下操作:

public ConcreteBuilder someOverridenMethod(Object someParameter)
{
    super(someParameter);
    return this;
}

通过这种方法,我可以确保返回一个实例,我可以在其上发出链式调用。虽然这不像 Telescoping Anti-pattern 那样糟糕,但它是一个接近第二的模式,我认为它有点“hacky”。
是否有另一种解决我的问题的方法,我不知道吗?最好是与设计模式一致的解决方案。谢谢!

你链接的SO问题告诉你确切地如何做到这一点,在答案中没有涉及抽象类。 - Brian Roach
可能是Subclassing a Java Builder class的重复问题。 - Brian Roach
1
我已经阅读了该问题,并在我的问题中提到了它。正如我所说,由于几个缺点,那个问题没有提供答案。那个答案中没有涉及抽象类,因为它错误地应用了该模式。 - Eric Tobias
8个回答

70

使用递归界限是可以实现的,但子类型构造器也需要是泛型,并且你需要一些中间抽象类。这有点繁琐,但仍然比非泛型版本更容易。

/**
 * Extend this for Mammal subtype builders.
 */
abstract class GenericMammalBuilder<B extends GenericMammalBuilder<B>> {
    String sex;
    String name;

    B sex(String sex) {
        this.sex = sex;
        return self();
    }

    B name(String name) {
        this.name = name;
        return self();
    }

    abstract Mammal build();

    @SuppressWarnings("unchecked")
    final B self() {
        return (B) this;
    }
}

/**
 * Use this to actually build new Mammal instances.
 */
final class MammalBuilder extends GenericMammalBuilder<MammalBuilder> {
    @Override
    Mammal build() {
        return new Mammal(this);
    }
}

/**
 * Extend this for Rabbit subtype builders, e.g. LopBuilder.
 */
abstract class GenericRabbitBuilder<B extends GenericRabbitBuilder<B>>
        extends GenericMammalBuilder<B> {
    Color furColor;

    B furColor(Color furColor) {
        this.furColor = furColor;
        return self();
    }

    @Override
    abstract Rabbit build();
}

/**
 * Use this to actually build new Rabbit instances.
 */
final class RabbitBuilder extends GenericRabbitBuilder<RabbitBuilder> {
    @Override
    Rabbit build() {
        return new Rabbit(this);
    }
}

有一种避免使用“具体”叶子类的方法,比如我们有这样一个类:

class MammalBuilder<B extends MammalBuilder<B>> {
    ...
}
class RabbitBuilder<B extends RabbitBuilder<B>>
        extends MammalBuilder<B> {
    ...
}

那么您需要使用钻石创建新的实例,并在引用类型中使用通配符:

static RabbitBuilder<?> builder() {
    return new RabbitBuilder<>();
}

那能行是因为类型变量的限制确保了例如RabbitBuilder中的所有方法都具有一个带有RabbitBuilder的返回类型,即使类型参数只是通配符。

虽然如此,我并不太喜欢那种方式,因为你需要在任何地方都使用通配符,并且只能使用钻石操作符或原始类型创建新实例。我想你无论哪种方式都会遇到一些尴尬。


顺便说一下:

@SuppressWarnings("unchecked")
final B self() {
    return (B) this;
}

有一种避免未经检查的转换的方法,那就是将该方法设置为抽象方法:

abstract B self();

然后在叶子子类中覆盖它:

@Override
RabbitBuilder self() { return this; }

那样做的问题在于虽然它更加类型安全,但子类可以返回一个不同于this的东西。基本上,无论哪种方式,子类都有机会做错事情,所以我真的不认为有什么理由喜欢其中一种方法胜过另一种。


谢谢,这看起来很有前途。我应该能够在今天晚些时候测试它并提供一些反馈! - Eric Tobias
我已经实现了一个样例测试实例,看起来运行良好。目前我能看到两个缺点。首先,对于每个具体构建器的级别,我都有一个额外的抽象构建器。然而,这似乎是不可避免的。在类的构造函数中引用抽象类似乎是可以的。最后一个问题,你会说你的解决方案是CRGP的自然扩展吗? - Eric Tobias
3
额外的类有点啰嗦,但正如上面所示,你可以将“build”方法仅放在具体类上以用作组织。将构建方法放在通用类上可能意味着你会有许多无用地构建超类的方法或第二个复杂的通用系统。至于自然扩展,除了这可能是最优雅的扩展之外,我不知道该说些什么。你遇到的是模式中一个合法的问题。 - Radiodef
这似乎是个有前途的解决方案,但是我收到了一个未经检查的类型转换警告,并且底部叶子节点从我的GenericMammalBuilderRabbitBuilder的相当部分返回Object对象。 - Visionary Software Solutions
1
顺便提一下:这也是lombok在其新的实验性@SuperBuilder中使用的方法。 - Jan Rieke
显示剩余5条评论

6

面对同样的问题,我使用了emcmanus在https://community.oracle.com/blogs/emcmanus/2010/10/24/using-builder-pattern-subclasses提出的解决方案。

这里简要复述他/她偏爱的解决方案。假设我们有两个类,ShapeRectangle,其中Rectangle继承自Shape

public class Shape {

    private final double opacity;

    public double getOpacity() {
        return opacity;
    }

    protected static abstract class Init<T extends Init<T>> {
        private double opacity;

        protected abstract T self();

        public T opacity(double opacity) {
            this.opacity = opacity;
            return self();
        }

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

    public static class Builder extends Init<Builder> {
        @Override
        protected Builder self() {
            return this;
        }
    }

    protected Shape(Init<?> init) {
        this.opacity = init.opacity;
    }
}

这里有一个抽象的内部类Init,还有一个实际的实现内部类Builder。在实现Rectangle时会很有用:

public class Rectangle extends Shape {
    private final double height;

    public double getHeight() {
        return height;
    }

    protected static abstract class Init<T extends Init<T>> extends Shape.Init<T> {
        private double height;

        public T height(double height) {
            this.height = height;
            return self();
        }

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

    public static class Builder extends Init<Builder> {
        @Override
        protected Builder self() {
            return this;
        }
    }

    protected Rectangle(Init<?> init) {
        super(init);
        this.height = init.height;
    }
}

为了实例化Rectangle
new Rectangle.Builder().opacity(1.0D).height(1.0D).build();

再次,一个抽象的Init类,继承自Shape.Init,以及一个实际实现的Build。每个Builder类都会实现self方法,该方法负责返回正确转换版本的本身。

Shape.Init <-- Shape.Builder
     ^
     |
     |
Rectangle.Init <-- Rectangle.Builder

4
如果有人遇到同样的问题,我建议采用以下解决方案,符合“优先使用组合而非继承”设计模式。
父类
其主要元素是父类Builder必须实现的接口:
public interface RabbitBuilder<T> {
    public T sex(String sex);
    public T name(String name);
}

这是更改后的父类:

public class Rabbit {
    public String sex;
    public String name;

    public Rabbit(Builder builder) {
        sex = builder.sex;
        name = builder.name;
    }

    public static class Builder implements RabbitBuilder<Builder> {
        protected String sex;
        protected String name;

        public Builder() {}

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

        @Override
        public Builder sex(String sex) {
            this.sex = sex;
            return this;
        }

        @Override
        public Builder name(String name) {
            this.name = name;
            return this;
        }
    }
}

子类

子类Builder必须实现相同的接口(使用不同的泛型类型):

public static class LopBuilder implements RabbitBuilder<LopBuilder>

在子类Builder中,引用父级Builder的字段:
private Rabbit.Builder baseBuilder;

这样可以确保父级 Builder 方法在子级中被调用,但是它们的实现不同:
@Override
public LopBuilder sex(String sex) {
    baseBuilder.sex(sex);
    return this;
}

@Override
public LopBuilder name(String name) {
    baseBuilder.name(name);
    return this;
}

public Rabbit build() {
    return new Lop(this);
}

Builder的构造函数:
public LopBuilder() {
    baseBuilder = new Rabbit.Builder();
}

构建子类的构造函数:
public Lop(LopBuilder builder) {
    super(builder.baseBuilder);
}

你的实现在子类中缺少build()方法。 - Stan Mots
实际上,即使没有组合,覆盖父构建器setter方法也总是有效的。子类允许返回更具体的类型。在比较所有这些构建器实现之后,我建议简单地覆盖父setter方法。 - benez

3
我在使用构建器创建对象层次结构时,采用以下准则:
  1. 将类的构造函数至少设为 受保护,并将其用作复制构造函数,因此将其传递给类本身的实例。
  2. 使字段成为非 final private,并使用 getter 方法访问它们。
  3. 为构建器添加 包私有 setter,这也对于对象序列化框架很有用。
  4. 为每个类制作一个通用构建器,该类将具有子类构建器。此构建器已经包含了当前类的setter方法,但我们还会创建第二个非通用构建器,该构建器包含构造函数和构建方法。
  5. 构建器不会拥有任何字段。相反,处于层次结构顶部的通用构建器将包含用于构建具体对象的通用字段。
Rabbit 将如下所示:
public class Rabbit {

    // private non-final fields
    private String sex;
    private String name;

    // copy constructor
    Rabbit(Rabbit rabbit) {
        sex = rabbit.sex;
        name = rabbit.name;
    }

    // no-arg constructor for serialization and builder
    Rabbit() {}

    // getter methods

    public final String getSex() {
        return sex;
    }

    public final String getName() {
        return name;
    }

    // package private setter methods, good for serialization frameworks

    final void setSex(String sex) {
        this.sex = sex;
    }

    final void setName(String name) {
        this.name = name;
    }

    // create a generic builder for builders that have subclass builders
    abstract static class RBuilder<R extends Rabbit, B extends RBuilder<R, B>> {

        // the builder creates the rabbit
        final R rabbit;

        // here we pass the concrete subclass that will be constructed
        RBuilder(R rabbit) {
            this.rabbit = rabbit;
        }

        public final B sex(String sex) {
            rabbit.setSex(sex);
            return self();
        }

        public final B name(String name) {
            rabbit.setName(name);
            return self();
        }

        @SuppressWarnings("unchecked")
        final B self() {
            return (B) this;
        }

    }

    // the builder that creates the rabbits
    public static final class Builder extends RBuilder<Rabbit, Builder> {

        // creates a new rabbit builder
        public Builder() {
            super(new Rabbit());
        }

        // we could provide a public copy constructor to support modifying rabbits
        public Builder(Rabbit rabbit) {
            super(new Rabbit(rabbit));
        }

        // create the final rabbit
        public Rabbit build() {
            // maybe make a validate method call before?
            return new Rabbit(rabbit);
        }
    }
}

还有我们的Lop

public final class Lop extends Rabbit {

    // private non-final fields
    private float earLength;
    private String furColour;

    // copy constructor
    private Lop(Lop lop) {
        super(lop);
        this.earLength = lop.earLength;
        this.furColour = lop.furColour;
    }

    // no-arg constructor for serialization and builder
    Lop() {}

    // getter methods

    public final float getEarLength() {
        return earLength;
    }

    public final String getFurColour() {
        return furColour;
    }

    // package private setter methods, good for serialization frameworks

    final void setEarLength(float earLength) {
        this.earLength = earLength;
    }

    final void setFurColour(String furColour) {
        this.furColour = furColour;
    }

    // the builder that creates lops
    public static final class Builder extends RBuilder<Lop, Builder> {

        public Builder() {
            super(new Lop());
        }

        // we could provide a public copy constructor to support modifying lops
        public Builder(Lop lop) {
            super(new Lop(lop));
        }

        public final Builder earLength(float length) {
            rabbit.setEarLength(length);
            return self(); // this works also here
        }

        public final Builder furColour(String colour) {
            rabbit.setFurColour(colour);
            return self();
        }

        public Lop build() {
            return new Lop(rabbit);
        }
    }
}

优点:

  • 使用单个派生类来精确地复制您的类的对象层次结构,以建造当前类的对象。不需要创建人工父级。
  • 该类没有依赖于它的构建器。它只需要自己的一个实例来复制字段,这对于替代工厂可能很有用。
  • 这些类非常适合与JSON或Hibernate等序列化框架一起使用,因为它们通常需要存在getter和setter。例如,Jackson可以很好地使用包私有 setter。
  • 构建器中不需要重复字段。构建器包含要构造的对象。
  • 子类型构建器中不需要覆盖setter方法,因为直接父类是通用的。
  • 内置支持复制构造函数,允许创建实例的修改版本,使对象“有点像不可变”。

缺点:

  • 至少需要一个额外的通用构建器。
  • 字段不是final,因此将它们设为public是不安全的。
  • 类本身需要附加的setter方法,以便从构建器中调用。

让我们创建一些兔子..

@Test
void test() {
    // creating a rabbit
    Rabbit rabbit = new Rabbit.Builder() //
            .sex("M")
            .name("Rogger")
            .build();

    assertEquals("M", rabbit.getSex());

    // create a lop
    Lop lop = new Lop.Builder() //
            .furColour("Gray")
            .name("Rabbit")
            .earLength(4.6f)
            .build();

    // modify only the name of the lop
    lop = new Lop.Builder(lop) //
            .name("Lop")
            .build();

    assertEquals("Gray", lop.getFurColour());
    assertEquals("Lop", lop.getName());
}

2

我做了一些实验,发现这对我非常有效。请注意,我更喜欢在开始时创建实际实例并调用该实例上的所有设置器。这只是我的个人偏好。

与接受的答案主要的区别在于:

  1. 我传递一个指示返回类型的参数
  2. 不需要Abstract...和final builder
  3. 我创建了一个'newBuilder'便捷方法。

代码如下:

public class MySuper {
    private int superProperty;

    public MySuper() { }

    public void setSuperProperty(int superProperty) {
        this.superProperty = superProperty;
    }

    public static SuperBuilder<? extends MySuper, ? extends SuperBuilder> newBuilder() {
        return new SuperBuilder<>(new MySuper());
    }

    public static class SuperBuilder<R extends MySuper, B extends SuperBuilder<R, B>> {
        private final R mySuper;

        public SuperBuilder(R mySuper) {
            this.mySuper = mySuper;
        }

        public B withSuper(int value) {
            mySuper.setSuperProperty(value);
            return (B) this;
        }

        public R build() {
            return mySuper;
        }
    }
}

然后一个子类看起来像这样:

public class MySub extends MySuper {
    int subProperty;

    public MySub() {
    }

    public void setSubProperty(int subProperty) {
        this.subProperty = subProperty;
    }

    public static SubBuilder<? extends MySub, ? extends SubBuilder> newBuilder() {
        return new SubBuilder(new MySub());
    }

    public static class SubBuilder<R extends MySub, B extends SubBuilder<R, B>>
        extends SuperBuilder<R, B> {

        private final R mySub;

        public SubBuilder(R mySub) {
            super(mySub);
            this.mySub = mySub;
        }

        public B withSub(int value) {
            mySub.setSubProperty(value);
            return (B) this;
        }
    }
}

以及一个子子类

public class MySubSub extends MySub {
    private int subSubProperty;

    public MySubSub() {
    }

    public void setSubSubProperty(int subProperty) {
        this.subSubProperty = subProperty;
    }

    public static SubSubBuilder<? extends MySubSub, ? extends SubSubBuilder> newBuilder() {
        return new SubSubBuilder<>(new MySubSub());
    }

    public static class SubSubBuilder<R extends MySubSub, B extends SubSubBuilder<R, B>>
        extends SubBuilder<R, B> {

        private final R mySubSub;

        public SubSubBuilder(R mySub) {
            super(mySub);
            this.mySubSub = mySub;
        }

        public B withSubSub(int value) {
            mySubSub.setSubSubProperty(value);
            return (B)this;
        }
    }

}

为了验证它是否完全正常工作,我使用了以下测试:
MySubSub subSub = MySubSub
        .newBuilder()
        .withSuper (1)
        .withSub   (2)
        .withSubSub(3)
        .withSub   (2)
        .withSuper (1)
        .withSubSub(3)
        .withSuper (1)
        .withSub   (2)
        .build();

由于您的通用构建器已经具有类型为R的通用字段,因此无需在子类构建器中复制该字段。 - benez

2

这个表单似乎基本可用。虽然不太整洁,但它看起来可以避免你的问题:

class Rabbit<B extends Rabbit.Builder<B>> {

    String name;

    public Rabbit(Builder<B> builder) {
        this.name = builder.colour;
    }

    public static class Builder<B extends Rabbit.Builder<B>> {

        protected String colour;

        public B colour(String colour) {
            this.colour = colour;
            return (B)this;
        }

        public Rabbit<B> build () {
            return new Rabbit<>(this);
        }
    }
}

class Lop<B extends Lop.Builder<B>> extends Rabbit<B> {

    float earLength;

    public Lop(Builder<B> builder) {
        super(builder);
        this.earLength = builder.earLength;
    }

    public static class Builder<B extends Lop.Builder<B>> extends Rabbit.Builder<B> {

        protected float earLength;

        public B earLength(float earLength) {
            this.earLength = earLength;
            return (B)this;
        }

        @Override
        public Lop<B> build () {
            return new Lop<>(this);
        }
    }
}

public class Test {

    public void test() {
        Rabbit rabbit = new Rabbit.Builder<>().colour("White").build();
        Lop lop1 = new Lop.Builder<>().earLength(1.4F).colour("Brown").build();
        Lop lop2 = new Lop.Builder<>().colour("Brown").earLength(1.4F).build();
        //Lop.Builder<Lop, Lop.Builder> builder = new Lop.Builder<>();
    }

    public static void main(String args[]) {
        try {
            new Test().test();
        } catch (Throwable t) {
            t.printStackTrace(System.err);
        }
    }
}

虽然我已经成功构建了RabbitLop(两种形式),但我现阶段无法弄清楚如何实例化一个Builder对象并使其具有完整的类型。

该方法的本质依赖于Builder方法中对(B)的转换。这允许您定义对象的类型和Builder的类型,并在构建对象时保留它们。

如果有人能够找出正确的语法(此处为错误语法),我将不胜感激。

Lop.Builder<Lop.Builder> builder = new Lop.Builder<>();

谢谢你的回答。我建议你尝试从构建器声明中分别删除“R”和“L”。这样应该仍然可以工作并解决你的问题。 - Eric Tobias
@EricTobias - 你是对的!代码已更改。我仍然无法弄清如何创建一个“Builder”并将其分配给一个变量。 - OldCurmudgeon
你的构建器被声明为静态的,你不应该能够实例化它! ;) - Eric Tobias
你可以实例化static内部类。在这种情况下,static表示它没有对其父类/对象的引用。 - OldCurmudgeon
没错,是我的问题。至于语法,看起来是正确的。如果你想的话,可以开一个问题并发布你的错误信息!;) - Eric Tobias
不幸的是,您还将“Rabbit”和“Lop”类泛型化,仅支持“Builder”。 - benez

2
以下IEEE会议论文《Java中的精细化流畅构建器》提供了一个全面的解决方案。它将原始问题分解为继承缺陷准不变性两个子问题,并展示了如何通过解决这两个子问题,在Java经典构建器模式中开启继承支持和代码重用。请参考Refined Fluent Builder in Java

1
由于您不能使用泛型,现在可能的主要任务是以某种方式放宽类型限制。我不知道您如何处理这些属性,但如果您使用HashMap将它们存储为键值对,那么在构建器中只需一个set(key, value)包装方法(或者可能不再需要构建器)。
缺点是在处理存储的数据时需要进行额外的类型转换。
如果这种情况太宽松,那么您可以保留现有的属性,但具有通用的set方法,该方法使用反射并根据“key”名称搜索setter方法。虽然我认为反射可能过度。

嗯,我可以使用泛型。只是我不能使用似乎在大多数情况下都有效的提供的解决方案!;) - Eric Tobias

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