Edycja: Chciałbym zaznaczyć, że to pytanie opisuje problem teoretyczny i jestem świadomy, że mogę użyć argumentów konstruktora dla parametrów obowiązkowych lub zgłosić wyjątek czasu wykonywania, jeśli interfejs API jest używany nieprawidłowo. Jednak szukam rozwiązania, które nie wymaga argumentów konstruktora ani sprawdzania czasu wykonywania.
Wyobraź sobie, że masz taki Car
interfejs:
public interface Car {
public Engine getEngine(); // required
public Transmission getTransmission(); // required
public Stereo getStereo(); // optional
}
Jak sugerują komentarze, Car
musi mieć Engine
i Transmission
ale Stereo
jest opcjonalne. Oznacza to, że konstruktor, który może build()
mieć Car
instancję, powinien mieć build()
metodę tylko wtedy, gdy Engine
i Transmission
oba zostały już przekazane instancji konstruktora. W ten sposób moduł sprawdzania typu odmówi skompilowania kodu, który próbuje utworzyć Car
instancję bez znaku Engine
lub Transmission
.
Wymaga to Kreatora kroków . Zazwyczaj implementujesz coś takiego:
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;
}
};
}
}
}
Jest to sieć różnych klas wypełniaczy ( Builder
, BuilderWithEngine
, CompleteBuilder
), że jeden dodatek wymagane metody setter po drugim, z ostatniej klasy zawierającej wszystkie opcjonalne metody setter, jak również.
Oznacza to, że użytkownicy tego konstruktora kroków ograniczają się do kolejności, w jakiej autor udostępnił obowiązkowe programy ustawiające . Oto przykład możliwych zastosowań (zwróć uwagę, że wszystkie są ściśle uporządkowane: engine(e)
najpierw, następnie transmission(t)
, a na końcu opcjonalnie 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();
Istnieje jednak wiele scenariuszy, w których nie jest to idealne rozwiązanie dla użytkownika konstruktora, szczególnie jeśli konstruktor ma nie tylko programy ustawiające, ale także addery lub jeśli użytkownik nie może kontrolować kolejności, w której niektóre właściwości konstruktora będą dostępne.
Jedyne rozwiązanie, o którym mogłem pomyśleć, jest bardzo skomplikowane: dla każdej kombinacji ustawionych lub jeszcze ustawionych właściwości obowiązkowych stworzyłem dedykowaną klasę konstruktora, która wie, które potencjalne inne elementy ustawiające obowiązkowe należy wywołać przed przybyciem do stan, w którym build()
metoda powinna być dostępna, a każdy z tych ustawiających zwraca bardziej kompletny typ konstruktora, który jest o krok bliżej do zawarcia build()
metody.
Dodałem poniższy kod, ale możesz powiedzieć, że używam systemu typów do utworzenia FSM, który pozwala ci utworzyć a Builder
, który można przekształcić w a, BuilderWithEngine
lub BuilderWithTransmission
który oba można następnie przekształcić w a CompleteBuilder
, który implementujebuild()
metoda. Opcjonalne elementy ustawiające można wywoływać w każdym z tych wystąpień programu budującego.
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;
}
};
}
}
}
Jak widać, nie skaluje się to dobrze, ponieważ wymagana liczba różnych klas konstruktorów wynosiłaby O (2 ^ n), gdzie n jest liczbą obowiązkowych ustawień.
Stąd moje pytanie: czy można to zrobić bardziej elegancko?
(Szukam odpowiedzi, która działa z Javą, chociaż Scala też byłaby do przyjęcia)
.engine(e)
dwa razy dla jednego konstruktora?
build()
jeśli nie nazwali engine(e)
i transmission(t)
wcześniej.
Engine
implementacji, a później zastąpić ją bardziej szczegółową implementacją. Ale najprawdopodobniej będzie to więcej sensu, jeśli engine(e)
nie był rozgrywający, ale żmija: addEngine(e)
. Byłoby to przydatne dla Car
konstruktora, który może produkować samochody hybrydowe z więcej niż jednym silnikiem / silnikiem. Ponieważ jest to wymyślony przykład, nie wdałem się w szczegóły, dlaczego warto to zrobić - dla zwięzłości.
this
?