Wzorzec konstruktora nie rozwiązuje „problemu” wielu argumentów. Ale dlaczego wiele argumentów jest problematycznych?
- Wskazują, że twoja klasa może robić zbyt wiele . Istnieje jednak wiele typów, które zgodnie z prawem zawierają wiele członków, których nie można rozsądnie zgrupować.
- Testowanie i rozumienie funkcji z wieloma wejściami staje się wykładniczo bardziej skomplikowane - dosłownie!
- Gdy język nie oferuje nazwanych parametrów, wywołanie funkcji nie jest samok dokumentujące . Czytanie wywołania funkcji z wieloma argumentami jest dość trudne, ponieważ nie masz pojęcia, co powinien zrobić 7. parametr. Nie zauważysz nawet, że argumenty piąty i szósty zostały zamienione przypadkowo, szczególnie jeśli jesteś w języku dynamicznie typowanym lub wszystko dzieje się jako ciąg znaków lub gdy z
true
jakiegoś powodu ostatni parametr jest z jakiegoś powodu.
Udawanie nazwanych parametrów
Na wzór Builder adresy tylko jeden z tych problemów, a mianowicie dotyczy naprawiania wywołań funkcji z wieloma argumentami * . Więc wywołanie funkcji jak
MyClass o = new MyClass(a, b, c, d, e, f, g);
może stać się
MyClass o = MyClass.builder()
.a(a).b(b).c(c).d(d).e(e).f(f).g(g)
.build();
Pattern Wzorzec konstruktora pierwotnie miał służyć jako podejście niezależne od reprezentacji przy składaniu obiektów złożonych, co jest znacznie większym dążeniem niż tylko nazwane argumenty parametrów. W szczególności wzorzec konstruktora nie wymaga płynnego interfejsu.
Zapewnia to dodatkowe bezpieczeństwo, ponieważ wybuchnie, jeśli wywołasz metodę konstruktora, która nie istnieje, ale w przeciwnym razie nie przyniesie ci niczego, czego nie miałby komentarz w wywołaniu konstruktora. Ponadto ręczne tworzenie programu budującego wymaga kodu, a więcej kodu zawsze może zawierać więcej błędów.
W językach, w których łatwo jest zdefiniować nowy typ wartości, odkryłem, że o wiele lepiej jest używać mikropipowania / małych typów do symulowania nazwanych argumentów. Nazywa się tak, ponieważ typy są naprawdę małe, ale w końcu piszesz dużo więcej ;-)
MyClass o = new MyClass(
new MyClass.A(a), new MyClass.B(b), new MyClass.C(c),
new MyClass.D(d), new MyClass.E(e), new MyClass.F(f),
new MyClass.G(g));
Oczywiście, nazwy typu A
, B
, C
, ... powinny być nazwami samodokumentujące które ilustrują znaczenie parametr, często o tej samej nazwie, jak można dać zmienną parametru. W porównaniu z idiomem builder-for-named-arguments wymagana implementacja jest znacznie prostsza, a zatem mniej prawdopodobne, że zawiera błędy. Na przykład (ze składnią Java-ish):
class MyClass {
...
public static class A {
public final int value;
public A(int a) { value = a; }
}
...
}
Kompilator pomaga zagwarantować, że wszystkie argumenty zostały dostarczone; za pomocą Konstruktora musiałbyś ręcznie sprawdzić brakujące argumenty lub zakodować maszynę stanu w systemie typu języka hosta - oba prawdopodobnie zawierałyby błędy.
Istnieje inne powszechne podejście do symulacji nazwanych argumentów: pojedynczy obiekt parametru abstrakcyjnego , który używa składni klasy wbudowanej do inicjowania wszystkich pól. W Javie:
MyClass o = new MyClass(new MyClass.Arguments(){{ argA = a; argB = b; argC = c; ... }});
class MyClass {
...
public static abstract class Arguments {
public int argA;
public String ArgB;
...
}
}
Można jednak zapomnieć o polach, a jest to rozwiązanie specyficzne dla języka (widziałem zastosowania w JavaScript, C # i C).
Na szczęście konstruktor może nadal sprawdzać poprawność wszystkich argumentów, co nie ma miejsca, gdy obiekty są tworzone w stanie częściowo zbudowanym, i wymagać od użytkownika dostarczenia dalszych argumentów za pomocą ustawiaczy lub init()
metody - te wymagają najmniejszego wysiłku kodowania, ale wymagają trudniej jest pisać poprawne programy.
Tak więc, mimo że istnieje wiele podejść do rozwiązania problemu „wiele nienazwanych parametrów utrudnia utrzymanie kodu”, pozostają inne problemy.
Zbliża się problem z korzeniem
Na przykład problem z testowalnością. Kiedy piszę testy jednostkowe, potrzebuję możliwości wstrzykiwania danych testowych i zapewnienia implementacji testowych, aby wyśmiewać zależności i operacje, które mają zewnętrzne skutki uboczne. Nie mogę tego zrobić, gdy tworzysz dowolne klasy w swoim konstruktorze. O ile odpowiedzialność twojej klasy nie polega na tworzeniu innych obiektów, nie powinna ona tworzyć żadnych nietrywialnych klas. Towarzyszy temu pojedynczy problem odpowiedzialności. Im bardziej skoncentrowana jest odpowiedzialność klasy, tym łatwiej jest ją przetestować (i często łatwiej jest z niej korzystać).
Najłatwiejszym i często najlepszym podejściem jest, aby konstruktor przyjmował w pełni zbudowane zależności jako parametr , chociaż to przenosi odpowiedzialność za zarządzanie zależnościami na osobę wywołującą - nie jest to również idealne, chyba że zależności są niezależnymi jednostkami w modelu domeny.
Czasami zamiast tego stosuje się (abstrakcyjne) fabryki lub systemy wstrzykiwania pełnej zależności , choć w większości przypadków mogą to być nadmierne umiejętności. W szczególności zmniejszają one liczbę argumentów tylko wtedy, gdy wiele z tych argumentów to obiekty quasi-globalne lub wartości konfiguracyjne, które nie zmieniają się między instancjami obiektów. Np jeśli parametry a
i d
były global-owski, chcielibyśmy dostać
Dependencies deps = new Dependencies(a, d);
...
MyClass o = deps.newMyClass(b, c, e, f, g);
class MyClass {
MyClass(Dependencies deps, B b, C c, E e, F f, G g) {
this.depA = deps.newDepA(b, c);
this.depB = deps.newDepB(e, f);
this.g = g;
}
...
}
class Dependencies {
private A a;
private D d;
public Dependencies(A a, D d) { this.a = a; this.d = d; }
public DepA newDepA(B b, C c) { return new DepA(a, b, c); }
public DepB newDepB(E e, F f) { return new DepB(d, e, f); }
public MyClass newMyClass(B b, C c, E e, F f, G g) {
return new MyClass(deps, b, c, e, f, g);
}
}
W zależności od aplikacji może to być zmieniacz gier, w którym metody fabryczne nie mają prawie żadnych argumentów, ponieważ wszystkie mogą być dostarczone przez menedżera zależności lub może to być duża ilość kodu, który komplikuje tworzenie instancji bez widocznych korzyści. Takie fabryki są znacznie bardziej przydatne do mapowania interfejsów na konkretne typy niż do zarządzania parametrami. Jednak to podejście próbuje rozwiązać problem zbyt wielu parametrów, a nie tylko ukrywać go za pomocą dość płynnego interfejsu.