2011-07-18 19 views
14

¿Cuál es la mejor práctica para usar el patrón de Builder en las jerarquías de objetos "profundas"? Para elaborar, exploré la idea de aplicar el patrón Builder tal como lo propuso Joshua Bloch, a mi código de enlace XML (estoy usando SimpleXML pero esta pregunta se aplicaría a cualquier caso). Mi jerarquía de objetos tiene 4 niveles de profundidad, con varios grados de complejidad. Con eso quiero decir, en algunos niveles tengo solo un par de propiedades para mis objetos, mientras que en otros niveles tengo hasta 10.Patrón de Java Builder y una jerarquía de objetos "profunda"

Considere este ejemplo hipotético (estoy omitiendo las anotaciones Simple XML para brevedad)

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

si quería cumplir creación del objeto Outermost utilizando constructores, ¿cuál sería la mejor manera de hacerlo? La respuesta más obvia es tener clases inner static Builder para cada una de las clases anteriores.

Pero, ¿eso no haría las cosas tan difíciles de manejar como el problema mismo del patrón de Builder trata de resolver? Estoy pensando en cosas como: esto exigirá un enfoque "de adentro hacia afuera", lo que significa que el objeto Innermost tendrá que estar completamente construido e instanciado antes de que pueda agregarse al objeto Middle. Pero todos sabemos que en la práctica (especialmente cuando uno construye XML o JSON), raramente tenemos información "oportuna" para lograr esto.

Lo más probable es que acabe teniendo variables para cada propiedad, en todos los niveles; y crea los objetos en el final. O bien, uno terminará teniendo el Creador para múltiples niveles flotando en el código, aumentando la confusión.

¿Alguna idea sobre cómo lograr esto con elegancia?

+0

IMHO Si está utilizando el patrón de generador, debe hacer que todos los campos privados sean definitivos. –

+0

@ AdamGent Gracias por señalarlo. De hecho, son definitivos en mi aplicación. De alguna manera se perdió eso al publicar la pregunta aquí. – curioustechizen

Respuesta

6

La descripción del patrón de generador here es supongo a lo que se refiere; es un poco diferente al patrón descrito en Wikipedia here, prefiero el anterior.

No veo que sus preocupaciones sobre el orden de construcción o la pérdida de encapsulamiento sean inevitables a partir de las descripciones que leí. Para mí, la gran pregunta es la estructura de tus datos brutos.

Supongamos que tenemos

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

Ahora podemos crear tantas middleBuilders ya que necesitamos

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

Podemos aplicar el mismo modelo a los otros niveles de anidamiento.

Para abordar su primer punto, una variable para cada propiedad: depende de cómo diseñamos los constructores y de dónde provienen nuestros datos. Si estamos, por ejemplo, viniendo de una UI, tenemos una variable por propiedad de todos modos, no estamos peor. Si, según mi sugerencia anterior, estamos iterando con alguna estructura de datos, entonces tal vez el constructor asuma la responsabilidad de interpretar esa estructura de datos. En mi ejemplo, pasamos las instancias de MiddleData. Algunos acoplamientos adicionales pero encapsulan los detalles.

Para abordar su segundo punto, no construimos cosas sobre la marcha, sino que estamos utilizando el generador como punto de acumulación para los datos. Eventualmente llamamos al método "Ir y construir", pero en ese punto deberíamos tener todos los datos en su lugar para que toda la jerarquía se construya.

+0

¡Eso fue detallado y eso fue rápido! Bueno, me refiero a la descripción anterior de Builder como usted señaló. Pero no estoy seguro de entender lo que estás sugiriendo aquí. ¿Estás diciendo que tengo una jerarquía de objetos de compilación que es paralela a mi jerarquía de clases? Tal vez la respuesta a eso me ayude a entender por qué piensas que el "Ir y construir" no necesita estar al revés. – curioustechizen

+1

Sí, estoy pensando en términos de que Outer Builder tenga conocimiento de Middle Builders y Middle Builders que conocen acerca de Inner Builders. Pensé en su preocupación de que podríamos recibir los datos en algún orden que no nos permitiría construir Outside-In. Por lo tanto, para la interpretación de datos afirmo que podemos construir los constructores en el orden que queramos, muy probablemente afuera, y acumular datos en los constructores como lo deseamos. En algún momento tenemos todo lo que necesitamos y en ese momento le pedimos a Outer que empiece a construir, que a su vez se abre camino recursivamente hacia adentro. Es muy probable que los objetos más internos consigan construir primero. – djna

+0

Sí, veo tu punto ahora. Intentaré este enfoque y actualizaré mis hallazgos aquí. – curioustechizen

4

Se puede hacer, pero podría decirse que no vale la pena hacerlo. La implementación obvia ...

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

... rápidamente se topa con un problema. Si intenta algo así como

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

que no va a compilar, porque opacity() no sabe que está devolviendo un Rectangle.Builder, sólo un Shape.Builder<Rectangle>. Así que hay que llamar a los atributos en orden, de más derivada al menos derivados:

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

Si desea evitar esto, es necesario hacer que los métodos de atributos genéricos, por lo que los métodos de superclase seguirá siendo devolver los constructores de la subclase. No hay manera de que yo sepa para que este 100% fiable, pero con algunos medicamentos genéricos autorreferenciales que pueden acercarse:

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

Nota @SuppressWarnings las anotaciones; si una subclase infringe la convención de que FooBuilder siempre se extiende FooSuperclassBuilder<Foo, FooBuilder>, el sistema se descompone.

Y puede ver lo feo que es el código. En este punto, tal vez es mejor abandonar Item 2 y en su lugar meditar en Item 16: Favor composition over inheritance.

+3

tu respuesta fue muy informativa; sin embargo, creo que no entendiste la pregunta original. No estaba buscando aplicar el patrón Builder a _herheritance trees_. Más bien, el problema que estaba tratando de resolver era aplicar el patrón del Constructor cuando un objeto está compuesto por otros campos no primitivos. El ejemplo en la pregunta ilustra esto. – curioustechizen

1

Si genera código de un esquema XML con JAXB, el complemento "fluent-builder" de jaxb2-rich-contract-plugin lo ayudará. Genera un patrón de generador profundo donde puede encadenar constructores entre sí y utiliza un método "end()" para finalizar la construcción de un objeto anidado y volver al contexto del generador de su elemento primario. Sin embargo, escribir esto a mano para una determinada clase de Java parece un poco tedioso ...

Cuestiones relacionadas