Ile argumentów konstruktora jest za dużo?

Załóżmy, że masz klasę o nazwie Customer, która zawiera następujące pola:

  • Nazwa użytkownika
  • E-Mail
  • Imię
  • Nazwisko

Załóżmy również, że zgodnie z Twoją logiką biznesową wszystkie obiekty klienta muszą mieć zdefiniowane te cztery właściwości.

Teraz możemy to zrobić dość łatwo, zmuszając konstruktora do określenia każdej z tych właściwości. Ale to dość łatwo zobaczyć, jak to może wymknąć się spod kontroli, gdy jesteś zmuszony aby dodać więcej wymaganych pól do obiektu Klienta.

Widziałem klasy, które przyjmują 20 + argumentów do swojego konstruktora i to tylko ból, aby ich używać. Alternatywnie, jeśli nie potrzebujesz tych pól, możesz napotkać ryzyko wystąpienia niezdefiniowanych informacji lub, co gorsza, błędów odwoływania się do obiektów, jeśli zdefiniowanie tych właściwości zależy od kodu wywołującego.

Czy są jakieś alternatywy dla tego czy tylko musisz zdecydować czy X Ilość argumentów konstruktora jest za dużo dla Ciebie?

Author: Aaronaught, 2008-09-02

14 answers

Dwa podejścia projektowe do rozważenia

The essence pattern

The fluent interface pattern

Oba są podobne pod względem intencji, ponieważ powoli budujemy obiekt pośredni, a następnie tworzymy nasz obiekt docelowy w jednym kroku.

Przykład płynnego interfejsu w działaniu to:

public class CustomerBuilder {
    String surname;
    String firstName;
    String ssn;
    public static CustomerBuilder customer() {
        return new CustomerBuilder();
    }
    public CustomerBuilder withSurname(String surname) {
        this.surname = surname; 
        return this; 
    }
    public CustomerBuilder withFirstName(String firstName) {
        this.firstName = firstName;
        return this; 
    }
    public CustomerBuilder withSsn(String ssn) {
        this.ssn = ssn; 
        return this; 
    }
    // client doesn't get to instantiate Customer directly
    public Customer build() {
        return new Customer(this);            
    }
}

public class Customer {
    private final String firstName;
    private final String surname;
    private final String ssn;

    Customer(CustomerBuilder builder) {
        if (builder.firstName == null) throw new NullPointerException("firstName");
        if (builder.surname == null) throw new NullPointerException("surname");
        if (builder.ssn == null) throw new NullPointerException("ssn");
        this.firstName = builder.firstName;
        this.surname = builder.surname;
        this.ssn = builder.ssn;
    }

    public String getFirstName() { return firstName;  }
    public String getSurname() { return surname; }
    public String getSsn() { return ssn; }    
}


import static com.acme.CustomerBuilder.customer;

public class Client {
    public void doSomething() {
        Customer customer = customer()
            .withSurname("Smith")
            .withFirstName("Fred")
            .withSsn("123XS1")
            .build();
    }
}
 101
Author: toolkit,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-05-14 14:43:50

Widzę, że niektórzy zalecają siedem jako górną granicę. Najwyraźniej nie jest prawdą, że ludzie mogą mieć w głowie siedem rzeczy na raz; pamiętają tylko cztery (Susan Weinschenk, [1]} 100 rzeczy, które każdy projektant musi wiedzieć o ludziach[2]}, 48). Mimo to uważam cztery za coś w rodzaju wysokiej orbity okołoziemskiej. Ale to dlatego, że moje myślenie zostało zmienione przez Boba Martina.

W czysty kod , Wujek Bob argumentuje za trzema jako ogólną górną granicę liczby parametrów. Twierdzenie radykalne (40):

Idealna liczba argumentów dla funkcji wynosi zero (niladic). Następnie pojawia się jeden (monadyczny), a następnie dwa (dyadyczny). W miarę możliwości należy unikać trzech argumentów (triadycznych). Więcej niż trzy (poliadyczne) wymaga bardzo specjalnego uzasadnienia-i tak nie powinno być używane.

Mówi to ze względu na czytelność; ale także ze względu na testowalność:

Wyobraź sobie trudność pisania wszystkich przypadki testowe, aby upewnić się, że wszystkie różne kombinacje argumentów działają poprawnie.

Zachęcam do znalezienia kopii jego książki i przeczytania jego pełnego omówienia argumentów funkcji (40-43).

Zgadzam się z tymi, którzy wymienili zasadę jednej odpowiedzialności. Trudno mi uwierzyć, że klasa, która potrzebuje więcej niż dwóch lub trzech wartości / obiektów bez rozsądnych wartości domyślnych, ma tylko jedną odpowiedzialność i nie byłoby lepiej z inną klasą / align = "left" /

Teraz, jeśli wstrzykiwasz swoje zależności przez konstruktor, argumenty Boba Martina o tym, jak łatwo jest wywołać konstruktor, nie mają zastosowania (ponieważ zazwyczaj jest tylko jeden punkt w Twojej aplikacji, w którym to przesyłasz, lub nawet masz framework, który robi to za Ciebie). Jednak zasada jednej odpowiedzialności jest nadal istotna: gdy klasa ma cztery zależności, uważam, że zapach, który robi dużą ilość praca.

Jednak, jak w przypadku wszystkich rzeczy w informatyce, istnieją bez wątpienia uzasadnione przypadki posiadania dużej liczby parametrów konstruktora. Nie przekręcaj kodu, aby uniknąć użycia dużej liczby parametrów; ale jeśli używasz dużej liczby parametrów, zatrzymaj się i zastanów się nad tym, ponieważ może to oznaczać, że Twój kod jest już wypaczony.

 23
Author: Keith Pinson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-07-15 11:51:00

W Twoim przypadku, trzymaj się konstruktora. Informacje należy do Klienta i 4 pola są w porządku.

W przypadku, gdy masz wiele wymaganych i opcjonalnych pól, konstruktor nie jest najlepszym rozwiązaniem. Jak powiedział @boojiboy, trudno jest czytać, a także trudno jest napisać kod klienta.

@ contagious zasugerował użycie domyślnego wzorca i setterów dla opcjonalnych atrybutów. To nakazuje, że pola są zmienne, ale to jest mały problem.

Joshua Block on Effective Java 2 mówi, że w tym przypadku należy rozważyć builder. Przykład zaczerpnięty z książki:

 public class NutritionFacts {  
   private final int servingSize;  
   private final int servings;  
   private final int calories;  
   private final int fat;  
   private final int sodium;  
   private final int carbohydrate;  

   public static class Builder {  
     // required parameters  
     private final int servingSize;  
     private final int servings;  

     // optional parameters  
     private int calories         = 0;  
     private int fat              = 0;  
     private int carbohydrate     = 0;  
     private int sodium           = 0;  

     public Builder(int servingSize, int servings) {  
      this.servingSize = servingSize;  
       this.servings = servings;  
    }  

     public Builder calories(int val)  
       { calories = val;       return this; }  
     public Builder fat(int val)  
       { fat = val;            return this; }  
     public Builder carbohydrate(int val)  
       { carbohydrate = val;   return this; }  
     public Builder sodium(int val)  
       { sodium = val;         return this; }  

     public NutritionFacts build() {  
       return new NutritionFacts(this);  
     }  
   }  

   private NutritionFacts(Builder builder) {  
     servingSize       = builder.servingSize;  
     servings          = builder.servings;  
     calories          = builder.calories;  
     fat               = builder.fat;  
     soduim            = builder.sodium;  
     carbohydrate      = builder.carbohydrate;  
   }  
}  

A następnie użyj go w ten sposób:

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).
      calories(100).sodium(35).carbohydrate(27).build();

Powyższy przykład został zaczerpnięty z Effective Java 2

I to nie dotyczy tylko konstruktora. Cytując Kenta Becka w wzorcach implementacji :

setOuterBounds(x, y, width, height);
setInnerBounds(x + 2, y + 2, width - 4, height - 4);

Uczynienie prostokąt jawnym jako obiekt lepiej wyjaśnia kod:

setOuterBounds(bounds);
setInnerBounds(bounds.expand(-2));
 13
Author: Marcio Aguiar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-02 19:26:11

Myślę, że twoje pytanie dotyczy bardziej konstrukcji klas niż liczby argumentów w konstruktorze. Gdybym potrzebował 20 kawałków danych (argumentów), aby pomyślnie zainicjować obiekt, prawdopodobnie rozważyłbym rozbicie klasy.

 5
Author: vitule,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-02 18:55:35

Myślę ,że odpowiedź "pure OOP" jest taka, że jeśli operacje na klasie są nieprawidłowe, gdy niektóre elementy nie są inicjowane, to te elementy muszą być ustawione przez konstruktor. Zawsze jest przypadek, w którym można użyć wartości domyślnych, ale zakładam, że nie rozważamy tego przypadku. Jest to dobre podejście, gdy API jest naprawione, ponieważ zmiana jednego dozwolonego konstruktora po upublicznieniu API będzie koszmarem dla Ciebie i wszystkich użytkowników Twojego kodu.

W C#, co I zrozum, że wytyczne dotyczące projektowania są takie, że niekoniecznie jest to jedyny sposób, aby poradzić sobie z sytuacją. Szczególnie w przypadku obiektów WPF, zauważysz, że klasy. NET mają tendencję do faworyzowania konstruktorów bez parametru i rzucają wyjątki, jeśli dane nie zostały zainicjowane do pożądanego stanu przed wywołaniem metody. Jest to prawdopodobnie głównie specyficzne dla projektowania opartego na komponentach; nie mogę wymyślić konkretnego przykładu klasy. NET, która zachowuje się w ten sposób. W Twoim przypadku zdecydowanie powoduje zwiększone obciążenie testowania, aby upewnić się, że Klasa nigdy nie zostanie zapisana do magazynu danych, chyba że właściwości zostały zweryfikowane. Szczerze mówiąc, z tego powodu wolałbym podejście "konstruktor ustawia wymagane właściwości", jeśli Twoje API jest albo ustawione w kamieniu, albo nie jest publiczne.

Jedyną rzeczą, jaką jestem jest to, że prawdopodobnie istnieje niezliczona ilość metod, które mogą rozwiązać ten problem, a każda z nich wprowadza swój własny zestaw problemów. The best thing to do jest nauczyć się jak najwięcej wzorów, jak to możliwe i wybrać najlepszy do pracy. (Czy to nie jest taka glina-z odpowiedzi?)

 4
Author: OwenP,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-02 18:55:05

Jeśli masz niepalatalnie wiele argumentów, po prostu spakuj je razem do klas structs / POD, najlepiej zadeklarowanych jako wewnętrzne klasy klasy, którą konstruujesz. W ten sposób nadal możesz wymagać pól podczas tworzenia kodu, który wywołuje konstruktor w sposób rozsądny.

 3
Author: McKenzieG1,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-02 18:48:09

Steve Mcconnell pisze w Code Complete, że ludzie mają problemy z utrzymaniem więcej 7 rzeczy w głowie na raz, więc to będzie numer, pod którym staram się pozostać.

 3
Author: Booji Boy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-02 18:52:00

Myślę, że wszystko zależy od sytuacji. Dla czegoś takiego jak twój przykład, klasa klienta, nie zaryzykowałbym szansy, że te dane będą niezdefiniowane w razie potrzeby. Z drugiej strony, przekazanie struktury wyczyściłoby listę argumentów, ale nadal mielibyśmy wiele rzeczy do zdefiniowania w strukturze.

 2
Author: Ethan Gunderson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-02 18:50:50

Myślę, że najłatwiej byłoby znaleźć akceptowalną wartość domyślną dla każdej wartości. W tym przypadku, każde pole wygląda tak, jakby było wymagane do skonstruowania, więc ewentualnie przeciążenie wywołania funkcji tak, że jeśli coś nie jest zdefiniowane w wywołaniu, ustawić go na domyślne.

Następnie wykonaj funkcje getter i setter dla każdej właściwości, tak aby wartości domyślne mogły zostać zmienione.

Implementacja Javy:

public static void setEmail(String newEmail){
    this.email = newEmail;
}

public static String getEmail(){
    return this.email;
}

Jest to również dobra praktyka, aby zachować swoje zmienne globalne zabezpieczony.

 2
Author: helloandre,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-02 18:51:22

Styl dużo się liczy i wydaje mi się, że jeśli istnieje konstruktor z 20+ argumentami, to projekt powinien zostać zmieniony. Podaj rozsądne wartości domyślne.

 2
Author: Adam Hollidge,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-08-13 15:59:47

Zamknąłbym podobne pola w obiekt z własną logiką budowy/walidacji.

Powiedzmy na przykład, jeśli masz

  • Biznesphone
  • BusinessAddress
  • HomePhone
  • HomeAddress

Stworzyłbym klasę, która przechowuje telefon i adres wraz z tagiem określającym wether jako" domowy "lub" biznesowy " Telefon/Adres. A następnie zmniejsz 4 pola do tylko tablicy.

ContactInfo cinfos = new ContactInfo[] {
    new ContactInfo("home", "+123456789", "123 ABC Avenue"),
    new ContactInfo("biz", "+987654321", "789 ZYX Avenue")
};

Customer c = new Customer("john", "doe", cinfos);

To powinno wyglądać mniej jak spaghetti.

Z pewnością, jeśli masz dużo pól, musi być jakiś wzór, który możesz wyodrębnić, który byłby ładną jednostką funkcji. I zrobić bardziej czytelny kod też.

I możliwe są również następujące rozwiązania:

  • rozłóż logikę walidacji zamiast przechowywać ją w jednej klasie. Sprawdzanie poprawności podczas wprowadzania danych przez użytkownika, a następnie sprawdzanie poprawności na warstwie bazy danych itd...
  • Stwórz CustomerFactory klasę, która pomoże mi skonstruować Customers
  • @marcio rozwiązanie też jest ciekawe...
 1
Author: chakrit,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-02 19:38:27

Po prostu Użyj domyślnych argumentów. W języku, który obsługuje domyślne argumenty metod (na przykład PHP), można to zrobić w podpisie metody:

public function doSomethingWith($this = val1, $this = val2, $this = val3)

Istnieją inne sposoby tworzenia wartości domyślnych, np. w językach obsługujących przeciążanie metod.

Oczywiście możesz również ustawić wartości domyślne podczas deklarowania pól, jeśli uznasz to za stosowne.

To naprawdę sprowadza się do tego, czy jest to właściwe, aby ustawić te wartości domyślne, lub jeśli obiekty powinny być specced out w budowie przez cały czas. To naprawdę decyzja, którą tylko Ty możesz podjąć.

 0
Author: Brian Warshaw,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-02 18:49:29

Zgadzam się na limit 7 pozycji boojiboy wspomina. Poza tym warto spojrzeć na anonimowe (lub specjalistyczne) typy, IDictionary lub indirection za pomocą klucza głównego do innego źródła danych.

 0
Author: spoulson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-02 18:54:49

O ile nie jest to więcej niż 1 argument, zawsze używam tablic lub obiektów jako parametrów konstruktora i polegam na sprawdzaniu błędów, aby upewnić się, że wymagane parametry są tam.

 -1
Author: Aldie,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-02 18:47:40