Законна "реальна робота" в конструкторі?


23

Я працюю над дизайном, але продовжуйте вражати блокпост. У мене є особливий клас (ModelDef), який по суті є власником складного дерева вузлів, побудованого за допомогою аналізу XML-схеми (думаю, DOM). Я хочу дотримуватися принципів хорошого дизайну (SOLID) та забезпечити легку перевірку отриманої системи. У мене є всі наміри використовувати DI для передачі залежностей в конструктор ModelDef (щоб вони могли легко бути замінені, якщо потрібно, під час тестування).

З чим я боюся, - це створення дерева вузлів. Це дерево повністю буде складено з простих "ціннісних" об'єктів, які не потрібно буде самостійно перевіряти. (Однак я все-таки можу передати абстрактний завод у ModelDef, щоб допомогти у створенні цих об'єктів.)

Але я постійно читаю, що конструктор не повинен робити жодної реальної роботи (наприклад, Flaw: Constructor виконує справжню роботу ). Це має для мене ідеальний сенс, якщо "реальна робота" означає побудову об'єктів, що залежать від важкої ваги, які згодом можна буде заглушити для тестування. (Вони повинні бути передані через DI.)

А як щодо об’єктів малої ваги, таких як це дерево вузлів? Дерево треба десь створити, правда? Чому б не через конструктор ModelDef (використовуючи, скажімо, метод buildNodeTree ())?

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

Я думав поставити код для створення дерева вузлів в окремому об'єкті "builder", але соромтеся називати його "будівельником", оскільки він насправді не відповідає шаблону Builder (який, здається, більше стосується усунення телескопізації конструктори). Але навіть якби я назвав це чимось іншим (наприклад, NodeTreeConstructor), він все ще відчуває себе трохи злому, щоб уникнути того, щоб конструктор ModelDef будував дерево вузлів. Це треба десь будувати; чому б не в об’єкті, який збирається ним володіти?


7
Ви завжди повинні насторожено ставитися до подібних тверджень. Загальне правило - це код чітким, функціональним, легким для тестування, повторного використання та обслуговування будь-яким способом, який може бути, який залежить від вашої ситуації. Якщо ви не отримуєте нічого, крім складності коду та плутанини, намагаючись слідувати такому «правилу», як тоді, це не було відповідним правилом для вашої ситуації. Усі ці «зразки» та мовні особливості є інструментами; використовуйте найкращий для вашої конкретної роботи.
Джейсон C

Відповіді:


26

І, крім того, що запропонував Росс Паттерсон, врахуйте цю позицію, яка є прямо протилежною:

  1. Візьміть максими, такі як "Не будеш робити реальних робіт у своїх конструкторах" із зерном солі.

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

    а) простий конструктор і купа складних статичних заводських методів;

    б) простий конструктор і купа більш складних конструкторів.

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

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

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

Поправка

Я припускаю, що якщо у вас є метод, поза ModelDefяким створюється ModelDefз XML, вам потрібно буде інстанціювати деяку динамічну структуру тимчасових дерев, заповнити її шляхом аналізу вашого XML, а потім створити нову ModelDefпередачу цієї структури як параметр конструктора. Отже, це, можливо, можна розглядати як застосування шаблону "Builder". Існує дуже близька аналогія між тим, що ви хочете зробити, і String& StringBuilderпари. Однак я знайшов це запитання, яке, мабуть, не погоджується з незрозумілих мені причин : Stackoverflow - StringBuilder та Builder Pattern . Отже, щоб уникнути тривалої дискусії щодо того, чи реалізує StringBuilderчи не реалізує модель "будівельник", я б сказав, що ви можете бути натхнені тим, якStrungBuilder працює над створенням рішення, яке відповідає вашим потребам, і відкладіть його, називаючи програму шаблону "Builder", поки ця маленька деталь не буде врегульована.

Дивіться це нове запитання: Програмісти SE: Чи "StringBuilder" є застосуванням шаблону дизайну Builder?


3
@RichardLevasseur Я просто пам'ятаю, що це тема, яка викликає занепокоєння та дискусію серед програмістів на C ++ на початку-середини дев'яностих. Якщо ви подивитесь на цю публікацію: gotw.ca/gotw/066.htm, ви побачите, що це досить складно, а досить складні речі, як правило, суперечливі. Я точно не знаю, але думаю, що на початку дев'яностих років частина цього матеріалу ще не була стандартизована. Але вибачте, я не можу надати гарну інформацію.
Майк Накіс

1
@Gurtz Я б вважав такий клас, як специфічний для програми "xml утиліти", оскільки формат файлу xml (або структура документа), ймовірно, пов'язаний з конкретним додатком, який ви розробляєте, незалежно від будь-яких можливостей повторно використовувати ваш "ModelDef".
Майк Накіс

1
@Gurtz так, я, мабуть, змусив би їх застосовувати методи основного класу "Application", або якщо це занадто багато клопоту, то статичні методи якогось допоміжного класу, таким чином, дуже схожий на те, що запропонував Росс Паттерсон.
Майк Накіс

1
@Gurtz вибачається за те, що раніше конкретно не звертався до підходу "будівельника". Я змінив свою відповідь.
Майк Накіс

3
@Gurtz Це можливо, але поза академічною цікавістю це не має значення. Не зациклюйтесь на "анти-шаблоні". Шаблони - це справді просто назви, щоб зручно описати загальні / корисні методи кодування для інших. Виконайте те, що вам потрібно зробити, ляпіть на ньому ярликом пізніше, якщо вам потрібно описати це. Цілком нормально реалізовувати щось, що "схоже на зразок конструктора, певним чином, можливо", якщо ваш код має сенс. Доцільно зосередитись на моделях, вивчаючи нові методики, просто не потрапляйте в пастку, думаючи, що все, що ви робите, має бути деяким названим шаблоном.
Джейсон C

9

Ви вже даєте найкращі причини не робити цієї роботи в ModelDefконструкторі:

  1. Немає нічого "легкого" в аналізі документа XML у дереві вузлів.
  2. Немає нічого очевидного в ModelDefтому, що говорить, що його можна створити лише з документа XML.

Схоже , ваш клас повинен мати різні статичні методи , такі як ModelDef.FromXmlString(string xmlDocument), ModelDef.FromXmlDoc(XmlDoc parsedNodeTree), і т.д.


Дякую за відповідь! Щодо навіювання статичних методів. Це будуть статичні заводи, які створюють екземпляр ModelDef (з різних джерел xml)? Або вони будуть нести відповідальність за завантаження вже створеного об'єкта ModelDef? Якщо останнє, я б занепокоєний тим, що об’єкт буде лише частково ініціалізований (оскільки ModelDef потребує дерева вузлів, щоб повністю ініціалізуватися). Думки?
Гурц

3
Вибачте, що я замикаюся, але так, що Росс означає статичні заводські методи, які повертають повністю сконструйовані екземпляри. Повний прототип був би чимось на кшталт public static ModelDef createFromXmlString( string xmlDocument ). Це досить поширена практика. Іноді я теж це роблю. Моє припущення, що ви також можете робити лише конструктори - це стандартний тип реакції на мою ситуацію, коли я підозрюю, що альтернативні підходи без поважних причин відхиляються як "не кошерні".
Майк Накіс

1
@ Майк-Накіс, дякую за уточнення. Отже, у цьому випадку статичний заводський метод побудує дерево вузлів, а потім передасть його в конструктор (можливо, приватний) ModelDef. Мати сенс. Спасибі.
Гурц

@Gurtz Рівно.
Росс Паттерсон

5

Я чув про це "правило" раніше. На мій досвід, це і правда, і хибність.

У більш «класичній» орієнтації на предмет ми говоримо про об'єкти, що інкапсулюють стан та поведінку. Таким чином, конструктор об'єктів повинен забезпечити ініціалізацію об'єкта до дійсного стану (і сигналізувати про помилку, якщо надані аргументи не роблять об'єкт дійсним). Переконайтесь, що об'єкт ініціалізований до дійсного стану, напевно, це звучить як реальна робота для мене. І ця ідея має переваги, якщо у вас є об'єкт, який дозволяє ініціалізацію до дійсного стану через конструктор, і об'єкт належним чином інкапсулює його в стан, так що кожен метод, який змінює стан, також перевіряє, чи не він змінює стан на щось погане ... тоді цей об'єкт по суті гарантує, що він "завжди дійсний". Це дійсно приємна власність!

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

Таким чином ми отримуємо іншу "школу думки", яка є ТВЕРДОЮ. І в цій школі думок, швидше за все, набагато правдивіше, що ми не повинні робити реальної роботи в конструкторі. Код SOLID часто (але не завжди) простіше перевірити. Але також може бути важче міркувати. Ми розбиваємо наш код на дрібні об'єкти з однією відповідальністю, і, таким чином, більшість наших об'єктів більше не інкапсулюють їх стан (і містять, як правило, стан або поведінку). Код перевірки, як правило, дістається до класу валідатора та тримається окремо від стану. Але тепер ми втратили згуртованість, ми вже не можемо бути впевнені, що наші об’єкти дійсні, коли ми їх отримаємо і будемо повністюми впевнені, що ми завжди повинні підтверджувати, що передумови, які ми думаємо, що у нас є щодо об'єкта, є істинними, перш ніж намагатися щось зробити з об'єктом. (Звичайно, загалом ви робите валідацію в одному шарі, а потім припускаєте, що об'єкт дійсний на нижчих шарах.) Але це простіше перевірити!

То хто прав?

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

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


Це фантастична відповідь.
jrahhali

0

З цим правилом існує принципова проблема, і саме це є "справжньою роботою"?

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

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

Об'єкт не повністю ініціалізований після закінчення конструктора . Це хороше правило, але воно також суперечить ряду інших правил, згаданих у цій статті. Хорошим прикладом того, як це могло суперечити іншим, є згадане питання, яке привело мене сюди. У цьому питанні когось хвилює використання sortметоду в конструкторі в тому, що, здається, є JavaScript через цей принцип. У цьому прикладі людина створювала об'єкт, який представляв відсортований список інших об'єктів. З метою обговорення уявіть, що у нас був несортований перелік об'єктів, і нам потрібен новий об'єкт для представлення відсортованого списку. Цей новий об’єкт нам потрібен, тому що частина нашої програми очікує відсортованого списку і дозволяє викликати цей об’єктSortedList. Цей новий об'єкт приймає несортований список, і отриманий об'єкт повинен представляти відсортований зараз об'єкт. Якби ми дотримувались інших правил, згаданих у цьому документі, а саме жодних статичних викликів методу, ніяких структур потоку управління, нічого іншого, як присвоєння, то отриманий об'єкт не був би сконструйований у дійсному стані, порушуючи інше правило його повністю ініціалізації після закінчення конструктора. Щоб виправити це, нам потрібно було б виконати основну роботу, щоб зробити несортований список відсортований у конструкторі. Це може порушити три інші правила, а інші правила не мають значення.

Зрештою, це правило не робити «справжньої роботи» в конструкторі є неправильно визначеним та хибним. Намагаючись визначити, що таке "справжня робота" - це вправність у марності. Найкращим правилом у цій статті є те, що коли конструктор закінчує, його слід повністю ініціалізувати. Існує безліч інших найкращих практик, які обмежують кількість роботи, виконаної в конструкторі. Більшість із них можна підсумувати принципам SOLID, і ті самі принципи не заважали б вам працювати в конструкторі.

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

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