Java建造者模式和“深层”对象层次结构

15
什么是在“深层”对象层次结构中使用Builder模式的最佳实践?更具体地说,我探索了Joshua Bloch提出的Builder模式,并将其应用于我的XML绑定代码(我正在使用SimpleXML,但此问题适用于任何情况)。我的对象层次结构有4个层级,复杂程度各不相同。我的意思是,在某些层面上,我的对象只有几个属性,而在其他层面上,我的对象可能多达10个。因此,考虑以下假设示例(为简洁起见,我省略了Simple XML注释)。
public class Outermost {

    private String title;
    private int channel;
    private List<Middle> middleList;

}

class Middle{
    private int id;
    private String name;
    private boolean senior;
    /* ... ... 10 such properties */

    private Innermost inner;
}

class Innermost{
    private String something;
    private int foo;
    /* ... Few more of these ..*/
}
如果我想使用构建器强制创建 Outermost 对象,那么最好的方法是什么?最明显的答案是为每个以上类别都创建一个 inner static Builder 类。但是,这难道不会使事情变得像Builder模式试图解决的问题一样笨重吗?我考虑到一些东西 - 这将强制采用“自内向外”的方法 - 意味着在可以将 Innermost 对象添加到 Middle 对象之前,必须完全构建和实例化它。但我们都知道,在实践中(特别是当我们正在构建XML或JSON时),我们很少有“及时”的信息可以完成这项任务。有可能,人们最终会为每个属性都拥有变量-跨越所有级别;并在最后创建对象。要么,人们最终会在代码中漂浮着多层级别的Builder,增加了混乱。那么,有没有关于如何优雅地完成这个任务的想法呢?

如果您正在使用构建器模式,则应将所有私有字段设为final。在我看来,这是最好的实践。 - Adam Gent
@AdamGent 感谢您指出这一点。它们在我的应用程序中确实是final的。在此发布问题时不知道怎么错过了这一点。 - curioustechizen
3个回答

6
这里需要翻译的内容是:

建造者模式的描述在这里,我猜这就是你指的内容;它与维基百科这里描述的模式有点不同,我更喜欢前者。

我没看出你所关注的构造顺序或封装丧失的问题是必然遵循我所读到的描述的。对我来说,最大的问题是原始数据的结构。

假设我们有:

 public OuterBuilder {
     // some outer attributes here

     private ArrayList<MiddleBuilder> m_middleList;

     public OuterBuild( mandatory params for Outers ){
          // populate some outer attributes
          // create empty middle array
     }

     public addMiddle(MiddleBuilder middler) {
              m_middleList.add(middler);
     } 
 }

现在我们可以创建任意数量的middleBuilders。
 while (middleDataIter.hasNext() ) {
      MiddleData data = middleDateIter.next();
      // make a middle builder, add it.
 }

我们可以将相同的模式应用到更深层次的嵌套中。
针对您提出的第一个问题,每个属性都有一个变量:这取决于我们如何设计构建器以及我们的数据来自何处。如果我们从UI中获取数据,那么每个属性都有一个变量,我们不会更糟糕。如果按照我上面的建议,我们正在迭代某些数据结构,那么可能是构建器负责解释该数据结构。在我的示例中,我们传递MiddleData实例。有一些额外的耦合,但它确实封装了细节。
针对您提出的第二个问题,我们不会边构建边进行,相反,我们有效地使用构建器作为数据累加点。最终,我们调用“Go and Build”方法,但此时我们应该已经准备好所有数据,因此整个层次结构就会构建完成。

那很详细,那也很快!好吧,我确实是指您指出的Builder的前一种描述。但我不确定我理解您在这里建议什么。您是说我有一个Builder对象层次结构,与我的类层次结构相似吗?也许对此的答案将帮助我理解为什么您认为“Go and Build”不需要从内到外。 - curioustechizen
1
是的,我考虑到了外部构建器知道中间构建器,中间构建器知道内部构建器。我认为你的担忧是我们可能会按某种顺序接收数据,这将不允许我们从外向内构建。因此,对于数据解释,我声称我们可以按任何顺序构建构建器,最有可能是从外向内,并根据需要在构建器中累积数据。在某个时刻,我们拥有了所有需要的东西,然后告诉外部开始构建,外部递归地向内部工作。因此,最内层的对象很可能首先被构建。 - djna
是的 - 我现在明白你的观点了。我将尝试这种方法并在此更新我的发现。 - curioustechizen

5
可以做,但这可能并不值得。明显的实现方法是...
class Shape
{
    private final double opacity;

    public double getOpacity()
    {
        return opacity;
    }

    public static abstract class Builder<T extends Shape> {

        private double opacity;

        public Builder<T> opacity(double opacity) {
            this.opacity = opacity;
            return this;
        }

        public abstract T build();
    }

    public static Builder<?> builder() {
        return new Builder<Shape>()
            {
                @Override
                    public Shape build()
                {
                    return new Shape(this);
                }
            };
    }

    protected Shape(Builder<?> builder) {
        this.opacity = builder.opacity;
    }
}

class Rectangle extends Shape {

    private final double height;
    private final double width;

    public double getHeight()
    {
        return height;
    }

    public double getWidth()
    {
        return width;
    }

    public static abstract class Builder<T extends Rectangle> extends Shape.Builder<T> {
        private double height;
        private double width;

        public Builder<T> height(double height) {
            this.height = height;
            return this;
        }

        public Builder<T> width(double width) {
            this.width = width;
            return this;
        }
    }

    public static Builder<?> builder() {
        return new Builder<Rectangle>()
            {
                @Override
                    public Rectangle build()
                {
                    return new Rectangle(this);
                }
            };
    }

    protected Rectangle(Builder<?> builder) {
        super(builder);
        this.height = builder.height;
        this.width = builder.width;
    }
}

快速运行遇到了问题。如果你尝试这样做:
Rectangle r = Rectangle.builder().opacity(0.5).height(50).width(100).build();

这段代码无法编译,因为opacity()不知道它正在返回一个Rectangle.Builder,只知道它返回一个Shape.Builder<Rectangle>。因此,您必须按最终派生类到最低派生类的顺序调用属性:

Rectangle r = Rectangle.builder().height(50).width(100).opacity(0.5).build();

如果您想绕过这个问题,需要将属性方法变成通用的,这样超类方法仍将返回子类构建器。据我所知,没有百分之百可靠的方法,但通过一些自引用的泛型可以接近实现:

class Shape
{
    private final double opacity;

    public double getOpacity ()
    {
        return opacity;
    }

    public static abstract class ShapeBuilder<S extends Shape, B extends ShapeBuilder<S, B>>
    {

        private double opacity;

        @SuppressWarnings( "unchecked" )
        public B opacity ( double opacity )
        {
            this.opacity = opacity;
            return (B) this;
        }

        public abstract S build ();
    }

    private static class DefaultShapeBuilder extends ShapeBuilder<Shape, DefaultShapeBuilder>
    {
        @Override
        public Shape build ()
        {
            return new Shape( this );
        }
    }

    public static ShapeBuilder<?, ?> builder ()
    {
        return new DefaultShapeBuilder();
    }

    protected Shape ( ShapeBuilder<?, ?> builder )
    {
        this.opacity = builder.opacity;
    }
}

class Rectangle extends Shape
{

    private final double height;
    private final double width;

    public double getHeight ()
    {
        return height;
    }

    public double getWidth ()
    {
        return width;
    }

    public static abstract class RectangleBuilder<S extends Rectangle, B extends RectangleBuilder<S, B>> extends ShapeBuilder<S, B>
    {
        private double height;
        private double width;

        @SuppressWarnings( "unchecked" )
        public B height ( double height )
        {
            this.height = height;
            return (B) this;
        }

        @SuppressWarnings( "unchecked" )
        public B width ( double width )
        {
            this.width = width;
            return (B) this;
        }
    }

    public static RectangleBuilder<?, ?> builder ()
    {
        return new DefaultRectangleBuilder();
    }

    protected Rectangle ( RectangleBuilder<?, ?> builder )
    {
        super( builder );
        this.height = builder.height;
        this.width = builder.width;
    }

    private static class DefaultRectangleBuilder extends RectangleBuilder<Rectangle, DefaultRectangleBuilder>
    {
        @Override
        public Rectangle build ()
        {
            return new Rectangle( this );
        }
    }
}

class RotatedRectangle extends Rectangle
{
    private final double theta;

    public double getTheta ()
    {
        return theta;
    }

    public static abstract class RotatedRectangleBuilder<S extends RotatedRectangle, B extends RotatedRectangleBuilder<S, B>> extends Rectangle.RectangleBuilder<S, B>
    {
        private double theta;

        @SuppressWarnings( "Unchecked" )
        public B theta ( double theta )
        {
            this.theta = theta;
            return (B) this;
        }
    }

    public static RotatedRectangleBuilder<?, ?> builder ()
    {
        return new DefaultRotatedRectangleBuilder();
    }

    protected RotatedRectangle ( RotatedRectangleBuilder<?, ?> builder )
    {
        super( builder );
        this.theta = builder.theta;
    }

    private static class DefaultRotatedRectangleBuilder extends RotatedRectangleBuilder<RotatedRectangle, DefaultRotatedRectangleBuilder>
    {
        @Override
        public RotatedRectangle build ()
        {
            return new RotatedRectangle( this );
        }
    }
}

class BuilderTest
{
    public static void main ( String[] args )
    {
        RotatedRectangle rotatedRectangle = RotatedRectangle.builder()
                .theta( Math.PI / 2 )
                .width( 640 )
                .height( 400 )
                .height( 400 )
                .opacity( 0.5d ) // note attribs can be set in any order
                .width( 111 )
                .opacity( 0.5d )
                .width( 222 )
                .height( 400 )
                .width( 640 )
                .width( 640 )
                .build();
        System.out.println( rotatedRectangle.getTheta() );
        System.out.println( rotatedRectangle.getWidth() );
        System.out.println( rotatedRectangle.getHeight() );
        System.out.println( rotatedRectangle.getOpacity() );
    }
}

请注意@SuppressWarnings注释; 如果一个子类违反了FooBuilder总是扩展FooSuperclassBuilder<Foo,FooBuilder>的约定,则系统会崩溃。
而且你可以看到代码变得多么丑陋。此时也许最好放弃条款2,转而冥想条款16:优先选择组合而非继承

3
你的回答非常详细,但我认为你误解了原始问题。我不是要将 Builder 模式应用于继承树。相反,我需要解决的问题是当一个对象由其他非原始字段组成时应用 Builder 模式。问题中的示例就是这样。 - curioustechizen

1
如果您使用JAXB从XML模式生成代码,那么jaxb2-rich-contract-plugin的“fluent-builder”插件会对您有所帮助。它生成一个深度构建器模式,您可以将构建器链接在一起,并使用“end()”方法完成嵌套对象的构建并返回其父级构建器上下文。然而,为特定的Java类手动编写这个过程似乎有点繁琐...

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