Modifica: vorrei sottolineare che questa domanda descrive un problema teorico e sono consapevole che posso usare gli argomenti del costruttore per i parametri obbligatori o lanciare un'eccezione di runtime se l'API è usato in modo errato. Tuttavia, sto cercando una soluzione che non richieda argomenti del costruttore o controllo del runtime.
Immagina di avere un'interfaccia Car
come questa:
public interface Car {
public Engine getEngine(); // required
public Transmission getTransmission(); // required
public Stereo getStereo(); // optional
}
Come suggerito dai commenti, un Car
deve avere Engine
e Transmission
ma un Stereo
è facoltativo. Ciò significa che un Builder che può build()
a% istanza diCar
dovrebbe sempre avere un metodo build()
se un Engine
e Transmission
sono già stati assegnati all'istanza del builder. In questo modo il programma di controllo dei tipi rifiuterà di compilare qualsiasi codice che tenta di creare un'istanza Car
senza Engine
o Transmission
.
Questo richiede un Step Builder . Normalmente implementeresti qualcosa del genere:
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;
}
};
}
}
}
Esiste una catena di diverse classi di builder ( Builder
, BuilderWithEngine
, CompleteBuilder
), che aggiunge un metodo setter richiesto dopo l'altro, con l'ultima classe contenente anche tutti i metodi setter opzionali.
Ciò significa che gli utenti di questo step builder sono confinati nell'ordine in cui l'autore ha reso disponibili setter obbligatori . Ecco un esempio di possibili usi (si noti che sono tutti rigorosamente ordinati: engine(e)
prima, seguito da transmission(t)
e infine% facoltativostereo(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();
Tuttavia, ci sono molti scenari in cui questo non è l'ideale per l'utente del costruttore, specialmente se il costruttore non ha solo setter, ma anche adder, o se l'utente non può controllare l'ordine in cui certe proprietà per il builder diventare disponibile.
L'unica soluzione a cui potrei pensare è molto complessa: per ogni combinazione di proprietà obbligatorie impostate o non ancora impostate, ho creato una classe di build dedicata che sa quale potenziale altro i setter obbligatori devono essere chiamati prima di arrivare a uno stato in cui dovrebbe essere disponibile il metodo build()
, e ciascuno di questi setter restituisce un tipo più completo di builder che è un passo avanti verso il contenimento di un metodo build()
.
Ho aggiunto il codice qui sotto, ma potresti dire che sto usando il sistema dei tipi per creare un FSM che ti consente di creare un Builder
, che può essere trasformato in BuilderWithEngine
o BuilderWithTransmission
, che entrambi possono quindi essere trasformato in CompleteBuilder
, che implementa il metodo build()
. I setter facoltativi possono essere richiamati su una di queste istanze del builder.
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;
}
};
}
}
}
Come puoi vedere, questo non è scalabile, poiché il numero di diverse classi di builder richieste sarebbe O (2 ^ n) dove n è il numero di setter obbligatori.
Quindi la mia domanda: può essere eseguita in modo più elegante?
(Sto cercando una risposta che funzioni con Java, anche se Scala sarebbe accettabile)