Modifica: vorrei sottolineare che questa domanda descrive un problema teorico e sono consapevole di poter utilizzare argomenti di costruzione per parametri obbligatori o generare un'eccezione di runtime se l'API viene utilizzata in modo errato. Tuttavia, sto cercando una soluzione che non richiede argomenti del costruttore o controllo del runtime.
Immagina di avere Car
un'interfaccia come questa:
public interface Car {
public Engine getEngine(); // required
public Transmission getTransmission(); // required
public Stereo getStereo(); // optional
}
Come suggeriscono i commenti, un Car
deve avere un Engine
e Transmission
ma un Stereo
è facoltativo. Questo significa che un costruttore che può un'istanza deve sempre e solo avere un metodo se un e avere sia già stata data l'istanza costruttore. In questo modo il controllo del tipo rifiuterà di compilare qualsiasi codice che tenti di creare un'istanza senza un o .build()
Car
build()
Engine
Transmission
Car
Engine
Transmission
Questo richiede uno Step Builder . In genere 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;
}
};
}
}
}
C'è una catena di diverse classi di build ( Builder
, BuilderWithEngine
, CompleteBuilder
), che aggiungere uno richiesto metodo setter dopo l'altro, con l'ultima classe che contiene tutti i metodi setter opzionali pure.
Ciò significa che gli utenti di questo step builder sono limitati all'ordine in cui l'autore ha reso disponibili setter obbligatori . Ecco un esempio di possibili usi (notare che sono tutti rigorosamente ordinati: engine(e)
primo, seguito da transmission(t)
, e infine facoltativo 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();
Tuttavia, ci sono molti scenari in cui questo non è l'ideale per l'utente del builder, specialmente se il builder non ha solo setter, ma anche additivi o se l'utente non può controllare l'ordine in cui diventeranno disponibili determinate proprietà per il builder.
L'unica soluzione a cui potrei pensare è molto contorta: per ogni combinazione di proprietà obbligatorie che sono state impostate o non ancora impostate, ho creato una classe di costruttore dedicata che sa quali potenziali altri setter obbligatori devono essere chiamati prima di arrivare a un indica dove build()
dovrebbe essere disponibile il metodo e ognuno di questi setter restituisce un tipo più completo di builder che è un passo avanti verso il contenimento di un build()
metodo.
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 un BuilderWithEngine
o BuilderWithTransmission
, che entrambi possono quindi essere trasformati in un CompleteBuilder
, che implementa ilbuild()
metodo. I setter opzionali possono essere invocati in 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 si adatta bene, poiché il numero di diverse classi di builder richieste sarebbe O (2 ^ n) dove n è il numero di setter obbligatori.
Da qui la mia domanda: questo può essere fatto in modo più elegante?
(Sto cercando una risposta che funzioni con Java, anche se Scala sarebbe accettabile)
.engine(e)
due volte per un costruttore?
build()
se non hai chiamato engine(e)
e transmission(t)
prima.
Engine
un'implementazione predefinita e successivamente sovrascriverla con un'implementazione più specifica. Ma molto probabilmente questo avrebbe più senso se engine(e)
non era un setter, ma una vipera: addEngine(e)
. Ciò sarebbe utile per un Car
costruttore in grado di produrre auto ibride con più di un motore / motore. Dato che questo è un esempio inventato, non sono entrato nei dettagli sul motivo per cui potresti voler farlo - per brevità.
this
?