Editar: gostaria de salientar que esta pergunta descreve um problema teórico e sei que posso usar argumentos construtores para parâmetros obrigatórios ou lançar uma exceção de tempo de execução se a API for usada incorretamente. No entanto, estou procurando uma solução que não exija argumentos de construtor ou verificação de tempo de execução.
Imagine que você tem uma Car
interface como esta:
public interface Car {
public Engine getEngine(); // required
public Transmission getTransmission(); // required
public Stereo getStereo(); // optional
}
Como os comentários sugerem, um Car
deve ter um Engine
e Transmission
mas a Stereo
é opcional. Isso significa que um Construtor que pode build()
uma Car
instância só deve ter um build()
método se um Engine
e Transmission
já tiverem sido dados à instância do construtor. Dessa forma, o verificador de tipos se recusará a compilar qualquer código que tente criar uma Car
instância sem um Engine
ou Transmission
.
Isso exige um Step Builder . Normalmente, você implementaria algo assim:
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;
}
};
}
}
}
Há uma cadeia de diferentes classes construtor ( Builder
, BuilderWithEngine
, CompleteBuilder
), que um add necessário método setter após o outro, com a última classe que contém todos os métodos setter opcionais também.
Isso significa que os usuários deste construtor de etapas estão confinados à ordem em que o autor disponibilizou os configuradores obrigatórios . Aqui está um exemplo de possíveis usos (observe que todos eles são estritamente ordenados: engine(e)
primeiro, seguido por transmission(t)
e, finalmente, 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();
No entanto, existem muitos cenários nos quais isso não é ideal para o usuário do construtor, especialmente se o construtor não tiver apenas setters, mas também adicionadores, ou se o usuário não puder controlar a ordem na qual determinadas propriedades para o construtor ficarão disponíveis.
A única solução em que pude pensar é muito complicada: para cada combinação de propriedades obrigatórias que foram definidas ou ainda não definidas, criei uma classe de construtor dedicada que sabe quais outros setters obrigatórios em potencial precisam ser chamados antes de chegar a um indique onde o build()
método deve estar disponível e cada um desses setters retorna um tipo mais completo de construtor, que está a um passo de conter um build()
método.
Adicionei o código abaixo, mas você pode dizer que estou usando o sistema de tipos para criar um FSM que permite criar um Builder
, que pode ser transformado em um BuilderWithEngine
ou BuilderWithTransmission
, que ambos podem ser transformados em um CompleteBuilder
, que implementa obuild()
método. Os setters opcionais podem ser chamados em qualquer uma dessas instâncias do construtor.
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 você pode ver, isso não é bem dimensionado, pois o número de diferentes classes de construtores necessárias seria O (2 ^ n) em que n é o número de setters obrigatórios.
Daí a minha pergunta: isso pode ser feito de maneira mais elegante?
(Estou procurando uma resposta que funcione com Java, embora Scala também seja aceitável)
.engine(e)
duas vezes para um construtor?
build()
se não ligou engine(e)
e transmission(t)
antes.
Engine
implementação padrão e depois substituí-la por uma implementação mais específica. Mas, muito provavelmente isso faria mais sentido se engine(e)
não era um setter, mas uma víbora: addEngine(e)
. Isso seria útil para um Car
construtor que pode produzir carros híbridos com mais de um motor / motor. Como este é um exemplo artificial, não entrei em detalhes sobre por que você pode querer fazer isso - por uma questão de brevidade.
this
?