【问题标题】:Builder Pattern and Inheritance生成器模式和继承
【发布时间】:2014-01-31 21:52:15
【问题描述】:

我有一个对象层次结构,它随着继承树的加深而增加复杂性。这些都不是抽象的,因此,它们的所有实例都服务于一个或多或少复杂的目的。

由于参数的数量非常多,我想使用构建器模式来设置属性,而不是编写多个构造函数。由于我需要满足所有排列,我的继承树中的叶类将具有伸缩构造函数。

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

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;
}

通过这种方法,我可以确保返回一个可以发出链调用的实例。虽然这没有伸缩反模式那么糟糕,但它紧随其后,我认为它有点“hacky”。​​

是否有其他我不知道的问题的解决方案?最好是符合设计模式的解决方案。谢谢!

【问题讨论】:

  • 您链接到的 SO 问题确切地告诉您如何执行此操作,并且答案中没有任何地方涉及抽象类。
  • 我已经阅读了上述问题,甚至在我的问题中提到了它。正如我所说,由于几个缺点,这个问题没有提供答案。该答案不涉及抽象类,因为它错误地应用了模式。

标签: java design-patterns inheritance builder


【解决方案1】:

在使用构建器创建对象层次结构时,我采用了以下准则:

  1. 使类的构造函数至少受保护并将其用作复制构造函数,从而将类本身的实例传递给它。
  2. 将字段设为非最终私有并使用 getter 访问它们。
  3. 为构建器添加 package private 设置器,这对于对象序列化框架也很好。
  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 与 package private 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】:

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

    我只是在这里复制他/她的首选解决方案。假设我们有两个类,ShapeRectangleRectangle 继承自 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
    

    【讨论】:

      【解决方案3】:

      以下 IEEE 会议投稿Refined Fluent Builder in Java 给出了该问题的全面解决方案。

      它将原始问题分解为 继承缺陷准不变性 两个子问题,并展示了这两个子问题的解决方案如何通过代码打开继承支持在 Java 中的经典构建器模式中重用。

      【讨论】:

        【解决方案4】:

        使用递归绑定当然可以,但是子类型构建器也需要是通用的,并且您需要一些临时抽象类。有点麻烦,不过还是比非泛型的版本简单。

        /**
         * 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,即使类型参数只是一个通配符。

        不过,我不太喜欢这种做法,因为您需要在任何地方使用通配符,而且您只能使用菱形或raw type 创建一个新实例。我想你最终会有点尴尬。


        顺便说一下,关于这个:

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

        有一种方法可以避免这种未经检查的强制转换,即使方法抽象:

        abstract B self();
        

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

        @Override
        RabbitBuilder self() { return this; }
        

        这样做的问题是,虽然它更安全,但子类可以返回 this 以外的东西。基本上,无论哪种方式,子类都有机会做错事,所以我真的看不出有什么理由更喜欢其中一种方法。

        【讨论】:

        • 谢谢,这看起来很有希望。我应该能够在今天晚些时候对其进行测试并提供一些反馈!
        • 我已经实现了一个示例测试实例化,它似乎工作正常。我现在可以看到两个缺点。首先,对于每个级别的具体构建器,我都有一个额外的抽象构建器。然而,这似乎是不可避免的。在类的构造函数中引用抽象类似乎很好。最后一个问题,您认为您的解决方案是 CRGP 的自然扩展吗?
        • 额外的类有点冗长,但如上所示,您可以通过将“build”方法仅放在具体类上来将其用作组织。将 build 方法放在泛型类上要么意味着您将拥有许多无用地构建超类的方法,要么意味着第二个复杂的泛型系统。至于自然扩展,除了这可能是最优雅的扩展之外,不知道该说什么。您遇到的是该模式的合法问题。
        • 谢谢,非常感谢!
        • 顺便说一句:这也是lombok uses with its the new experimental @SuperBuilder的方法。
        【解决方案5】:

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

        与公认答案的主要区别在于

        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 类型的通用字段,因此无需在子类构建器中复制该字段
        【解决方案6】:

        如果还有人遇到同样的问题,我建议以下解决方案,它符合“优先组合优于继承”设计模式。

        父类

        它的主要元素是父类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 中引用 parentBuilder 的字段:

        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() 方法。
        • 实际上覆盖父构建器设置方法将始终有效,即使没有组合。允许子类返回更具体的类型。在比较了所有这些构建器实现之后,我建议简单地覆盖父 setter 方法。
        【解决方案7】:

        这种形式似乎几乎可以工作。它不是很整洁,但看起来它避免了你的问题:

        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<>();
        

        【讨论】:

        • 感谢您的回答。我建议您尝试从 builder 的声明中分别删除 RL。它应该仍然可以工作并解决您的问题。
        • @EricTobias - 你是对的!代码已更改。我仍然不知道如何创建 Builder 并将其分配给变量。
        • 您的构建器被声明为静态的,您不应该实际实例化它! ;)
        • 可以实例化static内部类。 static 在这种情况下意味着它没有引用它的父类/对象。
        • 没错,我的错。至于语法,它看起来是正确的。如果您愿意,请打开一个问题并发布您的错误消息! ;)
        【解决方案8】:

        由于您不能使用泛型,所以现在的主要任务可能是以某种方式放松输入。 我不知道你之后如何处理这些属性,但是如果你使用 HashMap 将它们存储为键值对呢?因此,构建器中将只有一个 set(key, value) 包装器方法(或者可能不再需要构建器)。

        缺点是在处理存储的数据时需要额外的类型转换。

        如果这种情况过于松散,那么你可以保留现有的属性,但有一个通用的set方法,它使用反射并根据'key'名称搜索setter方法。虽然我认为反思会有点过头了。

        【讨论】:

        • 好吧,我可以使用泛型。我只是无法使用在大多数情况下似乎有效的提供的解决方案! ;)
        猜你喜欢
        • 1970-01-01
        • 2013-07-07
        • 1970-01-01
        • 1970-01-01
        • 2023-03-21
        • 2015-09-12
        • 2015-02-09
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多