¿Cómo debo manejar configuraciones incompatibles con el patrón Builder?

9

Esto está motivado por esta respuesta a una pregunta separada .

El patrón de construcción se usa para simplificar la inicialización compleja, especialmente con parámetros de inicialización opcionales). Pero no sé cómo administrar adecuadamente las configuraciones mutuamente excluyentes.

Aquí hay una Imageclase. Imagese puede inicializar desde un archivo o desde un tamaño, pero no ambos . Usar constructores para imponer esta exclusión mutua es obvio cuando la clase es lo suficientemente simple:

public class Image
{
    public Image(Size size, Thing stuff, int range)
    {
    // ... initialize empty with size
    }

    public Image(string filename, Thing stuff, int range)
    {
        // ... initialize from file
    }
}

Ahora suponga Imageque en realidad es lo suficientemente configurable para que el patrón de construcción sea útil, de repente esto podría ser posible:

Image image = new ImageBuilder()
                  .setStuff(stuff)
                  .setRange(range)
                  .setSize(size)           // <----------  NOT
                  .setFilename(filename)   // <----------  COMPATIBLE
                  .build();

Estos problemas deben detectarse en tiempo de ejecución en lugar de en tiempo de compilación, que no es lo peor. El problema es que la detección sistemática y exhaustiva de estos problemas dentro de la ImageBuilderclase podría ser compleja, especialmente en términos de mantenimiento.

¿Cómo debo lidiar con configuraciones incompatibles en el patrón de construcción?

kdbanman
fuente

Respuestas:

12

Tienes tu constructor. Sin embargo, en este punto necesita algunas interfaces.

Hay una interfaz FileBuilder que define un subconjunto de métodos (no setSize) y una interfaz SizeBuilder que define otro subconjunto de métodos (no setFilename). Es posible que desee tener una interfaz GenericBuilder que amplíe FileBuilder y SizeBuilder; no es necesario, aunque algunas personas pueden preferir ese enfoque.

El método setSize()devuelve un SizeBuilder. El método setFilename()devuelve un FileBuilder.

ImageBuilder tiene toda la lógica para ambos setSize()y setFileName(). Sin embargo, el tipo de retorno para estos especificaría la interfaz de subconjunto adecuada.

class ImageBulder implements FileBuilder, SizeBuilder {
    ImageBuilder() {
        doInitThings;
    }

    ImageBuilder setStuff(Thing) {
        doStuff;
        return this;
    }

    ImageBuilder setRange(int range) {
        rangeStuff;
        return this;
    }

    SizeBuilder setSize(Size size) {
        stuff;
        return this;
    }

    FileBuilder setFilename(String filename) {
        otherStuff;
        return this;
    }

    Image build() {
        return new Image(...);
    }
}

Una parte especial aquí es que una vez que tiene un SizeBuilder, todas las devoluciones deben ser SizeBuilders. La interfaz para esto se ve así:

interface SizeBuilder {
    SizeBuilder setRange(int range);
    SizeBuilder setSize(Size size);
    SizeBuilder setStuff(Thing stuff);
    Image build();
}

interface FileBuilder {
    FileBuilder setRange(int range);
    FileBuilder setFilename(String filename);
    FileBuilder setStuff(Thing stuff);
    Image build();
}

Por lo tanto, una vez que llame a uno de esos métodos, ahora no podrá llamar al otro y crear un objeto con un estado no válido.


fuente
Muy interesante, gracias. Sin embargo, estoy un poco confundido sobre cómo se usarían. Específicamente, no puedo entender cuáles serían los tipos de declaración e inicialización. Probablemente solo estoy imaginando cosas mucho más complicadas de lo necesario. ¿Podría incluir un ejemplo de uso?
kdbanman
El generador de imágenes devuelve la interfaz correspondiente al cambio de estado que ese método llama. Sin embargo, una vez que recupera una interfaz específica de ImageBuilder, las llamadas futuras contra ese objeto se realizan en esa interfaz, lo que restringe la capacidad de llamar a métodos incompatibles.
1
@rwong, aunque admito que no lo analicé demasiado, el problema que pensé que tenía con ese enfoque era que el "estado" del constructor podría restablecerse. Uno tendría que asegurarse de que una vez que se llamara a setSize (), todas las invocaciones adicionales del generador estuvieran en SizeBuilder. Si el tipo de SetRange () no era el SizeBuilder o algo que se extiende / implementos que se podría obtener en torno a llamar setFilename en él de nuevo. También tiene la situación (no descrita aquí) en la que, en lugar de tamaño, tiene int ancho y alto alto, por lo que ambos deben llamarse.
1
@MichaelT Teniendo en cuenta los intrincados problemas de elusión, sospecho que aplicar un orden estricto de inicialización de parámetros (lo que resulta en un árbol de prefijos de elementos de parámetros) podría ser algo bueno cuando se usa el patrón de construcción. Como resultado, los elementos de parámetros comunes como Rangey Stuffdeben inicializarse al principio, no en momentos arbitrarios.
rwong
1
@MichaelT: en ese punto, LSP entra en juego. Puede estar seguro de que los métodos del tipo aparente ( RangeAndStuffBuilder) se pueden invocar en el tipo real. Se pueden implementar restricciones adicionales al devolver más tipos basales para algunos métodos (aunque esto causará un aumento exponencial en los tipos), eliminando efectivamente las operaciones. Mientras los resultados del método no vuelvan a descender en la jerarquía, no obtendrá errores de tipo. El escenario setHeight/ setWidthpodría implementarse con una jerarquía de hermanos que no tiene un buildmétodo.
outis