У статті показується приклад тісно пов'язаних класів, проводитися їх рефакторинг з реалізацією паттерна Dependency Injection (впровадження залежності) і демонструється застосування фреймворка Ninject, що полегшує впровадження залежностей.
Кожен з нас писав подібний код хоча б раз в житті. А ті, в чиї обов'язки входить перевірка коду початківців програмістів, можуть бачити це досить часто. З синтаксичної і логічної точок зору в ньому немає нічого кримінального. Код відмінно відпрацює, виконавши всі від нього очікуване, і в ньому абсолютно нічого не треба чіпати, якщо це частина програми складається з п'яти класів. Питання виникають, коли такий код з'являється в реальному проекті, що нараховує десятки класів, що має довгий життєвий цикл і велику ймовірність зміни користувальницьких вимог, способу зберігання даних або підтримуваного устаткування.
Чим же він поганий? Якщо коротко, то залежністю конкретного класу Car від конкретного класу Engine. Уявімо, що конструктор класу Engine змінитися - в цьому випадку доведеться змінювати і Car. Або, наприклад, з'явитися ще один двигун. І що б машина змогла його використовувати, знову-таки, доведеться змінити клас Car. Крім того, подібна жорстко запрограмована залежність знижує тестованих класів - клас Car неможливо протестувати окремо від Engine або замінити Engine на помилковий об'єкт, для симуляції непередбачених обставин.
Що ж можна зробити для поліпшення коду? Перше - ввести інтерфейс IEngine, який буде реалізовуватися класом Engine.
Далі, змінити клас Car, що б замість класу Engine в ньому використовувався інтерфейс IEngine. І друге, не менш важлива зміна - клас Car не повинен створювати двигун сам, тепер конструктор отримує посилання на об'єкт, який реалізує інтерфейс IEngine.
І останнє, що необхідно зробити - це змінити функцію Main.
Вийшло в результаті рішення легше розширюваність, більш тестованого і є прикладом реалізації паттерна Dependency Injection (впровадження залежності) - залежність машина-двигун конфигурируется не всередині класу, а двигун, створений поза класом, впроваджується в нього.
Мінус такого рішення - загубилася простота створення нового екземпляра класу Car. Уявіть, якщо в програмі досить багато подібних залежностей, їх все доведеться створювати вручну знову і знову. Тому, ми підемо далі і подивимося, як можна полегшити життя використовуючи Ninject, фреймворк для автоматичного впровадження залежностей.
Основну ідею використання DI фреймворків для створення об'єктів можна описати так: "Мені потрібен об'єкт класу A, створи його, і мене не цікавить, що і як ти для цього будеш робити". І ось як буде виглядати створення машини з використанням Ninject:
Що таке ninjectKernel стане зрозуміло трохи пізніше, а зараз варто звернути увагу на відсутність будь-якої згадки IEngine або Engine. Завдяки початкових конфігурацій Ninject знає, що машині потрібно двигун і коли його просять створити новий екземпляр класу Car, він самостійно створює Engine і передає його в конструктор Car. Таким чином, все що потрібно - це запросити клас. Всі залежності, необхідні для його створення або роботи, DI фреймворк дозволить самостійно. Якщо, звичайно, він був попередньо правильно налаштований.
Ninject можна або завантажити з сайту Ninject.org у вигляді стабільної версії, або взяти у вигляді актуальних вихідних файлів з репозиторію. Я рекомендую скористатися другим способом, так як версія, що знаходиться в репозиторії, підтримує кілька нових можливостей, в тому числі код для інтеграції з ASP.NET MVC.
Для початку роботи з Ninject, в проект необхідно додати посилання на збірку Ninject.Core.dll, що реалізує базові можливості фреймворка. Другий крок - конфігурація. На жаль, Dependency Injection - це не магія і що б Ninject знав, яким чином вирішувати залежності, він повинен бути проінструктований. Для нашої програми необхідні тільки дві інструкції:
У той час як багато DI фреймворки покладаються на конфігураційні XML файли, Ninject має простий, інтуїтивно зрозумілий API для завдання залежностей, що використовує звичний синтаксис мови і дозволяє задіяти всі можливості IDE.
Перша інструкція пов'язує інтерфейс IEngine з класом Engine таким чином, що кожного разу, коли Ninject виявить необхідність в об'єкті типу IEngine, то буде створено об'єкт Engine. Друга інструкція описує бажану поведінку при запиті на створення екземпляра класу Car - необхідно створити цей екземпляр, все просто. Насправді, друга інструкція не обов'язкова - така поведінка реалізується фреймворком за замовчуванням. При необхідності автосвязиваніе класів можна і відключити, і налаштовувати все самому.
Тепер виникає питання - а де виконувати конфігурація? Конфігурація виконується у віртуальному методі Load класу, що реалізовує інтерфейс IModule. Найпростіше буде наслідувати такий клас від класу StandardModule.
При бажанні, може бути створено кілька таких модулів, наприклад для поділу конфігурацій для різних частин програми. Далі ці модулі використовуються для створення контейнера, який і буде виконувати для нас всю брудну роботу.