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

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

  • Имя пользователя
  • Электронное письмо
  • Имя
  • Фамилия

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

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

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

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

Что ж, очевидный ответ - больше, чем вам нужно.

Jodrell 01.11.2019 17:41
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
146
1
69 274
15
Перейти к ответу Данный вопрос помечен как решенный

Ответы 15

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

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

Verim 05.10.2018 12:24

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Keith Pinson 28.01.2014 18:21

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

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

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

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

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

Иногда это просто невозможно. Рассмотрим файл Excel с 50 столбцами, которые необходимо обработать. Идея класса MyExcelFileLine с конструктором с 50 аргументами довольно пугает.

anar khalilov 28.03.2016 16:07
Ответ принят как подходящий

Два подхода к проектированию, которые следует учитывать

Паттерн сущность

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

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

Примером беглого интерфейса в действии может быть:

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

Я знаю это как «Идиому именованных параметров»: parashift.com/c++-faq-lite/ctors.html#faq-10.18. Связанный: Существует также "Идиома именованного конструктора": parashift.com/c++-faq-lite/ctors.html#faq-10.8

Frank 03.02.2009 08:06

Можете ли вы разделить вызывающий и вызываемый сегменты кода, чтобы было более ясно, что они являются отдельными объектами?

Chris K 13.08.2009 20:32

Проверка нулевых значений (surname == null || firstName == null || ssn == null) должна выполняться НА ОБЪЕКТЕ Customer. Это связано с возможностью изменения этих значений до создания объекта Customer. Кроме того, убедитесь, что все поля в классе Customer объявлены как «закрытые окончательные», что делает Customer неизменяемым.

Andriy Drozdyuk 10.08.2010 23:00

@Frank, ваши ссылки сейчас кажутся неправильными, «Идиома именованных параметров» теперь находится на parashift.com/c++-faq-lite/ named-parameter-idiom.html «Идиома именованных конструкторов» теперь здесь: parashift.com/c++-faq-lite/ named-ctor-idiom.html

Lennart 11.08.2013 15:41

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

dragan.stepanovic 17.06.2015 18:11

Разве CustomerBuilder не больше похож на DTO?

Kamil Latosinski 23.04.2017 23:00

Я бы предложил НЕ бросать исключение NullPointException при проверке, является ли аргумент нулевым. NPE не для этого. Лучше выбросить IllegalArgumentException («Брошено, чтобы указать, что методу был передан недопустимый или несоответствующий аргумент». См. docs.oracle.com/javase/7/docs/api/java/lang/…)

Grmpfhmbl 04.05.2017 16:27

Хотя эти шаблоны ценны, они не отвечают на вопрос «Сколько аргументов конструктора слишком много». Кроме того, Камил прав: это похоже на DTO, а не на шаблон построителя. В шаблоне построителя обычно есть метод, называемый .build (), который предоставляет экземпляр. ИМХО, конструкторы дают немного больше свободы в выборе того, сколько параметров они могут принимать. С другой стороны, я считаю, что методы экземпляра должны быть более строгими в этом смысле, 0–3 должно быть хорошим практическим правилом. В любом случае, любое число больше 7 звучит как запах кода.

JavierIEH 01.06.2017 04:20

У вас есть опечатка в конце: должно быть CustomerBuilder, а не Customer в методе doSomething клиентского класса.

mybirthname 28.10.2017 19:14

@mybirthname - нет, опечатки нет

toolkit 02.11.2017 19:15

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

mybirthname 02.11.2017 21:39

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

dRamentol 29.03.2020 21:34

Я использую для линтинга стиль javascript и airbnb, это не позволяет создавать два класса в одном файле. Этот шаблон приводит к проблеме циклической зависимости, поскольку CustomerBuilder зависит от Customer, а Customer зависит от CustomerBuilder. Как мне решить эту проблему?

AdityaGovardhan 27.05.2020 06:22

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

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

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

Джошуа Блок в «Эффективной 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

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

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

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

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

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

Andriy Drozdyuk 10.08.2010 23:11

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

eaglei22 23.04.2019 17:50

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

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

  • BusinessPhone
  • BusinessAddress
  • Домашний телефон
  • Домашний адрес

Я бы сделал класс, который хранит телефон и адрес вместе с тегом, указывающим, является ли это «домашним» или «служебным» телефоном / адресом. А затем сократите 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.
  • Интересно и решение @ marcio ...

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

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

The ideal number of arguments for a function is zero (niladic). Next comes one (monadic) followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible. More than three (polyadic) requires very special justification—and then shouldn't be used anyway.

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

Imagine the difficulty of writing all the test cases to ensure that all various combinations of arguments work properly.

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

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

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

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

Я никогда не передаю аргументы конструкторам ... Я передаю их все в функции инициализации, а аргументом является 1 объект, содержащий все необходимые аргументы. Но тогда я использую javascript ... Что такое Java?

andygoestohollywood 06.02.2015 04:36

Мне всегда было интересно, как это работает с «классами данных», которые существуют только для хранения связанных данных. Если вы примените это к вопросу OP, его класс просто хранит данные для клиента. Есть мысли, как в таком случае можно уменьшить параметры?

0cd 07.12.2016 06:08

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

LegendLength 15.07.2017 13:48

Кстати, после того, как я стал функциональным программистом, я не стал учеником дяди Боба, каким был. Я больше не полностью согласен с этим ответом.

Keith Pinson 15.07.2017 14:52

Я провел всего пару глав в «Чистом коде», но, читая о монадических, диадических и триадных функциях, я задавался вопросом, были ли конструкторы исключением или нет. Я не помню, чтобы он четко проводил различие. Я вижу, что в попытке избежать функций диадики / триады (или чего-то большего) можно создать класс для обертывания аргументов. Но на данном этапе при создании класса-оболочки автор, похоже, не дает рекомендаций по определению свойств класса-оболочки.

eaglei22 23.04.2019 17:41

В более объектно-ориентированной ситуации проблемы вы можете использовать свойства в C#. Создание экземпляра объекта не очень помогает, но предположим, что у нас есть родительский класс, которому требуется слишком много параметров в своем конструкторе. Поскольку у вас могут быть абстрактные свойства, вы можете использовать это в своих интересах. Родительский класс должен определить абстрактное свойство, которое дочерний класс должен переопределить. Обычно класс может выглядеть так:

class Customer {
    private string name;
    private int age;
    private string email;

    Customer(string name, int age, string email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }
}

class John : Customer {
    John() : base("John", 20, "[email protected]") { 

    }
}

При слишком большом количестве параметров он может стать беспорядочным и нечитаемым. Тогда как этот метод:

class Customer {
    protected abstract string name { get; }
    protected abstract int age { get; }
    protected abstract string email { get; }
}

class John : Customer {
    protected override string name => "John";
    protected override int age => 20;
    protected override string email=> "[email protected]";
}

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

Другие вопросы по теме