Інжекція залежності та шаблон одиночного дизайну


89

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

Наприклад Logger. Я міг би використати Logger.GetInstance().Log(...) Але, замість цього, чому мені потрібно вводити кожен клас, який я створюю, за допомогою екземпляра реєстратора ?.

Відповіді:


66

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

Перегляньте цю презентацію на тему об’єктно-орієнтованого дизайну, і ви побачите, чому одинаки погані.

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

Майте на увазі, що об’єкт може бути фактично одиночним, але його все одно можна отримати за допомогою ін’єкції залежності, а не через Singleton.getInstance().

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


89

Сінглтон - це як комунізм: обидва вони чудово звучать на папері, але на практиці вибухають із проблемами.

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

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

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


9
@BryanWatts все сказане, Singletons все ще набагато швидше і менше схильні до помилок у звичайному середньомасштабному додатку. Зазвичай я не маю більше однієї можливої ​​реалізації для (користувальницького) реєстратора, так чому ** мені потрібно: 1. Створити для цього інтерфейс і змінювати його кожного разу, коли мені потрібно додати / змінити публічних учасників конфігурація DI 3. Приховати той факт, що у всій системі є єдиний об’єкт цього типу 4. Прив’язати себе до суворої функціональності, спричиненої передчасним розділенням проблем.
Урі Абрамсон,

@UriAbramson: Здається, ви вже прийняли рішення щодо компромісів, які ви віддаєте перевагу, тому я не буду намагатися вас переконати.
Bryan Watts

@BryanWatts Це не так, можливо, мій коментар був занадто жорстким, але насправді мене дуже цікавить, щоб почути, що ви маєте сказати про те, що я висловив ...
Урі Абрамсон

2
@UriAbramson: Досить справедливо. Чи принаймні ви погодитесь, що обмін реалізацій для ізоляції під час тестування важливий?
Bryan Watts

6
Використання синглтонів та введення залежностей не є взаємовиключними. Синглтон може реалізувати інтерфейс, тому може бути використаний для задоволення залежності від іншого класу. Той факт, що це синглтон, не змушує кожного споживача отримувати посилання за допомогою методу / властивості "GetInstance".
Олівер,

18

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

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

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

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


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

4
@kyoryu, я говорю про "звичайний" випадок, який IMHO передбачає із використанням (де-факто) стандартної системи реєстрації. (Які зазвичай можна налаштувати за допомогою файлів властивостей / XML, крім того.) Звичайно, є винятки - як завжди. Якщо я знаю, що моя програма є винятковою в цьому відношенні, я справді не буду використовувати Singleton. Але надмірна інженерія, кажучи: "Це може колись стати в нагоді", майже завжди марно витрачається.
Péter Török

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

1
@kyoryu, вибачте за неправильне припущення. Я побачив голос проти і коментар, тому підключив крапки - неправильний шлях, як виявляється :-( Я ніколи не відчував необхідності перейти на іншу структуру ведення журналу, але знову ж таки, я розумію, це може бути законним стурбованість у деяких проектах.
Péter Török

5
Виправте мене, якщо я помиляюся, але синглтон буде для LogFactory, а не для реєстратора. Плюс LogFactory, швидше за все, буде спільним файлом apache або фактом реєстрації Slf4j. Тож переключення реалізацій журналювання все одно безболісно. Хіба справжній біль при використанні DI для введення LogFactory не той факт, що вам зараз потрібно перейти до applicationContext, щоб створити кожен екземпляр у вашому додатку?
HDзберегти

9

Це в основному, але не спокійно про тести. Сінглтони були популярними, оскільки їх було легко споживати, але у сінглтонів є ряд мінусів.

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

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


Не зовсім. Йдеться про спільний змінний стан і статичні залежності, які роблять речі болючими в перспективі. Тестування - це лише очевидний і часто найболючіший приклад.
kyoryu

2

Приблизно єдиний раз, коли ви коли-небудь повинні використовувати Singleton замість Dependency Injection, це якщо Singleton представляє незмінне значення, таке як List.Empty або подібне (припускаючи незмінні списки).

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


1

Щойно перевірив статтю Monostate - це чудова альтернатива Singleton, але вона має деякі дивні властивості:

class Mono{
    public static $db;
    public function setDb($db){
       self::$db = $db;
    }

}

class Mapper extends Mono{
    //mapping procedure
    return $Entity;

    public function save($Entity);//requires database connection to be set
}

class Entity{
public function save(){
    $Mapper = new Mapper();
    $Mapper->save($this);//has same static reference to database class     
}

$Mapper = new Mapper();
$Mapper->setDb($db);

$User = $Mapper->find(1);
$User->save();

Чи не страшно це - оскільки Mapper насправді залежить від підключення до бази даних, щоб виконати save () - але якщо інший mapper був створений раніше - він може пропустити цей крок у отриманні своїх залежностей. Хоча акуратний, але він теж брудний, чи не так?


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