Чи завжди використання "нового" в конструкторі завжди погано?


37

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


9
Це не робить тестування неможливим. Хоча це робить підтримку та тестування вашого коду складніше. Наприклад, прочитайте нове - це клей .
Девід Арно

38
"завжди" - це завжди неправильно. :) Є найкраща практика, але винятків багато.
Пол

63
Яка мова це? newозначає різні речі різними мовами.
user2357112 підтримує Моніку

13
Це питання трохи залежить від мови, чи не так? Не всі мови навіть мають newключове слово. На яких мовах ви питаєте?
Брайан Оуклі

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

Відповіді:


36

Завжди є винятки, і я приймаю питання з написом "завжди", але так, це керівництво, як правило, є дійсним, а також застосовується і поза конструктором.

Використання нового в конструкторі порушує D у SOLID (принцип інверсії залежності). Це робить ваш код важким для тестування, оскільки тестування одиниць стосується ізоляції; важко виділити клас, якщо він має конкретні посилання.

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

Якщо не використовувати нове у конструкторі, ваш код стає більш гнучким. Це стосується також мов, які можуть використовувати інші конструкціїnew для ініціалізації об'єктів.

Однак, однозначно, потрібно використовувати хороші судження. Буває багато випадків, коли це добре використовувати new, або там, де було б краще цього не робити, але негативних наслідків у вас не буде. У якийсь момент кудись newтреба викликати. Просто будьте дуже обережні щодо дзвінківnew всередині класу, від якого залежить багато інших класів.

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

Чим більше посилань класу на нього, тим обережніше слід бути не дзвонити newз нього.


12
Я дійсно не бачу ніякого значення в цьому правилі. Існують правила та вказівки щодо того, як уникнути тісного з’єднання коду. Це правило, схоже, стосується саме тієї проблеми, за винятком того, що воно неймовірно занадто обмежувальне, не надаючи нам додаткової цінності. Тож який сенс? Чому б створити об’єкт фіктивного заголовка для моєї реалізації LinkedList замість того, щоб мати справу з усіма тими особливими випадками, які випливають із того, head = nullщоб яким-небудь чином поліпшити код? Чому залишати включені колекції як недійсні та створювати їх на вимогу краще, ніж робити це в конструкторі?
Voo

22
Ви пропускаєте суть. Так, модуль стійкості - це абсолютно щось, на чому модуль вищого рівня не повинен залежати. Але "ніколи не використовуй нове" не випливає з "деякі речі слід вводити, а не безпосередньо зв'язувати". Якщо ви візьмете свою надійну копію Agile Software Development, ви побачите, що Мартін, як правило, говорить про модулі, а не про окремі класи: "Модулі високого рівня стосуються політики високого рівня програми. Ці політики зазвичай мало цікавляться деталями, які реалізуються їх ».
Voo

11
Тому справа в тому, що вам слід уникати залежностей між модулями, особливо між модулями різного рівня абстрагування. Але модуль може бути більше, ніж один клас, і просто немає точки зведення інтерфейсів між щільно сполученим кодом того ж шару абстракції. У добре розробленому додатку, який відповідає принципам SOLID, не кожен клас повинен реалізовувати інтерфейс, і не кожен конструктор повинен завжди приймати інтерфейси лише як параметри.
Voo

18
Я прихильнився тому, що це багато суп-мовлення і не так багато дискусій з практичних міркувань. Є щось про репо до двох БД, але, чесно кажучи, це не є прикладом реального світу.
jpmc26

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

50

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

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


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

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


4
+1. Це абсолютно правильно. Ви повинні визначити, яка призначена поведінка класу, і що визначає, які деталі реалізації потрібно піддавати, а які ні. Існує якась тонка гра з інтуїцією, з якою ви повинні грати, визначаючи, що таке "відповідальність" класу.
jpmc26

27

Не всі співпрацівники досить цікаві для тестування модулів окремо, ви можете (побічно) протестувати їх через клас хостингу / інстанції. Це може не узгоджуватись із уявленнями деяких людей про необхідність тестування кожного класу, кожного публічного методу тощо, особливо коли робиться тест після. Під час використання TDD ви можете перефактурувати цей «співавтор», витягуючи клас, де він уже повністю перевіряється з першого тестового процесу.


14
Not all collaborators are interesting enough to unit-test separatelyкінець історії :-), Цей випадок можливий і ніхто не наважився згадати. @Joppe Я закликаю вас трохи детальніше відповісти. Наприклад, ви можете додати кілька прикладів класів, які є лише детальними відомостями про реалізацію (не підходять для заміни), і як вони можуть бути вилучені останніми, якщо ми вважаємо це за необхідне.
Laiv

Моделі доменів @Laiv - це, як правило, конкретні, неабразивні, ви не займаєтесь введенням туди вкладених об'єктів. Об'єкт простого значення / складний об'єкт, що не має логіки, також є кандидатом.
Джоппе

4
+1, абсолютно. Якщо мова налаштована так, що вам потрібно зателефонувати, new File()щоб зробити що-небудь, пов'язане з файлами, немає сенсу забороняти цей виклик. Що ви збираєтеся робити, написати регресійні тести проти Fileмодуля stdlib ? Не схоже. З іншого боку, покликання newна самостійно винайдений клас є більш сумнівним.
Кіліан Фот

7
@KilianFoth: Хоча блок удачі тестує все, що безпосередньо дзвонить new File().
Фоши

1
Це здогадки . Ми можемо знайти випадки, коли це не має сенсу, випадки, коли це не корисно, та випадки, коли це має сенс і це корисно. Це питання потреб та уподобань.
Laiv

13

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

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

У цьому випадку ви можете спробувати придумати 2 або 3 прості класи та деякі поведінки, які вони повинні реалізувати. Реалізовуйте заняття будь-яким способом, яким ви вважаєтесь природним, і пишіть одиничний тест на кожну поведінку. Складіть список будь-яких проблем, з якими ви зіткнулися, наприклад, якщо ви почали працювати в один бік, то довелося піти назад і змінити його пізніше; якщо ви заплуталися в тому, як повинні поєднуватися речі; якщо вас дратувало написання котельних табличок; тощо.

Потім спробуйте вирішити ту саму проблему, дотримуючись «правила». Знову складіть список проблем, з якими ви стикалися. Порівняйте списки та подумайте, які ситуації можуть бути кращими при дотриманні правила, а які - ні.


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

Основна логіка полягає в тому, щоб обчислити речі "всередині" програми на основі проблемної області. Він може містити класи , як User, Document, Order, Invoice, штраф і т.д. Це мати базові класи називати newдля інших основних класів, так як вони «внутрішньої» деталі реалізації. Наприклад, створення також Orderможе створити Invoiceі Documentдеталізувати те, що було замовлено. Не потрібно знущатися над ними під час тестів, адже це фактичні речі, які ми хочемо перевірити!

Порти та адаптери - це те, як основна логіка взаємодіє із зовнішнім світом. Це де речі , як Database, ConfigFile, EmailSenderі т.д. жити. Це те, що робить тестування важким, тому доцільно створити їх за межами основної логіки та ввести їх у міру необхідності (або з використанням ін'єкції залежності, або у вигляді аргументів методу тощо).

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

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


6

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

Завжди є винятки, але так, це правило, як правило, діє, а також застосовується і поза конструктором.

Використання нового в конструкторі порушує D у SOLID (принцип інверсії залежності). Це робить ваш код важким для тестування, оскільки тестування одиниць стосується ізоляції; важко виділити клас, якщо він має конкретні посилання.

-TheCatWhisperer-

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

Тим не менш, наведена цитата не завжди відповідає дійсності. У деяких випадках можуть бути заняття , які мають бути тісно пов'язані . Девід Арно прокоментував пару.

Звичайно, є винятки, коли клас є об'єктом незмінного значення , деталі реалізації тощо. Там, де вони повинні бути щільно з'єднані .

-Давид Арно-

Саме так. Деякі класи (наприклад, внутрішні класи) можуть бути просто деталями реалізації основного класу. Вони призначені для тестування разом з основним класом, і вони не обов'язково змінюються або розширюються.

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

Тож вірогідна відповідь, як завжди, залежить . Використання newконструкторів всередині може бути поганою практикою. Але не систематично завжди.

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


1: Основна мета SOLID - не зробити наш код більш перевіреним. Це зробити наш код більш толерантним до змін. Більш гнучка і, як наслідок, простіша перевірка

Примітка: Девід Арно, TheWhisperCat, сподіваюся, ви не заперечуєте, що я вас цитував.


3

Як простий приклад розглянемо наступний псевдокод

class Foo {
  private:
     class Bar {}
     class Baz inherits Bar {}
     Bar myBar
  public:
     Foo(bool useBaz) { if (useBaz) myBar = new Baz else myBar = new Bar; }
}

Оскільки це newє чистою деталізацією реалізації Foo, і те, і інше, Foo::Barі Foo::Bazє частиною Foo, коли одиничне тестування Fooне має сенсу глузувати з частин Foo. Ви знущаєтесь над деталями лише під Foo час тестування одиниць Foo.


-3

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

РЕДАКТУВАННЯ: Для користувачів, що перебувають у списку - ось посилання на книгу розробки програмного забезпечення, що позначає "нове" як можливий запах коду: https://books.google.com/books?id=18SuDgAAQBAJ&lpg=PT169&dq=new%20keyword%20code%20smell&pg=PT169 # v = одна сторінка & q = new% 20keyword% 20code% 20smell & f = false


15
Yes, using 'new' in your non-root classes is a code smell. It means you are locking the class into using a specific implementation, and will not be able to substitute another.Чому це проблема? Не кожна окрема залежність у дереві залежностей повинна бути відкрита для заміни
Laiv

4
@Paul: Реалізація за замовчуванням означає, що ви приймаєте чітко пов'язане посилання на конкретний клас, вказаний як за замовчуванням. Однак це не робить так званий "кодовий запах".
Роберт Харві

8
@EzoelaVacca: Я б з обережністю використовував слова "кодовий запах" у будь-якому контексті. Це якось, як сказати "республіканський" або "демократ"; що ці слова навіть означають? Як тільки ви даєте щось подібне на етикетці, ви перестаєте думати про реальні проблеми, і навчання припиняється.
Роберт Харві

4
"Більш гнучка" не є автоматично "кращою".
whatsisname

4
@EzoelaVacca: Використання newключового слова не є поганою практикою і ніколи не було. Це як ви використовуєте інструмент, який має значення. Наприклад, ви не використовуєте кувалду, де буде достатньо кульового молоточка.
Роберт Харві
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.