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)
źródło
this
?.engine(e)
dwa razy dla jednego konstruktora?build()
jeśli nie nazwaliengine(e)
itransmission(t)
wcześniej.Engine
implementacji, a później zastąpić ją bardziej szczegółową implementacją. Ale najprawdopodobniej będzie to więcej sensu, jeśliengine(e)
nie był rozgrywający, ale żmija:addEngine(e)
. Byłoby to przydatne dlaCar
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.Odpowiedzi:
Wygląda na to, że masz dwa różne wymagania w zależności od podanych przez ciebie wywołań metod.
Myślę, że pierwszym problemem jest to, że nie wiesz, co chcesz zrobić w klasie. Częściowo dlatego, że nie wiadomo, jak ma wyglądać zbudowany obiekt.
Samochód może mieć tylko jeden silnik i jedną skrzynię biegów. Nawet samochody hybrydowe mają tylko jeden silnik (być może a
GasAndElectricEngine
)Zajmę się obiema implementacjami:
i
Jeśli wymagany jest silnik i przekładnia, powinny one znajdować się w konstruktorze.
Jeśli nie wiesz, jaki silnik lub skrzynia biegów jest wymagana, nie ustawiaj jej jeszcze; to znak, że tworzysz konstruktora zbyt daleko na stosie.
źródło
Dlaczego nie użyć wzorca zerowego obiektu? Pozbądź się tego konstruktora, najbardziej eleganckim kodem, jaki możesz napisać, jest ten, którego tak naprawdę nie musisz pisać.
źródło
Car
, miałoby to sens, ponieważ liczba argumentów c'tor jest bardzo mała. Jednak gdy tylko masz do czynienia z czymś umiarkowanie złożonym (> = 4 obowiązkowe argumenty), cała sprawa staje się trudniejsza do obsługi / mniej czytelna („Czy silnik lub skrzynia biegów były najważniejsze?”). Dlatego użyjesz konstruktora: interfejs API zmusza cię do wyraźniejszego wyrażenia się na temat tego, co konstruujesz.Po pierwsze, chyba że masz dużo więcej czasu niż jakikolwiek sklep, w którym pracowałem, prawdopodobnie nie warto pozwolić na żadną kolejność operacji lub po prostu żyć z faktem, że możesz podać więcej niż jedno radio. Zauważ, że mówisz o kodzie, a nie o danych wejściowych użytkownika, więc możesz mieć asercje, które zawiodą podczas testowania jednostki, a nie sekundę przed kompilacją.
Jednakże, jeśli twoim ograniczeniem jest, jak podano w komentarzach, że musisz mieć silnik i skrzynię biegów, wymusz to, umieszczając wszystkie obowiązkowe właściwości konstruktora konstruktora.
Jeśli tylko stereo jest opcjonalny, to wykonanie ostatniego kroku przy użyciu podklas konstruktorów jest możliwe, ale poza tym zysk z uzyskania błędu w czasie kompilacji zamiast w testowaniu prawdopodobnie nie jest wart wysiłku.
źródło
Odgadłeś już właściwy kierunek dla tego pytania.
Jeśli chcesz uzyskać sprawdzanie czasu kompilacji, potrzebujesz
(2^n)
typów. Jeśli chcesz sprawdzać w czasie wykonywania, potrzebujesz zmiennej, która może przechowywać(2^n)
stany; wystarczyn
-bitowa liczba całkowita.Ponieważ C ++ obsługuje parametr szablonu innego niż typ (np. Wartości całkowite) , możliwe jest utworzenie instancji szablonu klasy C ++ na
O(2^n)
różne typy przy użyciu schematu podobnego do tego .Jednak w językach, które nie obsługują parametrów szablonów innych niż typy, nie można polegać na systemie typów w tworzeniu instancji
O(2^n)
różnych typów.Następną możliwością są adnotacje Java (i atrybuty C #). Te dodatkowe metadane można wykorzystać do wyzwalania zachowania zdefiniowanego przez użytkownika w czasie kompilacji, gdy używane są procesory adnotacji . Jednak wdrożenie ich byłoby zbyt pracochłonne. Jeśli korzystasz z frameworków, które zapewniają tę funkcję, użyj jej. W przeciwnym razie sprawdź następną okazję.
Na koniec zauważ, że przechowywanie
O(2^n)
różnych stanów jako zmiennych w czasie wykonywania (dosłownie jako liczba całkowita on
szerokości co najmniej bitów) jest bardzo łatwe. Z tego powodu najbardziej uprzywilejowane odpowiedzi zalecają wykonanie tego sprawdzenia w czasie wykonywania, ponieważ wysiłek potrzebny do wdrożenia sprawdzania czasu kompilacji jest zbyt duży w porównaniu z potencjalnym zyskiem.źródło