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 Car
interfaz como esta:
public interface Car {
public Engine getEngine(); // required
public Transmission getTransmission(); // required
public Stereo getStereo(); // optional
}
Como sugieren los comentarios, a Car
debe tener un Engine
y Transmission
pero a Stereo
es opcional. Eso significa que un constructor que puede build()
una Car
instancia debe sólo tenga un build()
método si una Engine
y Transmission
dos 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 Car
instancia sin un Engine
o 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 BuilderWithEngine
o 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.
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)
fuente
this
?.engine(e)
dos veces para un constructor?build()
si no ha llamadoengine(e)
ytransmission(t)
antes.Engine
implementació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 siengine(e)
no era un organismo, pero una víbora:addEngine(e)
. Esto sería útil para unCar
constructor 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.Respuestas:
Parece que tiene dos requisitos diferentes, según las llamadas de método que proporcionó.
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:
y
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.
fuente
¿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.
fuente
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.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.
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.
fuente
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; unn
entero 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 menosn
bits 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.fuente