Розбираємося з SOLID: Інверсія залежностей

Давайте глянемо на визначення принципу інверсії залежностей з вікіпедії:
Принцип інверсії залежностей (англ. dependency inversion principle, DIP) — важливий принцип об'єктно-орієнтованого програмування, який використовується для зменшення зв'язності в комп'ютерних програмах. Входить в п'ятірку принципів SOLID.

Формулювання:

A. Модулі верхніх рівнів не повинні залежати від модулів нижніх рівнів. Обидва типи модулів повинні залежати від абстракцій.
B. Абстракції не повинні залежати від деталей. Деталі повинні залежати від абстракцій.
Більшість розробників, з якими мені доводилося спілкуватися, розуміють тільки другу частину визначення. Мовляв "ну а що тут такого, треба зав'язувати класи не на конкретну реалізацію а на інтерфейс". І начебто вірно, але тільки кому повинен належати інтерфейс? Та й чому взагалі цей принцип так важливий? Давайте розбиратися.
Модулі
модуль — логічно взаємопов'язана сукупність функціональних елементів.
Що б не було непорозуміння, введемо трохи термінології. Під модулем ми будемо розуміти будь-яку функціонально пов'язану частина системи. Наприклад фреймворк ми можемо помістити як окремий незалежний модуль, а логіку роботи з користувачами — в іншій.
Модуль, це ніщо інше, як елемент декомпозиції системи. Модуль може включати в себе інші модулі, формуючи щось на зразок дерева. Відповідно можна виділити модулі різних рівнів:
Граф залежностей
Тут стрілочки між модулями показують хто що використовує. Відповідно ці ж стрілочки будуть показувати нам напрямку залежностей між нашими модулями.
І ось настав час додати "ще одну кнопочку". І ми розуміємо що функціонал цієї кнопки реалізовано у модулі E. Ми не роздумуючи полізли додавати те що нам треба, і нам довелося поміняти інтерфейс взаємодії з нашим модулем.
Ми вже хотіли закрити завдання, закоммитить код… але ми ж щось поміняли… підемо дивитися не зламали ми кого. І тут виявляється що з-за наших змін зламався модуль B. Окей. Полагодили. А раптом хтось хто використовує модуль B теж зламався? І в правду! Модуль A теж відвалився. Чиним… Коммитимся, пушим. Добре якщо є тести, тоді про проблеми ми дізнаємося швидко і швидко зможемо виправити. Але давайте подивимося правді в очі, мало хто пише тести.
А ще колезі вашому прилетів баг від тестувальника, мовляв модуль C зламався. Виявилося, що він по необережності зав'язався на ваш модуль E, а вам про це не сказав. Та ще й цей модуль складається з купи файлів, і всім від модуля E щось потрібно. І от тепер і він лазить по своїй частині графа залежностей (тому що йому простіше в ньому орієнтуватися ніж вам, не ваша ж частина системи) і проклинає вас.
Зміни у графі залежностей
На малюнку вище, помаранчевий кружечок означає модуль, який ми хотіли поправити. А червоні — які довелося поправити. І не факт що кожен гурток — один клас. Це можуть бути цілі компоненти. І добре якщо модулів у нас не дуже багато і вони не сильно перетинаються між собою. А що якщо у нас кожен кружечок був би пов'язаний з кожним? Це ж лагодити все на будь-який пчих. І в підсумку просте завдання "кнопочку додати" перетворюється в рефакторинг шматка системи. Як бути?
Інтерфейси і пізніше зв'язування
Пізніше зв'язування означає, що об'єкт зв'язується з викликом функції тільки під час виконання програми, а не на етапі компіляції.
Як відомо, інтерфейси визначають якийсь контракт. І кожен об'єкт, який реалізує цей контракт, зобов'язаний його дотримуватися. Наприклад пишемо ми реєстрацію користувачів. І згадуємо вимога — пароль користувача повинен бути надійно захэширован на випадок витоку даних з бази. Припустимо що в даний момент ми не знаємо як правильно це робити. І припустимо що ми ще не вибрали фреймворк або бібліотек для того щоб робити проект. Божевілля, я знаю… Але давайте уявимо, що у нас зараз немає нічого, крім логіки програми.
Ми згадуємо про вимогу, але не кидати ж нам все? Давайте все ж спочатку закінчимо з реєстрацією користувача, а вже потім будемо розбиратися як чого робити. Треба все ж послідовно підходити до роботи. А тому замість того щоб гуглити "як правильно хэшировать пароль" або розбиратися як це робити в нашому фреймворку, давайте зробимо інтерфейс
PasswordEncoder
. Зробивши це, ми створимо "контракт". Мовляв всякий хто зважиться реалізувати цей інтерфейс, зобов'язаний надати надійне і безпечне хешування пароля. Сам же інтерфейс буде до божевілля простим:
interface PasswordEncoder
{
public function encode(string $password): string;
}

Це саме те, що нам потрібно для роботи в даний момент часу. Ми не хочемо знати, як це буде відбуватися, ми ще не знаємо про сіль і повільне хешування. Ми можемо зробити заглушку, яка на момент розробки повертати те, що ми запхали. А вже потім зробимо нормальну реалізацію. Точно так само ми можемо вчинити з відправкою email-а про те що ми успішно зареєстрували користувача. Ми можемо навіть паралельно посадити ще людей, які будуть ці інтерфейси реалізовувати для нас, що б справу швидше йшло. Краса.
А принадність у тому, що ми можемо динамічно замінити реалізацію. Тобто безпосередньо перед викликом реєстрації користувача вибрати, який енкодер паролів нам треба використовувати. Саме це мається на увазі під пізнім зв'язуванням. Можливість "" реалізацію прямо перед використанням неї.
У мовах з динамічною системою типів, такий як в PHP, є ще більш простий спосіб домогтися пізнього зв'язування — не використовувати тайп хінтінг. Від слова зовсім. Правда зробивши це, ми повністю втратимо статичну (представлену в коді) інформацію про те, хто що використовує. І коли ми щось змінимо, нам вже не вийде так просто визначити, не зламався код. Це як вимкнути світло і шукати парні шкарпетки в горі з 99 одного лівого та 1-ого правого.
Інверсія залежностей
Отже, ми вже визначилися, що модуль E все ламає. І ваш колега захотів захиститися від майбутніх змін у "чужому" коді. Як ніяк, він з цього модуля використовує тільки одну функцію.
Для цього у своєму модулі C він створив інтерфейс, і написав простенький адаптер, який приймає залежність потрібного модуля і надає доступ тільки до потрібного методу. Тепер якщо ви поправите — виправити "поломку" можна буде в одному місці.
Причому цей інтерфейс розташований на кордоні модуля C, коли адаптер — на кордоні модуля E. Мовляв коли розробнику модуля E спаде в голову поправити свій код, йому доведеться полагодити наш адаптер.
Ну а ми вирішили що скоро взагалі перепишемо цей модуль і нам так само варто захистити наш залежний модуль. Оскільки ми використовуємо з модуля E побільше, то інтерфейс вашого колеги нам не годиться. Нам потрібно реалізувати свій. Нам також доведеться реалізувати цей інтерфейс в рамках модуля E, щоб потім, коли ми будемо переписувати його, не забути підправити реалізацію. Поглянемо що у нас вийшло:
Граф залежностей з інверсією
Дуже важливо те, що у нас два інтерфейсу, а не один. Якщо б ми помістили інтерфейс в модуль E, ми б не усунули залежності між модулями. Тим більше, різним модулям потрібні різні можливості. Наше завдання ізолювати рівно ту частину, яку ми збираємося використовувати. Це значно спростить підтримку.
Так само, якщо ви подивіться на картинку вище, ви можете помітити, що оскільки реалізація адаптерів лежить в модулі E, тепер цей модуль змушений реалізовувати інтерфейси з інших модулів. Тим самим ми инвертировали напрям стрілки, що вказує залежність. Ми инвертировали залежності.
Не всі залежності варті того, щоб їх перевертання
Модулі тепер менше пов'язані між собою, чого ми, власне, і добивалися. Ми не стали робити це для всього, оскільки змін в інших модулях найближчі пару років не передбачається. Не варто хвилюватися про зміни у тому, що рідко змінюється. А ось якщо у вас є шматки системи, які часто змінюються, або ви просто зараз не знаєте що там буде за підсумком, має сенс захиститися від можливих змін.
наприклад, якщо нам знадобиться логгер, ми завжди зможемо використовувати інтерфейс
PSR\Logger
оскільки він стандартизований, а такі речі вкрай рідко змінюються. Потім ми зможемо вибрати будь-логгер реалізує цей інтерфейс на наш смак:
Інверсія залежності між компонентами
Як ви можете бачити, завдяки цьому інтерфейсу, наш додаток все ще не залежить від конкретного логер. Логгер ж залежить від цієї абстракції. Але обидва "модуля" не залежать один від одного.
Ізоляція
Інтерфейси і пізніше зв'язування дозволяють нам "абстрагувати" реалізацію логіки від сторонніх деталей. Ми повинні намагатися робити модулі як можна більш ізольованими і самодостатніми. Коли всі модулі незалежні, ми отримуємо можливість і незалежно їх розвивати. А це може бути важливо з точки зору бізнесу.
Часто, коли мова заходить про абстракції, люди люблять доводити все до крайності, забуваючи навіщо спочатку все це потрібно.
Коли проект планується підтримувати набагато довше, ніж період підтримки вашого фреймворку, має сенс всі використовувані речі загорнути в адаптери. Це свого роду крайність, але в таких умовах вона виправдана. Міняти фреймворк ми навряд чи будемо, а ось оновити мажорну версію в майбутньому без болю ми мабуть би хотіли.
Або, наприклад, ще одна поширена помилка — абстракція від сховища. Можливість повної заміни бази даних ні в якому разі не є метою реалізації цієї абстракції, це скоріше критерій якості. Замість цього ми просто повинні дати такий рівень ізоляції, щоб наша логіка не залежала від можливостей бази даних. Причому це не означає, що ми не повинні користуватися цими можливостями.
наприклад ми реалізували пошук в нашій улюбленій MySQL, але в підсумку був потрібен більш якісна реалізація. І ми вирішили взяти ElasticSearch для цього, просто тому, що з ним робити пошук швидше. Відмовлятися від MySQL ми так само не можемо, але завдяки збудованій абстракції, ми можемо додати ще одну базу даних, щоб ефективніше виконати конкретне завдання.
Або ми робимо чергову соц мережа, і нам треба якось трекать репости. Так, ми можемо зробити це на MySQL але вийде незручно. Тут напрошуються графові бази даних. І таких сценаріїв маси. Ми повинні керуватися здоровим глуздом, в першу чергу, а не догмами.
На цьому мабуть все. Я впевнений, що я не все сказав і можуть залишитися питання, бо не соромимося задавати їх у коментарях. Так само я впевнений, що знаю аж ніяк не все, і буду радий коментарям розкривають тему трохи глибше, приклади з життя, коли інверсія залежності допомогла, чи могла б допомогти. Ну і якщо ви знайшли неточності/помилки у статті — буду радий повідомленнями в лічку.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.