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 Carinterfejs:
public interface Car {
public Engine getEngine(); // required
public Transmission getTransmission(); // required
public Stereo getStereo(); // optional
}
Jak sugerują komentarze, Carmusi mieć Enginei Transmissionale Stereojest opcjonalne. Oznacza to, że konstruktor, który może build()mieć Carinstancję, powinien mieć build()metodę tylko wtedy, gdy Enginei Transmissionoba zostały już przekazane instancji konstruktora. W ten sposób moduł sprawdzania typu odmówi skompilowania kodu, który próbuje utworzyć Carinstancję bez znaku Enginelub 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, BuilderWithEnginelub BuilderWithTransmissionktó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.
Engineimplementacji, 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 Carkonstruktora, 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?