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





Если не более одного аргумента, я всегда использую массивы или объекты в качестве параметров конструктора и полагаюсь на проверку ошибок, чтобы убедиться в наличии необходимых параметров.
Это ужасная идея, потому что в этом случае у вас нет компилятора, проверяющего неверные аргументы, никаких подсказок типа, ...
Если у вас слишком много аргументов, просто упакуйте их вместе в структуры / классы 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. Очевидно, это было опровергнуто: четыре - более точный верхний предел.
Я согласен с ограничением в 7 предметов, которое упоминает Буджибой. Помимо этого, возможно, стоит обратить внимание на анонимные (или специализированные) типы, IDictionary или косвенное обращение через первичный ключ к другому источнику данных.
Я думаю, что "чистый ООП" ответ заключается в том, что если операции с классом недействительны, когда определенные члены не инициализированы, тогда эти члены должны быть установлены конструктором. Всегда есть случай, когда можно использовать значения по умолчанию, но я предполагаю, что мы не рассматриваем этот случай. Это хороший подход, когда API исправлен, потому что изменение единственного допустимого конструктора после того, как API станет общедоступным, станет кошмаром для вас и всех пользователей вашего кода.
Что я понимаю в рекомендациях по дизайну C#, так это то, что это не обязательно единственный способ справиться с ситуацией. В частности, с объектами WPF вы обнаружите, что классы .NET предпочитают конструкторы без параметров и будут вызывать исключения, если данные не были инициализированы в желаемое состояние перед вызовом метода. Это, вероятно, в основном относится к компонентному дизайну; Я не могу привести конкретный пример класса .NET, который ведет себя подобным образом. В вашем случае это определенно вызовет повышенную нагрузку на тестирование, чтобы гарантировать, что класс никогда не сохраняется в хранилище данных, если свойства не были проверены. Честно говоря, из-за этого я бы предпочел подход «конструктор устанавливает требуемые свойства», если ваш API либо установлен в камне, либо не является общедоступным.
Единственное, в чем я уверен, являюсь - это то, что существует, вероятно, бесчисленное множество методологий, которые могут решить эту проблему, и каждая из них представляет свой собственный набор проблем. Лучше всего выучить как можно больше шаблонов и выбрать лучший для работы. (Разве это не отговорка от ответа?)
Я думаю, ваш вопрос больше касается дизайна ваших классов, чем количества аргументов в конструкторе. Если бы мне понадобилось 20 частей данных (аргументов) для успешной инициализации объекта, я бы, вероятно, подумал о разделении класса.
Иногда это просто невозможно. Рассмотрим файл Excel с 50 столбцами, которые необходимо обработать. Идея класса MyExcelFileLine с конструктором с 50 аргументами довольно пугает.
Два подхода к проектированию, которые следует учитывать
Паттерн сущность
Паттерн свободный интерфейс
Оба они похожи по замыслу в том, что мы медленно создаем промежуточный объект, а затем за один шаг создаем наш целевой объект.
Примером беглого интерфейса в действии может быть:
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
Можете ли вы разделить вызывающий и вызываемый сегменты кода, чтобы было более ясно, что они являются отдельными объектами?
Проверка нулевых значений (surname == null || firstName == null || ssn == null) должна выполняться НА ОБЪЕКТЕ Customer. Это связано с возможностью изменения этих значений до создания объекта Customer. Кроме того, убедитесь, что все поля в классе Customer объявлены как «закрытые окончательные», что делает Customer неизменяемым.
@Frank, ваши ссылки сейчас кажутся неправильными, «Идиома именованных параметров» теперь находится на parashift.com/c++-faq-lite/ named-parameter-idiom.html «Идиома именованных конструкторов» теперь здесь: parashift.com/c++-faq-lite/ named-ctor-idiom.html
Мне определенно нравится беглость клиентского кода, но мне не нравится дублирование переменных экземпляра в CustomerBuilder и Customer. Кроме того, этот пример хорош, если все переменные экземпляра являются необязательными, но если все они обязательны и у вас их десятки, то я не уверен, что вы сможете избежать конструктора со всеми этими аргументами. Если у вас нет конструктора со всеми этими обязательными атрибутами, то я, как кодировщик клиента, не смогу увидеть это требование через интерфейс класса, который я собираюсь создать, а это то, чего я бы не стал. подобно.
Разве CustomerBuilder не больше похож на DTO?
Я бы предложил НЕ бросать исключение NullPointException при проверке, является ли аргумент нулевым. NPE не для этого. Лучше выбросить IllegalArgumentException («Брошено, чтобы указать, что методу был передан недопустимый или несоответствующий аргумент». См. docs.oracle.com/javase/7/docs/api/java/lang/…)
Хотя эти шаблоны ценны, они не отвечают на вопрос «Сколько аргументов конструктора слишком много». Кроме того, Камил прав: это похоже на DTO, а не на шаблон построителя. В шаблоне построителя обычно есть метод, называемый .build (), который предоставляет экземпляр. ИМХО, конструкторы дают немного больше свободы в выборе того, сколько параметров они могут принимать. С другой стороны, я считаю, что методы экземпляра должны быть более строгими в этом смысле, 0–3 должно быть хорошим практическим правилом. В любом случае, любое число больше 7 звучит как запах кода.
У вас есть опечатка в конце: должно быть CustomerBuilder, а не Customer в методе doSomething клиентского класса.
@mybirthname - нет, опечатки нет
@toolkit ооо, извините, я ошибся, я не видел клиента статического метода, и на первый взгляд он выглядел как класс Customer. В любом случае, я поддержал предыдущий комментарий, действительно хороший ответ.
Это не отвечает на вопрос, и шаблон в примере - это шаблон построителя, который является хорошо документированным шаблоном создания. Нет такой вещи, как беглый узор. Это просто стиль.
Я использую для линтинга стиль javascript и airbnb, это не позволяет создавать два класса в одном файле. Этот шаблон приводит к проблеме циклической зависимости, поскольку CustomerBuilder зависит от Customer, а Customer зависит от CustomerBuilder. Как мне решить эту проблему?
В вашем случае придерживайтесь конструктора. Информация находится в поле «Заказчик», и 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));
Конечно, если в конструкторе требуются все аргументы, вы просто переместите один огромный конструктор из одного места в другое.
Я знаю, что это было написано некоторое время назад, но мне нравится это решение. Теперь, когда используются именованные параметры, считается ли это хорошей практикой?
Я бы инкапсулировал подобные поля в отдельный объект с собственной логикой построения / проверки.
Скажем, например, если у вас есть
Я бы сделал класс, который хранит телефон и адрес вместе с тегом, указывающим, является ли это «домашним» или «служебным» телефоном / адресом. А затем сократите 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.Я вижу, что некоторые люди рекомендуют семь в качестве верхнего предела. Очевидно, неправда, что люди могут держать в голове сразу семь вещей; они могут помнить только четыре (Сьюзан Вайншенк, 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?
Мне всегда было интересно, как это работает с «классами данных», которые существуют только для хранения связанных данных. Если вы примените это к вопросу OP, его класс просто хранит данные для клиента. Есть мысли, как в таком случае можно уменьшить параметры?
@Puneet, также есть аналогичная критика, когда конструктор может принимать всего 3 аргумента, но все эти аргументы являются большими составными классами. По сути, вы отправляете конструктору 60 параметров, просто они упакованы.
Кстати, после того, как я стал функциональным программистом, я не стал учеником дяди Боба, каким был. Я больше не полностью согласен с этим ответом.
Я провел всего пару глав в «Чистом коде», но, читая о монадических, диадических и триадных функциях, я задавался вопросом, были ли конструкторы исключением или нет. Я не помню, чтобы он четко проводил различие. Я вижу, что в попытке избежать функций диадики / триады (или чего-то большего) можно создать класс для обертывания аргументов. Но на данном этапе при создании класса-оболочки автор, похоже, не дает рекомендаций по определению свойств класса-оболочки.
В более объектно-ориентированной ситуации проблемы вы можете использовать свойства в 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]";
}
На мой взгляд, это намного более чистый код, и в этом случае подрядчики не нужны, что оставляет место для других необходимых параметров.
Что ж, очевидный ответ - больше, чем вам нужно.