Вопрос

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

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

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

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

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

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

Это было полезно?

Решение

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

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

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

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

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

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

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

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

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

Я советую вам найти копию его книги и прочитать полное обсуждение аргументов функций (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();

Пример выше был взят из Эффективная 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 фрагментов данных (аргументов) для успешной инициализации объекта, я бы, вероятно, рассмотрел возможность разделения класса.

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

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

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

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

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

Java-реализация:

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

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

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

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

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

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

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

  • БизнесТелефон
  • БизнесАдрес
  • Домашний телефон
  • Домашний адрес

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

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top