Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java Builder pattern and a "deep" object hierarchy

What is the best practice for using Builder pattern in "deep" object hierarchies? To elaborate, I explored the idea of applying the Builder pattern as proposed by Joshua Bloch, to my XML binding code (I am using SimpleXML but this question would apply to any case). My object hierarchy is 4 levels deep, with various degree of complexity. By that, I mean, in some levels I have just a couple of properties for my objects, whereas at some other levels I have up to 10.

So consider this hypothetical example (I am leaving out the Simple XML annotations for brevity)

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 ..*/
}

If I wanted to enforce creation of the Outermost object using builders, what would be the best way to go about it? The most obvious answer is to have inner static Builder classes for each of the above classes.

But, wouldn't that make things as unwieldy as the very problem Builder pattern tries to solve? I am thinking about stuff like - this will enforce an "inside out" approach - meaning that the Innermost object will have to be fully constructed and instantiated before it can be added to the Middle object. But we all know that in practice (especially when one is building XML or JSON), we rarely have "timely" information to accomplish this.

Chances are, one will end up having variables for each and every property - across all levels; and create the objects in the very end. OR, one will end up having Builder for multiple levels floating around in the code, adding to the confusion.

So, any ideas on how to elegantly accomplish this?

like image 574
curioustechizen Avatar asked Jul 18 '11 14:07

curioustechizen


2 Answers

The description of the Builder Pattern here is I guess what you are referring to; it's a .little different than the pattern described in Wikipedia here, I prefer the former.

I don't see that your concerns about order of construction or loss of encapsulation inevitable follow from the descriptions I read. For me the big question is the structure of your raw data.

Suppose we have

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

Now we can create as many middleBuilders as we need

 while (middleDataIter.hasNext() ) {
      MiddleData data = middleDateIter.next();
      // make a middle builder, add it.
 }

We can apply the same pattern to the further levels of nesting.

To address your first point, a variable for every property: depends on how we design the builders and where our data is coming from. If we're, say coming from a UI then we pretty much have a variable per property anyway, we're no worse off. If as per my suggestion above we're iterating some data structure, then maybe the builder takes responsibility for iterpreting that data structure. In my example we pass MiddleData instances down. Some extra coupling but it does encapsulate the details.

To address your second point we don't build things as we go, instead we're effectively using the builder as the accumulation point for the data. Eventually we call the "Go and Build" method, but at that point we should have all the data in place so the whole hierarchy just builds.

like image 152
djna Avatar answered Oct 14 '22 11:10

djna


It can be done, but it's arguably not worth doing. The obvious implementation...

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

...quickly runs into a problem. If you try something like

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

it's not going to compile, because opacity() doesn't know it's returning a Rectangle.Builder, just a Shape.Builder<Rectangle>. So you have to call the attributes in order, from most-derived to least-derived:

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

If you want to get around this, you need to make the attribute methods generic, so that the superclass methods will still return the subclass builders. There's no way AFAIK to make this 100% reliable, but with some self-referential generics you can get close:

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

Note the @SuppressWarnings annotations; if a subclass breaks the convention that FooBuilder always extends FooSuperclassBuilder<Foo, FooBuilder>, the system breaks down.

And you can see how ugly the code gets. At this point maybe it's better to abandon Item 2 and instead meditate on Item 16: Favor composition over inheritance.

like image 43
David Moles Avatar answered Oct 14 '22 11:10

David Moles