Сколько аргументов конструктора слишком много?

Скажем, у вас есть класс Customer, который содержит следующие поля:

  • UserName
  • Email
  • Имя
  • Фамилия

Предположим также, что в соответствии с вашей бизнес-логикой все объекты Customer должны иметь эти четыре свойства.

Теперь мы можем сделать это довольно легко, заставив конструктор указать каждое из этих свойств. Но довольно легко понять, как это может вырваться из-под контроля, когда вы вынуждены добавлять дополнительные объекты к объекту Customer.

Я видел классы, которые принимают более 20 аргументов в свой конструктор, и это просто боль, чтобы их использовать. Но, в качестве альтернативы, если вы не требуете этих полей, вы рискуете иметь undefined информацию или, что еще хуже, объекты, ссылающиеся на ошибки, если вы полагаетесь на вызывающий код, чтобы указать эти свойства.

Есть ли альтернативы этому или вам просто нужно решить, слишком ли много аргументов X аргументов конструктора для вас?

Ответ 1

Два подхода к рассмотрению

суть шаблон

плавный интерфейс

Они оба похожи в намерении, поскольку мы медленно создаем промежуточный объект, а затем создаем наш целевой объект за один шаг.

Примером свободного интерфейса в действии будет:

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();
    }
}

Ответ 2

Я вижу, что некоторые люди рекомендуют семь в качестве верхнего предела. По-видимому, неправда, что люди могут держать в голове семь вещей сразу; они могут помнить только четыре (Сьюзан Вайншенк, 100 вещей, которые каждый дизайнер должен знать о людях, 48). Тем не менее, я считаю, что четыре - это что-то вроде высокой земной орбиты. Но это потому, что мое мышление было изменено Бобом Мартином.

В "Чистом коде" дядя Боб утверждает, что три в качестве общего верхнего предела для числа параметров. Он делает радикальное требование (40):

Идеальное число аргументов для функции равно нулю (niladic). Далее идет одно (монадическое), за которым следуют два (диадический). По возможности следует избегать трех аргументов (триад). Более трех (полиадических) требует особого оправдания &mdash, а затем не должно использоваться в любом случае.

Он говорит это из-за удобочитаемости; но и из-за проверяемости:

Представьте себе трудность написания всех тестовых примеров, чтобы обеспечить правильную работу всех различных комбинаций аргументов.

Я призываю вас найти копию его книги и прочитать его полное обсуждение аргументов функции (40-43).

Я согласен с теми, кто упомянул принцип единой ответственности. Мне трудно поверить, что класс, который нуждается в более чем двух или трех значениях/объектах без разумных значений по умолчанию, действительно имеет только одну ответственность и не будет лучше с извлеченным другим классом.

Теперь, если вы вводите свои зависимости через конструктор, аргументы Боба Мартина о том, как легко вызвать конструктор, не так сильно применяются (потому что обычно тогда в приложении есть только одна точка, где вы подключаете это, или у вас даже есть фреймворк, который делает это для вас). Тем не менее, принцип единой ответственности все еще имеет значение: как только класс имеет четыре зависимости, я считаю, что этот запах делает большой объем работы.

Однако, как и во всех вещах в области информатики, существуют несомненные действительные случаи наличия большого числа параметров конструктора. Не прерывайте свой код, чтобы избежать использования большого количества параметров; но если вы используете большое количество параметров, остановитесь и подумайте, потому что это может означать, что ваш код уже искажен.

Ответ 3

В вашем случае придерживайтесь конструктора. Информация принадлежит Клиенту и 4 поля в порядке.

В случае, когда у вас много обязательных и необязательных полей, конструктор не является лучшим решением. Как сказал @boojiboy, его трудно читать, а также сложно написать код клиента.

@contagious предложил использовать шаблон по умолчанию и настройки для необязательных атрибутов. Это означает, что поля изменяемы, но это небольшая проблема.

Joshua Block on Effective Java 2 говорит, что в этом случае вы должны рассмотреть строителя. Пример из книги:

 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;  
   }  
}  

И затем используйте его следующим образом:

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

Приведенный выше пример был взят из Эффективная Java 2

И это относится не только к конструктору. Citation Kent Beck в Шаблоны внедрения:

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

Создание прямоугольника в явном виде как объект объясняет код лучше:

setOuterBounds(bounds);
setInnerBounds(bounds.expand(-2));

Ответ 4

Я думаю, что ваш вопрос больше о дизайне ваших классов, чем о количестве аргументов в конструкторе. Если бы мне понадобилось 20 частей данных (аргументов) для успешной инициализации объекта, я бы, вероятно, подумал о разложении класса.

Ответ 5

Я думаю, что ответ "чистого ООП" заключается в том, что если операции над классом недействительны, когда некоторые члены не инициализируются, то эти члены должны быть заданы конструктором. Всегда есть случай, когда значения по умолчанию могут использоваться, но я предполагаю, что мы не рассматриваем этот случай. Это хороший подход, когда API исправлен, потому что изменение единого допустимого конструктора после того, как API станет общедоступным, станет кошмаром для вас и всех пользователей вашего кода.

В С# то, что я понимаю в руководстве по дизайну, заключается в том, что это не единственный способ справиться с ситуацией. В частности, с объектами WPF вы обнаружите, что классы .NET склонны использовать конструкторы без параметров и будут генерировать исключения, если данные не были инициализированы до желаемого состояния перед вызовом метода. Это, вероятно, в основном характерно для разработки на основе компонентов; Я не могу придумать конкретный пример .NET-класса, который ведет себя таким образом. В вашем случае это определенно вызовет повышенную нагрузку на тестирование, чтобы гарантировать, что класс никогда не сохраняется в хранилище данных, если свойства не были проверены. Честно говоря, из-за этого я предпочел бы, чтобы подход "конструктор устанавливает требуемые свойства", если ваш API либо установлен в камне, либо не публичен.

Единственное, что я уверен в том, что существует, вероятно, множество методологий, которые могут решить эту проблему, и каждый из них представляет свой собственный набор проблем. Лучшее, что нужно сделать, это узнать как можно больше моделей и выбрать лучший для работы. (Разве это не такой ответ?)

Ответ 6

Если у вас есть неприятно много аргументов, просто упакуйте их вместе в классы structs/POD, предпочтительно объявленные как внутренние классы класса, который вы строите. Таким образом, вы по-прежнему можете требовать поля при создании кода, который вызывает конструктор, который может быть удобочитаемым.

Ответ 7

Стив Макконнелл пишет в Code Complete, что у людей есть проблемы с сохранением более 7 вещей в голове за раз, так что это будет число, в котором я пытаюсь остаться.

Ответ 8

Стиль очень много, и мне кажется, что если есть конструктор с 20 + аргументами, тогда дизайн должен быть изменен. Предоставьте разумные значения по умолчанию.

Ответ 9

Я думаю, все зависит от ситуации. Для чего-то вроде вашего примера, класса клиента, я не рискнул бы, если бы эти данные были undefined, когда это было необходимо. С другой стороны, передача структуры очистит список аргументов, но у вас все еще будет много чего определить в структуре.

Ответ 10

Я бы подумал, что самым простым способом было бы найти приемлемое значение по умолчанию для каждого значения. В этом случае каждое поле выглядит так, как будто оно потребуется для построения, поэтому возможно перегрузить вызов функции, чтобы, если что-то не определено в вызове, установите его по умолчанию.

Затем создайте функции getter и setter для каждого свойства, чтобы значения по умолчанию могли быть изменены.

Реализация Java:

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

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

Это также хорошая практика для обеспечения безопасности глобальных переменных.

Ответ 11

Я бы инкапсулировал подобные поля в собственный объект с собственной логикой построения/проверки.

Скажите, например, если у вас

  • BusinessPhone
  • BusinessAddress
  • HomePhone
  • HomeAddress

Я бы сделал класс, в котором хранятся телефон и адрес вместе с тегом, определяющим, что это "домашний" или "деловой" телефон/адрес. А затем уменьшите 4 поля до всего массива.

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);

Это должно сделать его менее похожим на спагетти.

Конечно, если у вас много полей, должен быть какой-то шаблон, который вы можете извлечь, что сделало бы красивую единицу функции. И сделайте также для более читаемого кода.

И следующие решения:

  • Разложите логику проверки вместо сохранения в одном классе. Подтвердите, когда пользователь вводит их, а затем снова проверяет на уровне базы данных и т.д.
  • Создайте класс CustomerFactory, который поможет мне построить Customer s
  • Решение @marcio также интересно...

Ответ 12

Если это не более одного аргумента, я всегда использую массивы или объекты в качестве параметров конструктора и полагаюсь на проверку ошибок, чтобы убедиться в наличии необходимых параметров.

Ответ 13

Просто используйте аргументы по умолчанию. На языке, который поддерживает аргументы метода по умолчанию (например, PHP), вы можете сделать это в сигнатуре метода:

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

Существуют и другие способы создания значений по умолчанию, например, на языках, поддерживающих перегрузку метода.

Конечно, вы также можете установить значения по умолчанию при объявлении полей, если сочтете это целесообразным.

На самом деле все сводится к тому, действительно ли вам нужно установить эти значения по умолчанию, или если ваши объекты должны быть конкретизированы при построении все время. Это действительно решение, которое вы можете сделать только.

Ответ 14

Я согласен на ограничение 7 пунктов, о которых упоминает Буджибой. Помимо этого, возможно, стоит рассмотреть анонимные (или специализированные) типы, IDictionary или косвенное обращение через первичный ключ к другому источнику данных.