1. Dependency Inversion
Пам'ятаєш, ми колись говорили, що в серверній програмі не можна просто так створювати потоки через new Thread().start()
? Потоки повинен створювати лише контейнер. Тепер ми розвинемо цю думку ще далі.
Усі об'єкти теж повинен створювати лише контейнер. Звичайно, не йдеться про всі об'єкти, а скоріше про так звані бізнес-об'єкти. Їх часто називають бінами. Ноги цього підходу ростуть з п'ятого принципу SOLID, який вимагає позбавлятися класів і переходити на інтерфейси:
- Модулі верхнього рівня повинні залежати від модулів нижнього рівня. І ті, й інші мають залежати від абстракцій.
- Абстракції не повинні залежати від деталей. Реалізація має залежати від абстракції.
Модулі не повинні містити посилання на конкретні реалізації, а всі залежності та взаємодія між ними мають будуватися суто на основі абстракцій (тобто інтерфейсів). Саму суть цього правила можна записати однією фразою: всі залежності мають бути у вигляді інтерфейсів.
Незважаючи на свою фундаментальність і простоту, це правило порушується найчастіше. А саме — щоразу, коли в коді програми/модуля ми використовуємо оператор new і створюємо новий об'єкт конкретного типу, таким чином замість залежності від інтерфейсу утворюється залежність від реалізації.
Зрозуміло, цього не можна уникнути і об'єкти десь мають створюватися. Але, принаймні, потрібно звести до мінімуму кількість місць, де це робиться і в яких явно вказуються класи, а також локалізувати та ізолювати такі місця, щоб вони не були розкидані всюди в програмному коді.
Дуже хорошим рішенням є шалена ідея про те, щоб сконцентрувати створення нових об'єктів у межах спеціалізованих об'єктів та модулів – фабрик, сервіс локаторів, IoC-контейнерів.
У якомусь сенсі таке рішення слідує Принципу єдиного вибору (Single Choice Principle), який говорить: "Кожного разу, коли система програмного забезпечення повинна підтримувати безліч альтернатив, їх повний список має бути відомий лише одному модулю системи".
Тому, якщо в майбутньому доведеться додати нові варіанти (або нові реалізації, як у наведеному нами випадку створення нових об'єктів), достатньо буде провести оновлення лише того модуля, в якому міститься ця інформація, а всіх інших модулів це не торкнеться, і вони зможуть продовжувати свою роботу як зазвичай.
Приклад 1
Було б розумно замість new ArrayList
писати щось типу List.new()
. JDK підставила б вам правильну реалізацію списку: ArrayList, LinkedList або навіть ConcurrentList.
Наприклад, компілятор дивиться, що до об'єкту є звернення з різних потоків і ставить туди потокобезпечну реалізацію. Або занадто багато вставок у середину списку, тоді реалізація буде заснована на LinkedList.
Приклад 2
Це вже сталося із сортуваннями, наприклад. Коли ти востаннє писав алгоритм для сортування колекції? Натомість тепер всі користуються методом Collections.sort()
, а елементи колекції повинні підтримувати інтерфейс Comparable (порівнюваний).
Якщо до методу sort()
передати колекцію з менше ніж 10 елементів, її цілком можна відсортувати сортуванням бульбашкою (Bubble sort), а не Quicksort.
Приклад 3
Компілятор вже стежить за тим, як ти конкатенуєш рядки та заміниш ваш код на StringBuilder.append()
.
2. Інвертування залежностей практично
Тепер найцікавіше: давай подумаємо, як нам поєднати теорію та практику. Яким чином модулі можуть коректно створювати та отримувати свої “залежності” та не порушувати Dependency Inversion?
Для цього при проєктуванні модуля ти маєш вирішити для себе:
- що модуль виконує, яку функцію виконує;
- що модулю потрібно від його оточення, тобто з якими об'єктами/модулями йому доведеться мати справу;
- як він це отримуватиме.
Щоб дотриматися принципів Dependency Inversion, тобі обов'язково потрібно визначитися з тим, які зовнішні об'єкти використовує ваш модуль і як він буде отримувати на них посилання.
І тут можливі такі варіанти:
- модуль сам створює об'єкти;
- модуль бере об'єкти із контейнера;
- модуль гадки не має, звідки беруться об'єкти.
Проблема в тому, що для створення об'єкта необхідно викликати конструктор конкретного типу і в результаті модуль залежатиме не від інтерфейсу, а від конкретної реалізації. Але якщо ми не хочемо, щоб у коді модуля об'єкти створювалися явно, можна використовувати патерн Фабричний Метод (Factory Method ).
"Суть полягає в тому, що замість безпосереднього інстанціювання об'єкта через new, ми надаємо класу-клієнту деякий інтерфейс для створення об'єктів. Оскільки такий інтерфейс при правильному дизайні завжди може бути перевизначений, ми отримуємо певну гнучкість при використанні низькорівневих модулів у модулях високого рівня".
У випадках, коли потрібно створювати групи або сімейства взаємопов'язаних об'єктів, замість Фабричного Методу використовується абстрактна фабрика (abstract factory ).
3. Використання Service Locator
Модуль бере необхідні об'єкти у того, хто їх вже має. Передбачається, що у системі є певний репозиторій об'єктів, у якому модулі можуть “класти” свої об'єкти і “брати” об'єкти з репозиторію.
Цей підхід реалізується шаблоном Локатор Сервісів (Service Locator), основна ідея якого полягає в тому, що в програмі є об'єкт, який знає, як отримати всі залежності (сервіси), що можуть знадобитися.
Головна відмінність від фабрик полягає в тому, що Service Locator не створює об'єкти, а фактично вже містить інстанційовані об'єкти (або знає де/як їх отримати, а якщо і створює, то лише один раз при першому зверненні). Фабрика при кожному зверненні створює новий об'єкт, який ти отримуєш у повну власність і можеш робити з ним, що хочеш.
Важливо! Локатор сервісів видає посилання на ті самі вже існуючі об'єкти. Тому з об'єктами, виданими Service Locator, потрібно бути дуже обережним, оскільки одночасно з тобою ними може скористатися ще хтось.
Об'єкти в Service Locator можуть бути додані безпосередньо через конфігураційний файл та взагалі будь-яким зручним для програміста способом. Сам Service Locator може стати статичним класом із набором статичних методів, синглтоном чи інтерфейсом і передаватися необхідним класам через конструктор чи метод.
Service Locator іноді називають антипатерном і не рекомендують використовувати (бо він створює неявні зв'язки і дає лише видимість гарного дизайну). Докладно можна почитати у Марка Сімана:
4. Dependency Injection
Модуль взагалі не дбає про “добування” залежностей. Він лише визначає, що йому потрібно для роботи, проте необхідні залежності йому поставляються (впроваджуються) ззовні кимось іншим.
Це так і називається – Впровадження залежностей (Dependency Injection). Зазвичай необхідні залежності передаються або як параметри конструктора (Constructor Injection), або через методи класу (Setter injection).
Такий підхід інвертує процес створення залежності – замість самого модуля створення залежностей контролює хтось ззовні. Модуль із активного емітента об'єктів стає пасивним — не він створює, а для нього створюють інші.
Така зміна напряму дії називається Інверсія Контролю (Inversion of Control), або Принцип Голлівуду – "Не дзвоніть нам, ми самі вам подзвонимо".
Це гнучке рішення, що дає модулям найбільшу автономність. Можна сказати, що лише воно повною мірою реалізує "Принцип єдиної відповідальності" – модуль має бути повністю сфокусований на тому, щоб добре виконувати свою функцію і не дбати ні про що інше.
Забезпечення модуля всім необхідним для роботи – це окреме завдання, яким повинен займатися відповідний "фахівець" (зазвичай керуванням залежностями та їх впровадження займається якийсь контейнер – IoC-контейнер).
По суті, тут все як у житті: у добре організованій компанії програмісти програмують, а столи, комп'ютери та все необхідне для роботи купує та забезпечує офіс-менеджер. Або якщо використовувати метафору програми як конструктора — модуль не повинен думати про проводи: збіркою конструктора займається хтось інший, а не самі деталі.
Використання інтерфейсів для опису залежностей між модулями (Dependency Inversion) + коректне створення та впровадження цих залежностей (насамперед Dependency Injection) є ключовими техніками зниження зв'язаності.
Вони є тим фундаментом, на якому взагалі тримається слабка зв'язаність коду, його гнучкість, стійкість до змін, перевикористання, і без якого всі інші техніки майже не мають сенсу. Це основа основ слабкої зв'язаності та хорошої архітектури.
Принцип Inversion of Control (разом з Dependency Injection та Service Locator) детально розуміється Мартіном Фаулером: "Inversion of Control Containers and the Dependency Injection pattern" та "Inversion of Control".
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ