Сколько аргументов конструктора слишком много?
Допустим, у вас есть класс Customer, который содержит следующие поля:
- UserName
- Эл. адрес
- Имя
- Фамилия
Скажем также, что в соответствии с вашей бизнес-логикой все объекты Customer должны иметь эти четыре свойства.
Теперь мы можем сделать это довольно легко, заставив конструктор указать каждое из этих свойств. Но довольно легко увидеть, как это может выйти из-под контроля, когда вы вынуждены добавить дополнительные обязательные поля в объект Customer.
Я видел классы, которые принимают в своем конструкторе более 20 аргументов, и использовать их было просто больно. Но, в качестве альтернативы, если вам не требуются эти поля, вы рискуете получить неопределенную информацию или, что еще хуже, ссылаться на ошибки объекта, если для определения этих свойств вы используете вызывающий код.
Есть ли альтернативы этому или вам просто нужно решить, слишком ли много X аргументов конструктора для вас, чтобы жить с ним?
14 ответов
Два дизайнерских подхода к рассмотрению
Сущность картины
Они оба похожи по замыслу в том, что мы медленно создаем промежуточный объект, а затем создаем наш целевой объект за один шаг.
Примером свободного интерфейса в действии будет:
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();
}
}
Я вижу, что некоторые люди рекомендуют семь как верхний предел. Очевидно, это не правда, что люди могут держать в голове семь вещей одновременно; они могут вспомнить только четыре (Сьюзен Вайншенк, 100 вещей, которые каждый дизайнер должен знать о людях, 48). Несмотря на это, я считаю, что четыре - что-то вроде высокой околоземной орбиты. Но это потому, что мое мышление было изменено Бобом Мартином.
В " Чистом коде" дядя Боб утверждает три для общего верхнего предела количества параметров. Он делает радикальное утверждение (40):
Идеальное число аргументов для функции равно нулю (niladic). Далее следует одно (монадическое), за которым следует два (диадическое). По возможности следует избегать трех аргументов (триадных). Более трех (полиадических) требует особого обоснования, и их не следует использовать в любом случае.
Он говорит это из-за читабельности; но также из-за тестируемости:
Представьте себе трудность написания всех тестовых случаев, чтобы гарантировать, что все различные комбинации аргументов работают правильно.
Я призываю вас найти копию его книги и прочитать его полное обсуждение функциональных аргументов (40-43).
Я согласен с теми, кто упомянул принцип единой ответственности. Мне трудно поверить, что класс, которому нужно более двух или трех значений / объектов без разумных значений по умолчанию, действительно несет только одну ответственность, и не будет лучше, если извлечен другой класс.
Теперь, если вы вводите свои зависимости через конструктор, аргументы Боба Мартина о том, как легко вызвать конструктор, не так уж применимы (потому что обычно в вашем приложении есть только одна точка, где вы подключаете это, или вы даже есть рамки, которые делают это за вас). Тем не менее, принцип единой ответственности по-прежнему актуален: если у класса четыре зависимости, я считаю, что он пахнет большой работой.
Однако, как и во всех вещах в области компьютерных наук, существуют бесспорные обоснованные случаи наличия большого количества параметров конструктора. Не искажайте ваш код, чтобы избежать использования большого количества параметров; но если вы используете большое количество параметров, остановитесь и подумайте, потому что это может означать, что ваш код уже искажен.
В вашем случае придерживайтесь конструктора. Информация принадлежит клиенту и 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();
Приведенный выше пример был взят из Effective Java 2
И это относится не только к конструктору. Цитирование Кента Бека в шаблонах реализации:
setOuterBounds(x, y, width, height);
setInnerBounds(x + 2, y + 2, width - 4, height - 4);
Делая прямоугольник явным как объект, лучше объясняется код:
setOuterBounds(bounds);
setInnerBounds(bounds.expand(-2));
Я думаю, что "чистый ООП" ответ заключается в том, что если операции над классом недопустимы, когда определенные члены не инициализированы, то эти члены должны быть установлены конструктором. Всегда есть случай, когда можно использовать значения по умолчанию, но я предполагаю, что мы не рассматриваем этот случай. Это хороший подход, когда API исправлен, потому что изменение единственного допустимого конструктора после того, как API станет общедоступным, станет кошмаром для вас и всех пользователей вашего кода.
В C#, что я понимаю о руководящих принципах проектирования, это то, что это не обязательно единственный способ справиться с ситуацией. В частности, с объектами WPF вы обнаружите, что классы.NET предпочитают конструкторы без параметров и будут генерировать исключения, если данные не были инициализированы в желаемое состояние перед вызовом метода. Это, вероятно, в основном характерно для компонентно-ориентированного проектирования; Я не могу придумать конкретный пример класса.NET, который ведет себя таким образом. В вашем случае это определенно увеличит нагрузку на тестирование, чтобы гарантировать, что класс никогда не будет сохранен в хранилище данных, если свойства не были проверены. Честно говоря, из-за этого я бы предпочел подход "конструктор устанавливает требуемые свойства", если ваш API установлен в камне или не общедоступен.
Одна вещь, в которой я уверен, - это то, что, вероятно, существует множество методологий, которые могут решить эту проблему, и каждая из них представляет свой собственный набор проблем. Лучше всего выучить как можно больше шаблонов и выбрать лучший для работы. (Разве это не такая отговорка от ответа?)
Я думаю, что ваш вопрос больше касается дизайна ваших классов, чем количества аргументов в конструкторе. Если бы мне понадобилось 20 частей данных (аргументов) для успешной инициализации объекта, я бы, вероятно, подумал о том, чтобы разбить класс.
Если у вас есть неимоверно много аргументов, просто объедините их в классы Struct / POD, предпочтительно объявленные как внутренние классы создаваемого вами класса. Таким образом, вы все еще можете требовать поля, делая код, который вызывает конструктор, достаточно читабельным.
Я думаю, что все зависит от ситуации. Что-то вроде вашего примера, класса клиентов, я бы не рискнул, если эти данные будут неопределенными при необходимости. С другой стороны, передача структуры очистит список аргументов, но вам все равно придется многое определить в структуре.
Я бы инкапсулировал подобные поля в собственный объект со своей собственной логикой построения / проверки.
Скажем, например, если у вас есть
- 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
s - Решение @ Marcio также интересно...
Стив Макконнелл пишет в Code Complete, что у людей возникают проблемы с тем, чтобы держать в голове больше 7 вещей за раз, так что это будет число, под которым я пытаюсь остаться.
Стиль имеет большое значение, и мне кажется, что если есть конструктор с более чем 20 аргументами, то дизайн должен быть изменен. Предоставьте разумные значения по умолчанию.
Я думаю, что самый простой способ - найти приемлемое значение по умолчанию для каждого значения. В этом случае каждое поле выглядит так, как будто его необходимо построить, поэтому возможно перегрузите вызов функции, чтобы, если что-то не было определено в вызове, установить его по умолчанию.
Затем создайте функции getter и setter для каждого свойства, чтобы можно было изменить значения по умолчанию.
Реализация Java:
public static void setEmail(String newEmail){
this.email = newEmail;
}
public static String getEmail(){
return this.email;
}
Это также хорошая практика для обеспечения безопасности ваших глобальных переменных.
В более объектно-ориентированной ситуации проблемы вы можете использовать свойства в 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, "John@email.com") {
}
}
При слишком большом количестве параметров он может стать беспорядочным и нечитаемым.
Тогда как этот метод:
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=> "John@email.com";
}
На мой взгляд, это намного более чистый код, и в этом случае подрядчики не нужны, что оставляет место для других необходимых параметров.
Просто используйте аргументы по умолчанию. На языке, который поддерживает аргументы метода по умолчанию (например, PHP), вы можете сделать это в сигнатуре метода:
public function doSomethingWith($this = val1, $this = val2, $this = val3)
Существуют и другие способы создания значений по умолчанию, например, в языках, поддерживающих перегрузку методов.
Конечно, вы также можете установить значения по умолчанию при объявлении полей, если сочтете это целесообразным.
На самом деле все сводится к тому, уместно ли вам устанавливать эти значения по умолчанию или нужно, чтобы ваши объекты постоянно находились в процессе разработки. Это действительно решение, которое может принять только ты.
Я согласен на ограничение в 7 предметов, которое упоминает Боджибой. Помимо этого, возможно, стоит обратить внимание на анонимные (или специализированные) типы, IDictionary или косвенное обращение через первичный ключ к другому источнику данных.
Если это не более 1 аргумента, я всегда использую массивы или объекты в качестве параметров конструктора и полагаюсь на проверку ошибок, чтобы убедиться в наличии необходимых параметров.