Сколько аргументов конструктора слишком много? - PullRequest
121 голосов
/ 02 сентября 2008

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

  • UserName
  • E-mail
  • Имя
  • Фамилия

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

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

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

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

Ответы [ 14 ]

108 голосов
/ 02 сентября 2008

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

Шаблон сущность

свободный интерфейс шаблон

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

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

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();
    }
}
26 голосов
/ 20 марта 2013

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

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

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

Он говорит это из-за читабельности; но также из-за тестируемости:

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

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

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

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

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

13 голосов
/ 02 сентября 2008

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

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

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

Джошуа Блок по эффективной 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();

Приведенный выше пример взят из Effective Java 2

И это относится не только к конструктору. Цитирование Кента Бека в Шаблонах реализации :

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

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

setOuterBounds(bounds);
setInnerBounds(bounds.expand(-2));
5 голосов
/ 02 сентября 2008

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

5 голосов
/ 02 сентября 2008

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

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

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

4 голосов
/ 02 сентября 2008

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

3 голосов
/ 02 сентября 2008

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

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

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

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

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

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

3 голосов
/ 02 сентября 2008

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

3 голосов
/ 02 сентября 2008

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

2 голосов
/ 02 сентября 2008

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

...