Шаблон Java Builder и «глубокая» иерархия объектов - PullRequest
14 голосов
/ 18 июля 2011

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

Итак, есть ли идеи о том, как элегантно это сделать?

Ответы [ 3 ]

6 голосов
/ 18 июля 2011

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

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

Предположим, у нас есть

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

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

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

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

4 голосов
/ 21 марта 2013

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

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

Если вы хотите обойти это, вам нужно сделать методы атрибутов общими, чтобы методы суперклассавсе равно вернет создателей подкласса.У 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>, система выходит из строя.

И вы можете видеть, насколько уродливым становится код.На данный момент, может быть, лучше отказаться от Item 2 и вместо этого медитировать на Item 16: Favor композиции вместо наследования .

1 голос
/ 05 октября 2015

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

...