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)
.engine(e)
dos veces para un constructor?
build()
si no ha llamado engine(e)
y transmission(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 si engine(e)
no era un organismo, pero una víbora: addEngine(e)
. Esto sería útil para un Car
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.
this
?