Konstruktor z mnóstwem parametrów vs wzorzec konstruktora


21

Dobrze wiadomo, że jeśli twoja klasa ma konstruktor z wieloma parametrami, powiedzmy więcej niż 4, to najprawdopodobniej jest to zapach kodu . Musisz ponownie rozważyć, czy klasa spełnia SRP .

Ale co, jeśli zbudujemy i obiekt, który zależy od 10 lub więcej parametrów, a ostatecznie zakończy się ustawieniem wszystkich tych parametrów za pomocą wzorca Builder? Wyobraź sobie, że budujesz Personobiekt typu z jego danymi osobowymi, informacjami o pracy, informacjami o przyjaciołach, informacjami o zainteresowaniach, informacjami o edukacji i tak dalej. To już jest dobre, ale jakoś ustawiłeś te same więcej niż 4 parametry, prawda? Dlaczego te dwa przypadki nie są uważane za takie same?

Odpowiedzi:


25

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 truejakiegoś 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 ai dbył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.


Naprawdę sprzeciwiłbym się części samodokumentującej. Jeśli masz przyzwoite IDE + przyzwoite komentarze, przez większość czasu konstruktor i definicja parametru są dostępne po jednym naciśnięciu klawisza.
JavierIEH

W Android Studio 3.0 nazwa parametru w konstruktorze jest wyświetlana obok wartości przekazywanej w wywołaniu konstruktora. np .: nowy A (operand1: 34, operand2: 56); operand1 i operand2 to nazwy parametrów w konstruktorze. Są pokazane przez IDE, aby kod był bardziej czytelny. Zatem nie ma potrzeby przechodzenia do definicji, aby dowiedzieć się, jaki jest parametr.
granat

9

Wzorzec konstruktora niczego nie rozwiązuje i nie naprawia błędów projektowych.

Jeśli masz klasę wymagającą zbudowania 10 parametrów, zbudowanie konstruktora do jej zbudowania nie poprawi nagle twojego projektu. Powinieneś zdecydować się na refaktoryzację danej klasy.

Z drugiej strony, jeśli masz klasę, być może proste DTO, w której pewne atrybuty klasy są opcjonalne, wzorzec konstruktora może ułatwić budowę tego obiektu.


1

Każda część, np. Dane osobowe, informacje o pracy (każda „praca”), każdy przyjaciel itp., Powinny zostać przekonwertowane na własne obiekty.

Zastanów się, jak zaimplementujesz funkcję aktualizacji. Użytkownik chce dodać nową historię pracy. Użytkownik nie chce zmieniać żadnych innych części informacji ani historii pracy - po prostu zachowaj je bez zmian.

W przypadku dużej ilości informacji, gromadzenie informacji pojedynczo jest nieuniknione. Możesz pogrupować je logicznie - w oparciu o ludzką intuicję (co ułatwia programistom) lub w oparciu o wzorzec użytkowania (jakie informacje są zwykle aktualizowane w tym samym czasie).

Wzorzec konstruktora to tylko sposób na przechwycenie listy (uporządkowanej lub nieuporządkowanej) typowanych argumentów, które można następnie przekazać do rzeczywistego konstruktora (lub konstruktor może odczytać przechwycone argumenty konstruktora).

Wytyczna 4 to tylko praktyczna zasada. Są sytuacje, które wymagają więcej niż 4 argumentów i nie ma rozsądnego ani logicznego sposobu ich grupowania. W takich przypadkach sensowne structmoże być utworzenie obiektu, który można wypełnić bezpośrednio lub za pomocą ustawień właściwości. Zauważ, że structi konstruktor w tym przypadku mają bardzo podobny cel.

W swoim przykładzie opisałeś już logiczny sposób grupowania ich. Jeśli masz do czynienia z sytuacją, w której nie jest to prawdą, być może możesz to zilustrować innym przykładem.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.