наскільки складним повинен бути конструктор


18

Я маю дискусію з колегою про те, скільки роботи може виконати конструктор. У мене є клас B, який внутрішньо вимагає іншого об'єкта A. Об'єкт A - один з небагатьох членів, якому клас B повинен виконувати свою роботу. Всі його публічні методи залежать від внутрішнього об'єкта А. Інформація про об'єкт A зберігається в БД, тому я намагаюся перевірити та отримати його, переглянувши його на БД у конструкторі. Мій колега зазначав, що конструктор не повинен робити більше роботи, крім того, щоб зафіксувати параметри конструктора. Оскільки всі публічні методи в будь-якому разі не зможуть, якщо об’єкт A не буде знайдений за допомогою входів до конструктора, я стверджував, що замість того, щоб дозволити створення екземпляра і пізніше вийти з ладу, насправді краще кинути рано в конструктор.

Що думають інші? Я використовую C #, якщо це має значення.

Читання Чи є коли-небудь причина робити всі роботи об’єкта в конструкторі? мені цікаво, що видобуток об'єкта A, перейшовши до БД, є частиною "будь-якої іншої ініціалізації, необхідної для того, щоб об'єкт був готовий до використання", тому що якби користувач передав конструкторам неправильні значення, я не зміг би використовувати жоден з його публічних методів.

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


8
Я не погоджуюсь. Подивіться на принцип інверсії залежності, було б краще, якщо об’єкт A передається об'єкту B у дійсному стані в його конструкторі.
Джиммі Хоффа

5
Ви питаєте, скільки можна зробити? Скільки потрібно зробити? Або скільки вашої конкретної ситуації потрібно зробити? Це три дуже різні питання.
Бобсон

1
Я погоджуюся з пропозицією Джиммі Хоффи переглянути ін'єкцію залежності (або "інверсію залежності"). Це була моя перша думка, коли я прочитав опис, тому що це здається, що ти вже на півдорозі; у вас вже є функціональні можливості, розділені на клас A, який використовується клас B. Я не можу говорити з вашим випадком, але я, як правило, вважаю, що код рефакторингу для використання схеми введення залежності робить класи простішими / менш переплетеними.
Учень доктора Вілі

1
Бобсон, я змінив назву на "повинен". Я прошу тут найкращої практики.
Таїн Кім

1
@JimmyHoffa: можливо, вам слід розширити свій коментар у відповідь.
кевін клайн

Відповіді:


22

Ваше запитання складається з двох абсолютно окремих частин:

Чи повинен я викинути виняток з конструктора, або я повинен мати метод невдачі?

Це явно застосування принципу Fail-fast . Зробити помилку конструктора набагато простіше налагодити, ніж потрібно знайти, чому метод не працює. Наприклад, ви можете отримати екземпляр, вже створений з якоїсь іншої частини коду, і ви отримаєте помилки під час виклику методів. Чи очевидно, що об’єкт створений неправильно? Ні.

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

Чи можу я отримати доступ до "інших частин системи, як БД" від конструктора.

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


2
Більш підходящим рішенням для цього є зазвичай додавання методу "відкритого типу", коли побудова безкоштовна, але жодного використання не можна робити перед тим, як "відкрити" / "підключити" / тощо, де саме відбувається вся фактична ініціалізація. Відмова конструкторів може спричинити всілякі проблеми, коли ви в майбутньому хочете зробити часткове будівництво об'єктів, що є корисною здатністю.
Джиммі Хоффа

@JimmyHoffa Я не бачу різниці між конструкторським методом та конкретним методом побудови.
Ейфорія

4
Ви можете не робити, але багато хто робить. Подумайте, коли ви створюєте об’єкт з'єднання, чи з'єднує його конструктор? Чи об'єкт корисний, якщо він не підключений? В обох питаннях відповідь "ні", тому що корисно, щоб об'єкти з'єднання були частково побудовані, щоб ви могли налаштувати налаштування та речі, перш ніж відкривати з'єднання. Це загальний підхід до цього сценарію. Я все ще думаю, що введення конструктора - це правильний підхід для сценаріїв, коли об'єкт A має залежність від ресурсів, але відкритий метод все-таки краще, ніж конструктор виконувати небезпечну роботу.
Джиммі Хоффа

@JimmyHoffa Але це специфічна вимога класу з'єднання, а не загальне правило проектування OOP. Я б насправді назвав це винятковим випадком, ніж правилом. Ви в основному говорите про схему побудови.
Ейфорія

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

19

Тьфу.

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

Для цього в конструкторі та використанні конструктора є дві частини . У конструкторі це незручно, тому що ти не можеш багато зробити там, коли трапляється виняток. Ви не можете повернути інший тип. Ви не можете повернути null. Ви в основному можете повторно скинути виняток, повернути зламаний об'єкт (поганий конвектор) або (в кращому випадку) замінити якусь внутрішню частину на здоровий за замовчуванням. Використовувати конструктор незручно, тому що тоді ваші інстанції (і дані всіх похідних типів!) Можуть кинутись. Тоді ви не можете їх використовувати в ініціалізаторах членів. Ви в кінцевому підсумку використовуєте винятки для логіки, щоб побачити, "чи моє творіння вдалося?", Крім читабельності (та крихкості), спричиненої спробою / ловити всюди.

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


2
Складні сценарії побудови - саме тому існують креативні моделі, як ви згадуєте тут. Це хороший підхід до цих проблемних конструкцій. Мені дано подумати про те, як в .NET Connectionоб’єкт може CreateCommandдля вас, щоб ви знали, що команда належним чином підключена.
Джиммі Хоффа

2
До цього я додам, що база даних - важка зовнішня залежність, припустимо, ви хочете в якийсь момент в майбутньому переключити бази даних (або знущатися над об’єктом для тестування), переміщуючи такий тип коду до класу Factory (або щось подібне до IService постачальник) видається чистішим підходом
Джейсон Сперський

4
чи можете ви детальніше розповісти про "поведінку виключень у конструкторах незручно"? Якби ви використовували Create, чи не потрібно було б перетворювати його на спробу лову так само, як ви б завершили виклик конструктору? Я відчуваю, що Create - це просто інша назва конструктора. Можливо, я не отримую вашої відповіді правильно?
Таїн Кім

1
Довелося спробувати наздогнати винятки, що киплять від конструкторів, +1 до аргументів проти цього. Працюючи з людьми, які говорять "Немає роботи в конструкторі", це також може створити деякі дивні візерунки. Я бачив код, де кожен об'єкт має не лише конструктор, але й певну форму Initialize (), де, вгадайте, що? Об'єкт насправді не дійсний, доки не буде викликаний також. І тоді у вас є дві речі для виклику: ctor та Initialize (). Проблема роботи над налаштуванням об'єкта не зникає - C # може дати інші рішення, ніж скажімо C ++. Заводи - це добре!
J Trana

1
@jtrana - так, ініціалізувати - це не добре. Конструктор ініціалізується на якийсь здоровий за замовчуванням або заводський, або створює метод, будує його і повертає корисний екземпляр.
Теластин

1

Баланс складності конструкції - методу справді є предметом дискусії про хороший стиль. Досить часто використовується порожній конструктор Class() {}із методом Init(..) {..}для більш складних речей. Серед аргументів, які слід враховувати:

  • Можливість серіалізації класів
  • Використовуючи метод відділення Init від конструктора, вам часто потрібно повторювати одну і ту ж послідовність Class x = new Class(); x.Init(..);багато разів, частково в Unit Tests. Однак не дуже добре, кришталево чисте для читання. Іноді я просто кладу їх в один рядок коду.
  • Коли цільовий блок-тест - це лише тест ініціалізації класу, краще мати власний Init, виконувати складніші дії, що виконуються один за одним, з проміжними твердженнями.

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