Java: ¿Cómo implementar un generador de pasos para el que no importa el orden de los establecedores?

10

Editar: me gustaría señalar que esta pregunta describe un problema teórico, y soy consciente de que puedo usar argumentos de constructor para parámetros obligatorios, o lanzar una excepción de tiempo de ejecución si la API se usa incorrectamente. Sin embargo, estoy buscando una solución que no requiera argumentos de constructor o verificación de tiempo de ejecución.

Imagina que tienes una Carinterfaz como esta:

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional
}

Como sugieren los comentarios, a Cardebe tener un Enginey Transmissionpero a Stereoes opcional. Eso significa que un constructor que puede build()una Carinstancia debe sólo tenga un build()método si una Enginey Transmissiondos han sido ya dadas a la instancia de constructor. De esa forma, el verificador de tipos se negará a compilar cualquier código que intente crear una Carinstancia sin un Engineo Transmission.

Esto requiere un Step Builder . Por lo general, implementaría algo como esto:

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional

    public class Builder {
        public BuilderWithEngine engine(Engine engine) {
            return new BuilderWithEngine(engine);
        }
    }

    public class BuilderWithEngine {
        private Engine engine;
        private BuilderWithEngine(Engine engine) {
            this.engine = engine;
        }
        public BuilderWithEngine engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            return new CompleteBuilder(engine, transmission);
        }
    }

    public class CompleteBuilder {
        private Engine engine;
        private Transmission transmission;
        private Stereo stereo = null;
        private CompleteBuilder(Engine engine, Transmission transmission) {
            this.engine = engine;
            this.transmission = transmission;
        }
        public CompleteBuilder engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        public CompleteBuilder stereo(Stereo stereo) {
            this.stereo = stereo;
            return this;
        }
        public Car build() {
            return new Car() {
                @Override
                public Engine getEngine() {
                    return engine;
                }
                @Override
                public Transmission getTransmission() {
                    return transmission;
                }
                @Override
                public Stereo getStereo() {
                    return stereo;
                }
            };
        }
    }
}

Hay una cadena de diferentes clases Builder ( Builder, BuilderWithEngine, CompleteBuilder), que agregue requiere método setter tras otro, con la última clase que contiene todos los métodos setter opcionales también.
Esto significa que los usuarios de este generador de pasos están confinados al orden en que el autor puso a disposición los setters obligatorios . Aquí hay un ejemplo de posibles usos (tenga en cuenta que todos están estrictamente ordenados: engine(e)primero, seguido de transmission(t), y finalmente el opcional stereo(s)).

new Builder().engine(e).transmission(t).build();
new Builder().engine(e).transmission(t).stereo(s).build();
new Builder().engine(e).engine(e).transmission(t).stereo(s).build();
new Builder().engine(e).transmission(t).engine(e).stereo(s).build();
new Builder().engine(e).transmission(t).stereo(s).engine(e).build();
new Builder().engine(e).transmission(t).transmission(t).stereo(s).build();
new Builder().engine(e).transmission(t).stereo(s).transmission(t).build();
new Builder().engine(e).transmission(t).stereo(s).stereo(s).build();

Sin embargo, hay muchos escenarios en los que esto no es ideal para el usuario del constructor, especialmente si el constructor no solo tiene setters, sino también sumadores, o si el usuario no puede controlar el orden en que ciertas propiedades para el constructor estarán disponibles.

La única solución que se me ocurre para eso es muy complicada: por cada combinación de propiedades obligatorias que se han establecido o aún no se ha establecido, creé una clase de constructor dedicada que sabe a qué otros posibles establecedores obligatorios se debe llamar antes de llegar a un indique dónde build()debería estar disponible el método, y cada uno de esos establecedores devuelve un tipo más completo de generador que está un paso más cerca de contener un build()método.
Agregué el código a continuación, pero podría decir que estoy usando el sistema de tipos para crear un FSM que le permite crear un Builder, que puede convertirse en uno BuilderWithEngineo BuilderWithTransmission, que ambos pueden convertirse en un CompleteBuilder, que implementa elbuild()método. Los invocadores opcionales se pueden invocar en cualquiera de estas instancias de constructor. ingrese la descripción de la imagen aquí

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional

    public class Builder extends OptionalBuilder {
        public BuilderWithEngine engine(Engine engine) {
            return new BuilderWithEngine(engine, stereo);
        }
        public BuilderWithTransmission transmission(Transmission transmission) {
            return new BuilderWithTransmission(transmission, stereo);
        }
        @Override
        public Builder stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class OptionalBuilder {
        protected Stereo stereo = null;
        private OptionalBuilder() {}
        public OptionalBuilder stereo(Stereo stereo) {
            this.stereo = stereo;
            return this;
        }
    }

    public class BuilderWithEngine extends OptionalBuilder {
        private Engine engine;
        private BuilderWithEngine(Engine engine, Stereo stereo) {
            this.engine = engine;
            this.stereo = stereo;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            return new CompleteBuilder(engine, transmission, stereo);
        }
        public BuilderWithEngine engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        @Override
        public BuilderWithEngine stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class BuilderWithTransmission extends OptionalBuilder {
        private Transmission transmission;
        private BuilderWithTransmission(Transmission transmission, Stereo stereo) {
            this.transmission = transmission;
            this.stereo = stereo;
        }
        public CompleteBuilder engine(Engine engine) {
            return new CompleteBuilder(engine, transmission, stereo);
        }
        public BuilderWithTransmission transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        @Override
        public BuilderWithTransmission stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class CompleteBuilder extends OptionalBuilder {
        private Engine engine;
        private Transmission transmission;
        private CompleteBuilder(Engine engine, Transmission transmission, Stereo stereo) {
            this.engine = engine;
            this.transmission = transmission;
            this.stereo = stereo;
        }
        public CompleteBuilder engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        @Override
        public CompleteBuilder stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
        public Car build() {
            return new Car() {
                @Override
                public Engine getEngine() {
                    return engine;
                }
                @Override
                public Transmission getTransmission() {
                    return transmission;
                }
                @Override
                public Stereo getStereo() {
                    return stereo;
                }
            };
        }
    }
}

Como puede ver, esto no escala bien, ya que el número de diferentes clases de constructor requeridas sería O (2 ^ n) donde n es el número de establecedores obligatorios.

De ahí mi pregunta: ¿Se puede hacer esto con más elegancia?

(Estoy buscando una respuesta que funcione con Java, aunque Scala también sería aceptable)

derabbink
fuente
1
¿Qué le impide usar un contenedor de IoC para soportar todas estas dependencias? Además, no estoy claro por qué, si el orden no importa como usted ha afirmado, ¿no podría simplemente usar métodos de establecimiento normales que regresan this?
Robert Harvey
¿Qué significa invocar .engine(e)dos veces para un constructor?
Erik Eidt
3
Si desea verificarlo de forma estática sin escribir manualmente una clase para cada combinación, es probable que tenga que usar cosas a nivel de barba como las macros o la metaprogramación de plantillas. Java no es lo suficientemente expresivo para eso, que yo sepa, y el esfuerzo probablemente no valdría la pena sobre soluciones verificadas dinámicamente en otros idiomas.
Karl Bielefeldt
1
Robert: El objetivo es que el verificador de tipo haga cumplir el hecho de que tanto un motor como una transmisión son obligatorios; de esta manera ni siquiera puede llamar build()si no ha llamado engine(e)y transmission(t)antes.
derabbink
Erik: es posible que desee comenzar con una Engineimplementación predeterminada y luego sobrescribirla con una implementación más específica. Pero lo más probable es que esto tendría más sentido si engine(e)no era un organismo, pero una víbora: addEngine(e). Esto sería útil para un Carconstructor que puede producir automóviles híbridos con más de un motor / motor. Dado que este es un ejemplo artificial, no entré en detalles sobre por qué querrías hacer esto, por brevedad.
derabbink

Respuestas:

3

Parece que tiene dos requisitos diferentes, según las llamadas de método que proporcionó.

  1. Solo un motor (requerido), solo una transmisión (requerida) y solo un estéreo (opcional).
  2. Uno o más motores (requeridos), una o más transmisiones (requeridas) y uno o más estéreos (opcionales).

Creo que el primer problema aquí es que no sabes lo que quieres que haga la clase. Parte de eso es que no se sabe cómo quiere que se vea el objeto construido.

Un automóvil solo puede tener un motor y una transmisión. Incluso los automóviles híbridos solo tienen un motor (quizás un GasAndElectricEngine)

Abordaré ambas implementaciones:

public class CarBuilder {

    public CarBuilder(Engine engine, Transmission transmission) {
        // ...
    }

    public CarBuilder setStereo(Stereo stereo) {
        // ...
        return this;
    }
}

y

public class CarBuilder {

    public CarBuilder(List<Engine> engines, List<Transmission> transmission) {
        // ...
    }

    public CarBuilder addStereo(Stereo stereo) {
        // ...
        return this;
    }
}

Si se requieren un motor y una transmisión, entonces deben estar en el constructor.

Si no sabe qué motor o transmisión se requiere, no configure uno todavía; es una señal de que estás creando el generador demasiado arriba en la pila.

Zymus
fuente
2

¿Por qué no usar el patrón de objeto nulo? Deshágase de este generador, el código más elegante que puede escribir es el que realmente no tiene que escribir.

public final class CarImpl implements Car {
    private final Engine engine;
    private final Transmission transmission;
    private final Stereo stereo;

    public CarImpl(Engine engine, Transmission transmission) {
        this(engine, transmission, new DefaultStereo());
    }

    public CarImpl(Engine engine, Transmission transmission, Stereo stereo) {
        this.engine = engine;
        this.transmission = transmission;
        this.stereo = stereo;
    }

    //...

}
Manchado
fuente
Esto es lo que pensé de inmediato. aunque no tendría el constructor de tres parámetros, solo el constructor de dos parámetros que tiene los elementos obligatorios y luego un setter para el estéreo ya que es opcional.
Encaitar
1
En un ejemplo simple (artificial) como Car, esto tendría sentido ya que el número de argumentos de c'tor es muy pequeño. Sin embargo, tan pronto como se trata de algo moderadamente complejo (> = 4 argumentos obligatorios), todo se vuelve más difícil de manejar / menos legible ("¿Primero fue el motor o la transmisión?"). Es por eso que usarías un generador: la API te obliga a ser más explícito sobre lo que estás construyendo.
derabbink
1
@derabbink ¿Por qué no romper tu clase en las más pequeñas en este caso? Usar un generador solo ocultará el hecho de que la clase está haciendo demasiado y se ha vuelto imposible de mantener.
Visto el
1
Felicitaciones por terminar con la locura del patrón.
Robert Harvey
@Spotted algunas clases solo contienen muchos datos. Por ejemplo, si desea producir una clase de registro de acceso que contenga toda la información relacionada sobre la solicitud HTTP y envíe los datos en formato CSV o JSON. Habrá una gran cantidad de datos y si desea aplicar algunos campos para que estén presentes durante el tiempo de compilación, necesitará un patrón de generador con un constructor de listas arg muy largo, que no se ve bien.
ssgao
1

En primer lugar, a menos que tenga mucho más tiempo que en cualquier tienda en la que haya trabajado, probablemente no valga la pena permitir ningún orden de operaciones o simplemente vivir con el hecho de que puede especificar más de una radio. Tenga en cuenta que está hablando de código, no de entrada del usuario, por lo que puede tener afirmaciones que fallarán durante la prueba de la unidad en lugar de un segundo antes en el momento de la compilación.

Sin embargo, si su restricción es, como se indica en los comentarios, que tiene que tener un motor y una transmisión, entonces imponga esto poniendo todas las propiedades obligatorias en el constructor del constructor.

new Builder(e, t).build();                      // ok
new Builder(e, t).stereo(s).build();            // ok
new Builder(e, t).stereo(s).stereo(s).build();  // exception on second call to stereo as stereo is already set 

Si es solo el estéreo lo que es opcional, entonces es posible hacer el paso final usando subclases de constructores, pero más allá de eso, la ganancia de obtener el error en el momento de la compilación en lugar de en las pruebas probablemente no valga la pena.

Pete Kirkham
fuente
0

el número de diferentes clases de constructor requeridas sería O (2 ^ n) donde n es el número de establecedores obligatorios.

Ya has adivinado la dirección correcta para esta pregunta.

Si desea obtener una verificación en tiempo de compilación, necesitará (2^n)tipos. Si desea obtener una verificación en tiempo de ejecución, necesitará una variable que pueda almacenar (2^n)estados; un nentero de un bit servirá.


Debido a que C ++ admite parámetros de plantilla que no son de tipo (por ejemplo, valores enteros) , es posible crear una instancia de una plantilla de clase de C ++ en O(2^n)diferentes tipos, utilizando un esquema similar a este .

Sin embargo, en idiomas que no admiten parámetros de plantilla que no sean de tipo, no puede confiar en el sistema de O(2^n)tipos para crear instancias de diferentes tipos.


La próxima oportunidad es con anotaciones de Java (y atributos de C #). Estos metadatos adicionales se pueden usar para activar el comportamiento definido por el usuario en tiempo de compilación, cuando se usan procesadores de anotaciones . Sin embargo, sería demasiado trabajo implementarlas. Si está utilizando marcos que proporcionan esta funcionalidad para usted, úselo. De lo contrario, verifique la próxima oportunidad.


Finalmente, observe que almacenar O(2^n)diferentes estados como una variable en tiempo de ejecución (literalmente, como un entero que tiene al menos nbits de ancho) es muy fácil. Esta es la razón por la cual todas las respuestas más votadas recomiendan que realice esta verificación en tiempo de ejecución, porque el esfuerzo necesario para implementar la verificación en tiempo de compilación es demasiado, en comparación con la ganancia potencial.

rwong
fuente