Конструктор, як правило, не повинен викликати методи


12

Я описав колезі, чому конструктор, який викликає метод, може бути антипатерном.

приклад (у моєму іржавому C ++)

class C {
public :
    C(int foo);
    void setFoo(int foo);
private:
    int foo;
}

C::C(int foo) {
    setFoo(foo);
}

void C::setFoo(int foo) {
    this->foo = foo
}

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

Редагувати: Я загалом говорю, але ми кодуємо пітон.


Це загальне правило чи специфічне для певних мов?
ChrisF

Яка мова? У C ++ це більше, ніж анти-шаблон: parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.5
LennyProgrammers

@ Lenny222, в ОП йдеться про "класичні методи", які - принаймні для мене - означають неінстанційні методи . Що тому не може бути віртуальним.
Péter Török

3
@Alb У Java це гаразд. Те, що ви не повинні робити, явно переходить thisдо будь-якого методу, який ви викликаєте з конструктора.
biziclop

3
@Stefano Borini: Якщо ви кодуєте в Python, чому б не показати приклад у Python замість іржавого C ++? Також, будь ласка, поясніть, чому це погано. Ми робимо це постійно.
S.Lott

Відповіді:


26

Ви не вказали мову.

У C ++ конструктор повинен остерігатися виклику віртуальної функції, оскільки фактична функція, яку він викликає, - це реалізація класу. Якщо це чистий віртуальний метод без реалізації, це буде порушенням доступу.

Конструктор може викликати невіртуючі функції.

Якщо ваша мова - це Java, де функції за замовчуванням зазвичай є віртуальними, має сенс бути обережними.

Здається, C # вирішує ситуацію так, як ви очікували: ви можете викликати віртуальні методи в конструкторах, і це викликає останню версію. Так що в C # не є антидіаграмою.

Загальною причиною виклику методів від конструкторів є те, що у вас є кілька конструкторів, які хочуть викликати загальний метод "init".

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

У Java та C # немає деструкторів, у них є фіналізатори. Я не знаю поведінки з Java.

Здається, що C # правильно справляється з очищенням з цього приводу.

(Зверніть увагу, що хоча Java та C # мають збір сміття, він управляє лише розподілом пам'яті. Існує інша очистка, яку повинен зробити ваш деструктор, не звільняючи пам'ять).


13
Тут є кілька невеликих помилок. Методи в C # за замовчуванням не є віртуальними. C # має іншу семантику, ніж C ++, коли викликає віртуальний метод у конструкторі; буде викликуватися віртуальний метод на найбільш похідному типі, а не віртуальний метод на частині типу, що будується в даний час. C # називає його методи доопрацювання "деструкторами", але ви праві, що вони мають семантику фіналізаторів. Віртуальні методи, що викликаються деструкторами C #, працюють так само, як і в конструкторах; називається найбільш похідний метод.
Ерік Ліпперт

@ Петер: Я призначив методи екземпляра. Вибачте за непорозуміння.
Стефано Борині

1
@Eric Lippert. Дякую за вашу експертизу щодо C #, я відповідним чином відредагував свою відповідь. Мені невідомо на цій мові, я дуже добре знаю C ++ і Java менш добре.
CashCow

5
Ласкаво просимо. Зауважте, що виклик віртуального методу в конструкторі базового класу в C # все ще є досить поганою ідеєю.
Ерік Ліпперт

Якщо ви викликаєте (віртуальний) метод на Java від конструктора, він завжди викликатиме найбільш похідне переосмислення. Однак те, що ви називаєте "так, як ви очікували", це те, що я б назвав заплутаним. Оскільки, хоча Java посилається на найбільш похідне переопределення, цей метод бачить лише оброблені ініціалізатори поданих файлів, але не конструктор власного запуску класу. Викликати метод до класу, який ще не встановив інваріант, може бути небезпечним. Тому я думаю, що C ++ зробив тут кращий вибір.
5gon12eder

18

Гаразд, тепер, коли плутанина щодо методів класу проти методів примірника очищена, я можу дати відповідь :-)

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

C ++ і C # вже обговорювались іншими. У Java буде називатися віртуальний метод найбільш похідного типу, однак цей тип ще не ініціалізований. Отже, якщо цей метод використовує будь-які поля з похідного типу, ці поля ще не можуть бути ініціалізовані належним чином у цей момент часу. Ця проблема детально обговорюється у розділі Effecive Java 2-го видання , пункт 17: Дизайн та документ на спадщину або ж забороняють .

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


3
(+1) "перебуваючи всередині конструктора, об'єкт ще не повністю побудований." Те саме, що "класичні методи проти екземпляра". Деякі мови програмування вважають його сконструйованим при введенні в регулятор, як ніби програміст, де конструктор призначає значення.
umlcat

7

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

Ось стаття, яка може допомогти задовольнити ваше звернення до влади: http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/

Мова йде не про методи виклику, а про конструктори, які роблять занадто багато. IMHO, виклик методів у конструкторі - це запах, який може вказувати на те, що конструктор занадто важкий.

Це пов’язано з тим, як легко перевірити свій код. Причини включають:

  1. Тестування блоків передбачає багато створення та руйнування - тому будівництво має бути швидким.

  2. Залежно від того, що ці методи роблять, може бути важко перевірити дискретні одиниці коду, не покладаючись на певну (потенційно нестабільну) передумову, встановлену в конструкторі (наприклад, отримання інформації з мережі).


3

По-філософськи, мета конструктора полягає в тому, щоб перетворити сиру частину пам'яті в екземпляр. Поки конструктор виконується, об'єкт ще не існує, тому називати його методи - погана ідея. Ви, можливо, не знаєте, чим вони займаються всередині, і вони можуть по праву вважати, що об'єкт існує принаймні (так!), Коли їх викликають.

Технічно, в цьому може бути нічого поганого, в C ++ і особливо в Python, вам належить бути обережними.

Практично слід обмежувати виклики лише такими методами, які ініціалізують членів класу.


2

Це не питання загального призначення. Це проблема в C ++, зокрема, при використанні методів успадкування та віртуальних, оскільки побудова об'єкта відбувається назад, а покажчик (-и) vtable повертається з кожним шаром конструктора в ієрархію спадкування, тому, якщо ви викликаєте віртуальний метод, ви можете не в кінцевому підсумку ви отримаєте той, який насправді відповідає класу, який ви намагаєтесь створити, який перемагає цілі використання віртуальних методів.

У мовах з розумною підтримкою OOP, які правильно встановили покажчик vtable з самого початку, цієї проблеми не існує.


2

Існує дві проблеми з викликом методу:

  • виклик віртуального методу, який може зробити щось несподіване (C ++) або використовувати частини об'єктів, які ще не були ініціалізовані
  • виклик публічного методу (який повинен застосовувати інваріанти класу), оскільки об'єкт ще не обов'язково завершений (і, отже, його інваріант може не містити)

Немає нічого поганого в тому, щоб викликати функцію помічника, доки вона не потрапляє у два попередні випадки.


1

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


Ініціалізуйте об’єкт.
Стефано Борині

@Stefano Borini: Як? В об'єктно-орієнтованій системі єдине, що ви можете зробити - це методи виклику. Або подивитися на це з протилежного кута: все робиться методами виклику. І "все", очевидно, включає ініціалізацію об'єкта. Отже, якщо для ініціалізації об'єкта потрібно викликати методи, але конструктори не можуть викликати методи, то як конструктор може ініціалізувати об'єкт?
Йорг W Міттаг

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

@Stefano Borini: "Ви можете просто ініціалізувати стан без жодного виклику, безпосередньо у внутрішні об'єкти". На жаль, коли це стосується методу, що ти робиш? Скопіювати та пропустити код?
S.Lott

1
@ S.Lott: ні, я називаю це, але я намагаюсь утримати його функцією модуля замість методу об'єкта, і якщо він надає дані повернення, я можу перевести його в стан об'єкта в конструкторі. Якщо мені дійсно доведеться мати об’єктний метод, я зроблю його приватним і поясню, що це для ініціалізації, наприклад, надання йому належної назви. Я б ніколи не закликав публічний метод встановлення статусу об'єкта від конструктора.
Стефано Борині

0

У теорії OOP це не має значення, але на практиці кожна мова програмування OOP обробляє конструктори по-різному . Я не використовую статичні методи дуже часто.

У C ++ & Delphi, Якщо мені довелося дати початкові значення деяким властивостям ("членам поля"), і код дуже розширений, я додаю деякі вторинні методи як розширення конструкторів.

І не називайте інших методів, які роблять складніші речі.

Що стосується методів "getters" та "setters", я зазвичай використовую приватні / захищені змінні для зберігання їх стану, а також "getters" та "setters".

У конструкторі я присвоюю значення "за замовчуванням" полям властивостей, БЕЗ виклику "accessors".

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