Чому не стало загальним зразком використовувати сетери в конструкторі?


23

Аксесуари та модифікатори (також сетери та геттери) корисні з трьох основних причин:

  1. Вони обмежують доступ до змінних.
    • Наприклад, змінна може бути доступна, але не змінюватися.
  2. Вони підтверджують параметри.
  3. Вони можуть викликати деякі побічні ефекти.

Університети, онлайн-курси, навчальні посібники, статті в блогах та приклади коду в Інтернеті все наголошують на важливості аксесуарів та модифікаторів, вони майже відчувають себе як "обов'язкові" для цього коду. Тож їх можна знайти навіть тоді, коли вони не надають додаткового значення, як-от код нижче.

public class Cat {
    private int age;

    public int getAge() {
        return this.age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

Як було сказано, дуже часто можна знайти більш корисні модифікатори, ті, які насправді підтверджують параметри та викидають виняток або повертають булевий сигнал, якщо введено недійсний вхід, приблизно так:

/**
 * Sets the age for the current cat
 * @param age an integer with the valid values between 0 and 25
 * @return true if value has been assigned and false if the parameter is invalid
 */
public boolean setAge(int age) {
    //Validate your parameters, valid age for a cat is between 0 and 25 years
    if(age > 0 && age < 25) {
        this.age = age;
        return true;
    }
    return false;
}

Але навіть тоді я майже ніколи не бачу, щоб модифікатори викликали конструктор, тому найпоширеніший приклад простого класу, з яким я стикаюся, це такий:

public class Cat {
    private int age;

    public Cat(int age) {
        this.age = age;
    }

    public int getAge() {
        return this.age;
    }

    /**
     * Sets the age for the current cat
     * @param age an integer with the valid values between 0 and 25
     * @return true if value has been assigned and false if the parameter is invalid
     */
    public boolean setAge(int age) {
        //Validate your parameters, valid age for a cat is between 0 and 25 years
        if(age > 0 && age < 25) {
            this.age = age;
            return true;
        }
        return false;
    }

}

Але можна подумати, що цей другий підхід набагато безпечніший:

public class Cat {
    private int age;

    public Cat(int age) {
        //Use the modifier instead of assigning the value directly.
        setAge(age);
    }

    public int getAge() {
        return this.age;
    }

    /**
     * Sets the age for the current cat
     * @param age an integer with the valid values between 0 and 25
     * @return true if value has been assigned and false if the parameter is invalid
     */
    public boolean setAge(int age) {
        //Validate your parameters, valid age for a cat is between 0 and 25 years
        if(age > 0 && age < 25) {
            this.age = age;
            return true;
        }
        return false;
    }

}

Ви бачите подібний зразок у своєму досвіді чи це просто мені не пощастило? А якщо так, то, на вашу думку, це спричиняє це? Чи є очевидний недолік використання модифікаторів від конструкторів або вони просто вважаються безпечнішими? Це щось інше?


1
@stg, дякую, цікавий момент! Чи можете ви розширити його і вкласти його у відповідь на прикладі поганого, що може статися?
Влад Спріз


8
@stg Насправді, це анти-шаблон для використання перезаписуваних методів у конструкторі, і багато інструментів якості коду вказують на це як на помилку. Ви не знаєте, що зробить перекрита версія, і це може зробити неприємні речі, такі як відпущення "цього" до завершення конструктора, що може призвести до різного роду дивних проблем.
Michał Kosmulski

1
@gnat: Я не вірю, що це дублікат програми. Чи повинні методи класу називати власні геттери та сетери? , через характер викликів конструктора викликається на об'єкт, який не повністю сформований.
Грег Бургхардт

3
Зауважте також, що ваша "перевірка" просто залишає вік неініціалізованим (або нульовим, або ... чимось іншим, залежно від мови). Якщо ви хочете, щоб конструктор вийшов з ладу, ви повинні перевірити значення повернення і викинути виняток на помилку. Як відомо, ваша перевірка просто внесла іншу помилку.
Марно

Відповіді:


37

Дуже загальні філософські міркування

Як правило, ми просимо, щоб конструктор надав (як пост-умови) деякі гарантії щодо стану побудованого об'єкта.

Як правило, ми також очікуємо, що методи екземплярів можуть припускати (як попередні умови), що ці гарантії вже зберігаються, коли вони викликаються, і їм потрібно лише переконатися, що їх не порушують.

Виклик методу екземпляра всередині конструктора означає, що деякі або всі ці гарантії ще не встановлені, через що важко міркувати про те, чи виконуються попередні умови методу екземпляра. Навіть якщо ви правильно це зробите, це може бути дуже крихким перед обличчям, наприклад. переупорядкування викликів методу екземпляра або інших операцій.

Мови також різняться тим, як вони вирішують виклики методам екземплярів, які успадковуються від базових класів / замінюються підкласами, поки конструктор все ще працює. Це додає ще один рівень складності.

Конкретні приклади

  1. Ваш власний приклад того, як ви вважаєте, що це має виглядати, сам по собі неправильний:

    public Cat(int age) {
        //Use the modifier instead of assigning the value directly.
        setAge(age);
    }

    це не перевіряє значення повернення з setAge. Мабуть, зателефонувати до сетера - це ще не гарантія коректності.

  2. Дуже прості помилки, такі як залежно від порядку ініціалізації, такі як:

    class Cat {
      private Logger m_log;
      private int m_age;
    
      public void setAge(int age) {
        // FIXME temporary debug logging
        m_log.write("=== DEBUG: setting age ===");
        m_age = age;
      }
    
      public Cat(int age, Logger log) {
        setAge(age);
        m_log = log;
      }
    };

    де моя тимчасова вирубка все зламала. Ого!

Існують також такі мови, як C ++, коли виклик сеттера з конструктора означає марну ініціалізацію за замовчуванням (якої для деяких змінних членів принаймні варто уникати)

Проста пропозиція

Це правда, що більшість кодів написано не так, але якщо ви хочете зберегти ваш конструктор чистим і передбачуваним, і все ж повторно використовувати свою логіку до і після умови, краще рішення:

class Cat {
  private int m_age;
  private static void checkAge(int age) {
    if (age > 25) throw CatTooOldError(age);
  }

  public void setAge(int age) {
    checkAge(age);
    m_age = age;
  }

  public Cat(int age) {
    checkAge(age);
    m_age = age;
  }
};

а ще краще, якщо можливо: кодуйте обмеження у типі властивості та перевірте його власне значення при призначенні:

class Cat {
  private Constrained<int, 25> m_age;

  public void setAge(int age) {
    m_age = age;
  }

  public Cat(int age) {
    m_age = age;
  }
};

І, нарешті, для повної повноти - самовивірна обгортка в C ++. Зауважте, що хоча він все ще робить виснажливу перевірку, оскільки цей клас не робить нічого іншого , це порівняно легко перевірити

template <typename T, T MAX, T MIN=T{}>
class Constrained {
    T val_;
    static T validate(T v) {
        if (MIN <= v && v <= MAX) return v;
        throw std::runtime_error("oops");
    }
public:
    Constrained() : val_(MIN) {}
    explicit Constrained(T v) : val_(validate(v)) {}

    Constrained& operator= (T v) { val_ = validate(v); return *this; }
    operator T() { return val_; }
};

Гаразд, це не зовсім повно, я залишив різні конструктори для копіювання та переміщення та завдання.


4
Сильно недооцінена відповідь! Дозвольте додати лише тому, що ОР побачила описану ситуацію в текстовій книзі, і це не робить її гарним рішенням. Якщо в класі є два способи встановити атрибут (за конструктором та сеттером), і один хоче застосувати обмеження для цього атрибута, усі обидва способи повинні перевірити обмеження, інакше це помилка дизайну .
Док Браун

Тож чи вважаєте ви використовувати методи сеттера в конструкторі поганою практикою, якщо в сеттерах немає 1) логіки, або 2) логіка в сеттерах не залежить від стану? Я цього не роблю часто, але особисто я використовував сеттер-методи в конструкторі саме з цієї останньої причини (коли в
сеттері

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

13

Коли об'єкт будується, він за визначенням не формується повністю. Поміркуйте, яким чином ви надали сеттер:

public boolean setAge(int age) {
    //Validate your parameters, valid age for a cat is between 0 and 25 years
    if(age > 0 && age < 25) {
        this.age = age;
        return true;
    }
    return false;
}

Що робити, якщо частина перевірки setAge()включила перевірку на ageмайно, щоб переконатися, що об’єкт міг лише збільшуватися у віці? Цей стан може виглядати так:

if (age > this.age && age < 25) {
    //...

Це здається досить невинною зміною, оскільки коти можуть рухатися через час лише в одному напрямку. Єдина проблема полягає в тому, що ви не можете використовувати його для встановлення початкового значення, this.ageоскільки воно передбачає, що this.ageвже має дійсне значення.

Уникаючи задачі в конструкторах, ясно стає зрозумілим, що єдине , що конструктор робить - це встановити змінну екземпляра та пропустити будь-яку іншу поведінку, що відбувається в сеттері.


Гаразд ... але, якщо ви насправді не випробовуєте конструкцію об'єкта та не виявляєте потенційних помилок, можливі помилки в будь-якому випадку . І тепер у вас є дві дві проблеми: у вас є прихований потенційний помилку, і вам потрібно примусово перевірити віковий діапазон у двох місцях.
svidgen

Проблеми немає, так як у Java / C # всі поля нульові перед викликом конструктора.
Дедуплікатор

2
Так, виклик методів екземплярів від конструкторів може бути важким, і зазвичай не є кращим. Тепер, якщо ви хочете перенести свою логіку перевірки в приватний, остаточний клас / статичний метод, і зателефонувати їй і з конструктора, і з сеттера, це було б добре.
Марно

1
Ні, неінстанційні методи уникають хитрості методів екземплярів, оскільки вони не торкаються частково побудованого екземпляра. Вам, звичайно, ще потрібно перевірити результат перевірки, щоб вирішити, чи вдалося будівництво.
Марно

1
У цій відповіді ІМХО не вистачає точки. "Коли об'єкт будується, він, за визначенням, не формується повністю" - звичайно, в середині процесу побудови, але коли конструктор виконаний, будь-які намічені обмеження на атрибути повинні дотримуватися, інакше можна обмежити це обмеження. Я б назвав це серйозною вадою дизайну.
Док Браун

6

Тут вже є кілька хороших відповідей, але, здається, ніхто досі не помітив, що ви задаєте тут дві речі в одному, що викликає певну плутанину. ІМХО ваш пост найкраще розглядати як два окремих питання:

  1. якщо в сеттері є валідація обмеження, чи не має він також бути в конструкторі класу, щоб переконатися, що його не можна обійти?

  2. чому б не зателефонувати сетеру безпосередньо з цією метою від конструктора?

Відповідь на перші запитання однозначно "так". Конструктор, коли він не кидає виняток, повинен залишити об'єкт у дійсному стані, і якщо певні значення заборонені для певних атрибутів, тоді абсолютно не має сенсу дозволити ctor обійти це.

Однак відповідь на друге питання, як правило, є "ні", якщо ви не вживаєте заходів, щоб уникнути переопределення сеттера у похідному класі. Тому кращою альтернативою є реалізація перевірки обмежень приватним методом, який можна повторно використовувати як із сеттера, так і з конструктора. Я не збираюся тут повторювати приклад @Useless, який показує саме таку конструкцію.


Я досі не розумію, чому краще витягувати логіку із сетера, щоб конструктор міг її використовувати. ... Як це насправді інакше, ніж просто викликати сеттерів у розумному порядку ?? Особливо враховуючи, що якщо ваші сетери такі просто, як вони "повинні", то єдиною частиною сеттера, який ваш конструктор, на даний момент, не викликає, є власне налаштування нижнього поля ...
svidgen

2
@svidgen: див. приклад JimmyJames: коротко кажучи, це не дуже гарна ідея використовувати перезавантажувані методи в ctor, що спричинить проблеми. Ви можете використовувати сеттер в ctor, якщо він остаточний і не вимагає будь-яких інших способів перезапису. Більше того, відокремлення валідації від способу поводження з ним, коли воно не вдається, пропонує вам можливість по-різному обробляти його в ctor та в сетері.
Док Браун

Що ж, я ще менше переконаний! Але, дякую, що
вказали

2
Під розумним порядком ви маєте на увазі крихку невидиму впорядковану залежність, яку легко зламати невинно виглядають зміни , правда?
Марно

@useless добре, ні ... Я маю на увазі, смішно, що ви запропонували б порядок, у якому виконується ваш код, не має значення. але, це насправді не було суть. Окрім надуманих прикладів, я просто не можу згадати екземпляр зі свого досвіду, коли я вважав, що було б гарною ідеєю перевірити перехресні властивості в сетері - така річ обов'язково призводить до "невидимих" вимог до порядку виклику . Незалежно від того, чи виникають ці виклики у конструкторі ..
svidgen

2

Ось дурний біт коду Java, який демонструє типи проблем, з якими ви можете зіткнутися, використовуючи не остаточні методи у своєму конструкторі:

import java.util.regex.Pattern;

public class Problem
{
  public static final void main(String... args)
  {
    Problem problem = new Problem("John Smith");
    Problem next = new NextProblem("John Smith", " ");

    System.out.println(problem.getName());
    System.out.println(next.getName());
  }

  private String name;

  Problem(String name)
  {
    setName(name);
  }

  void setName(String name)
  {
    this.name = name;
  }

  String getName()
  {
    return name;
  }
}

class NextProblem extends Problem
{
  private String firstName;
  private String lastName;
  private final String delimiter;

  NextProblem(String name, String delimiter)
  {
    super(name);

    this.delimiter = delimiter;
  }

  void setName(String name)
  {
    String[] parts = name.split(Pattern.quote(delimiter));

    this.firstName = parts[0];
    this.lastName = parts[1];
  }

  String getName()
  {
    return firstName + " " + lastName;
  }
}

Коли я запускаю це, я отримую NPE в конструкторі NextProblem. Це тривіальний приклад, звичайно, але все може швидко ускладнитися, якщо у вас є кілька рівнів успадкування.

Я думаю, що більшою причиною цього не стало загального є те, що це робить код трохи складніше зрозуміти. Особисто я майже ніколи не використовую методи встановлення, і мінливі мої члени майже незмінно (призначені для каламбуру) остаточними. Через це значення повинні бути встановлені в конструкторі. Якщо ви використовуєте незмінні предмети (і для цього є багато вагомих причин), питання суперечить.

У будь-якому випадку, це корисна мета повторно використовувати перевірку чи іншу логіку, і ви можете ввести її в статичний метод і викликати його як від конструктора, так і від сеттера.


Як це взагалі проблема використання сеттерів у конструкторі замість проблеми погано реалізованого успадкування ??? Іншими словами, якщо ви фактично визнаєте недійсним базовий конструктор, то чому б ви його навіть називали !?
svidgen

1
Я думаю, що справа в тому, що переосмислення сетера не повинно визнати конструктором недійсним.
Кріс Уолерт

@ChrisWohlert Добре ... але, якщо ви зміните логіку налаштування на конфлікт із логікою конструктора, це проблема незалежно від того, чи використовує ваш конструктор сеттер. Або він не під час будівництва, вона не пізніше, або він не невидимо (б / с ви обходили свої бізнес - правила).
svidgen

Ви часто не знаєте, як реалізується конструктор базового класу (він може бути десь у сторонній бібліотеці). Перевизначення методу, що перезаписується, не повинно порушувати контракт базової реалізації (і, мабуть, цей приклад робить це, не встановлюючи nameполе).
Халк

@svidgen Проблема тут полягає в тому, що виклик методів перезапису з конструктора знижує корисність їх переопределення - ви не можете використовувати жодні поля дочірнього класу в перезаписаних версіях, оскільки вони ще не можуть бути ініціалізовані. Що ще гірше, ви не повинні передавати thisпосилання на якийсь інший метод, оскільки ви не можете бути впевнені, що його ще повністю ініціалізовано.
Халк

1

Це залежить!

Якщо ви виконуєте просту перевірку чи видаєте неприємні побічні ефекти в сетері, які потрібно вражати щоразу, коли властивість встановлюється: використовуйте сетер. Альтернативою є, як правило, витягнути із сеттера логіку задачі та застосувати новий метод, за яким слідує фактичний набір, як із сеттера, так і з конструктора - який фактично такий же, як і використання сеттера! (За винятком випадків, коли ви підтримуєте свій, мабуть, невеликий, дворядковий сетер у двох місцях.)

Однак якщо вам потрібно виконати складні операції, щоб упорядкувати об'єкт до того, як певний сетер (або два!) Змогли успішно виконати, не використовуйте сетер.

І в будь-якому випадку є тестові випадки . Незалежно від того, яким маршрутом ви йдете, якщо у вас є одиничні тести, ви матимете хороший шанс виявити підводні камені, пов'язані з будь-яким варіантом.


Я також додам, якщо моя пам’ять з тієї далекої давнини навіть віддалено точна, це такі міркування, яких я викладав у коледжі. І це зовсім не відповідає успішним проектам, над якими я мав таємницю.

Якби мені довелося здогадатися, я пропустив пам’ятку «чистого коду» в якийсь момент, який визначив деякі типові помилки, які можуть виникнути в результаті використання сеттерів у конструкторі. Але, зі свого боку, я не став жертвою жодних таких помилок ...

Отже, я б заперечував, що це конкретне рішення не повинно бути чітким і догматичним.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.