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)
fonte
this
?.engine(e)
duas vezes para um construtor?build()
se não ligouengine(e)
etransmission(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 seengine(e)
não era um setter, mas uma víbora:addEngine(e)
. Isso seria útil para umCar
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.Respostas:
Você parece ter dois requisitos diferentes, com base nas chamadas de método fornecidas.
Acho que a primeira questão aqui é que você não sabe o que deseja que a turma faça. Parte disso é que não se sabe como você deseja que o objeto criado seja.
Um carro pode ter apenas um motor e uma transmissão. Até carros híbridos têm apenas um motor (talvez a
GasAndElectricEngine
)Vou abordar as duas implementações:
e
Se um motor e uma transmissão forem necessários, eles deverão estar no construtor.
Se você não sabe qual motor ou transmissão é necessária, não configure um ainda; é um sinal de que você está criando o construtor muito acima da pilha.
fonte
Por que não usar o padrão de objeto nulo? Livre-se desse construtor, o código mais elegante que você pode escrever é aquele que você realmente não precisa escrever.
fonte
Car
, isso faria sentido, pois o número de argumentos c'tor é muito pequeno. No entanto, assim que você lida com algo moderadamente complexo (> = 4 argumentos obrigatórios), tudo fica mais difícil de manusear / menos legível ("O motor ou a transmissão vieram primeiro?"). É por isso que você usaria um construtor: a API força você a ser mais explícito sobre o que está construindo.Em primeiro lugar, a menos que você tenha muito mais tempo do que qualquer loja em que trabalhei, provavelmente não vale a pena permitir nenhuma ordem de operações ou apenas conviver com o fato de que você pode especificar mais de um rádio. Observe que você está falando de código, não de entrada do usuário, para poder ter afirmações que falharão durante o teste de unidade, em vez de um segundo antes no tempo de compilação.
No entanto, se sua restrição é, conforme fornecido nos comentários, que você precisa ter um mecanismo e uma transmissão, imponha isso colocando todas as propriedades obrigatórias no construtor do construtor.
Se é apenas o estéreo que é opcional, é possível executar a etapa final usando subclasses de construtores, mas além disso, o ganho de obter o erro no tempo de compilação e não no teste provavelmente não vale o esforço.
fonte
Você já adivinhou a direção correta para esta pergunta.
Se você deseja obter a verificação em tempo de compilação, precisará de
(2^n)
tipos. Se você deseja obter a verificação em tempo de execução, precisará de uma variável que possa armazenar(2^n)
estados; umn
número inteiro de bits serve.Como o C ++ suporta parâmetros de modelo que não sejam do tipo (por exemplo, valores inteiros) , é possível que um modelo de classe C ++ seja instanciado em
O(2^n)
tipos diferentes, usando um esquema semelhante a este .No entanto, em idiomas que não suportam parâmetros de modelo que não sejam do tipo, não é possível confiar no sistema de
O(2^n)
tipos para instanciar tipos diferentes.A próxima oportunidade é com anotações Java (e atributos C #). Esses metadados adicionais podem ser usados para disparar o comportamento definido pelo usuário em tempo de compilação, quando processadores de anotação são usados. No entanto, seria muito trabalho para você implementá-los. Se você estiver usando estruturas que fornecem essa funcionalidade para você, use-a. Caso contrário, verifique a próxima oportunidade.
Por fim, observe que armazenar
O(2^n)
estados diferentes como uma variável em tempo de execução (literalmente, como um número inteiro com pelo menosn
bits de largura) é muito fácil. É por isso que as respostas mais votadas recomendam que você execute essa verificação em tempo de execução, porque o esforço necessário para implementar a verificação em tempo de compilação é muito alto, quando comparado ao ganho potencial.fonte