Шаблон Java Builder и "глубокая" иерархия объектов

Какова наилучшая практика использования шаблона Builder в "глубоких" иерархиях объектов? Чтобы разработать, я исследовал идею применения шаблона 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 для нескольких уровней, плавающих в коде, добавляя к путанице.

Итак, любые идеи о том, как изящно выполнить это?

Ответы

Ответ 1

Описание шаблона Builder here - я предполагаю, что вы имеете в виду; это немного отличается от шаблона, описанного в Wikipedia здесь, я предпочитаю первое.

Я не вижу, что ваши опасения относительно порядка строительства или потери инкапсуляции неизбежно вытекают из описаний, которые я читал. Для меня большой вопрос - это структура ваших исходных данных.

Предположим, что

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

Мы можем применить тот же шаблон к дальнейшим уровням вложенности.

Чтобы решить вашу первую точку, переменная для каждого свойства: зависит от того, как мы проектируем сборщиков и откуда происходят наши данные. Если мы говорим, что, исходя из пользовательского интерфейса, у нас в значительной степени есть переменная на каждое свойство, мы не хуже. Если согласно моему предложению выше мы повторяем некоторую структуру данных, тогда, возможно, строитель берет на себя ответственность за iterpreting эту структуру данных. В моем примере мы передаем экземпляры MiddleData. Некоторая дополнительная связь, но она инкапсулирует детали.

Чтобы решить вашу вторую точку, мы не строим вещи по мере того, как мы идем, вместо этого мы эффективно используем построитель в качестве точки накопления данных. В конце концов мы вызываем метод "Go and Build", но в этот момент мы должны иметь все данные на месте, чтобы вся иерархия просто строилась.

Ответ 2

Это можно сделать, но это, возможно, не стоит делать. Очевидная реализация...

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, просто a Shape.Builder<Rectangle>. Таким образом, вы должны вызвать атрибуты по порядку, от наиболее полученных до наименее полученных:

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

Если вы хотите обойти это, вам нужно сделать общие методы атрибутов, чтобы методы суперкласса все равно возвращали сборщики подкласса. Нет никакой возможности AFAIK сделать это на 100% надежным, но с некоторыми самореферентными дженериками вы можете приблизиться:

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

Если вы создадите код из XML-схемы с JAXB, вам поможет плагин "плавный строитель" из jaxb2-rich-contract-plugin. Он генерирует шаблон глубокого построителя, в котором вы можете объединить сборщиков вместе и использовать метод "end()", чтобы завершить построение вложенного объекта и вернуться к контексту компоновщика его родителя. Однако писать это вручную для данного Java-класса кажется немного утомительным...