1. Декомпозиція – наше все

Для наочності – ось тобі картинка з непоганої статті "Decoupling of Object-Oriented Systems", що ілюструє основні моменти, про які йтиметься.

Декомпозиція

Тобі все ще здається, що проєктування архітектури програми – це просто?

2. Інтерфейси, приховування реалізації

Головними для зменшення зв'язаності системи є принципи ООП – Інкапсуляція + Абстракція + Поліморфізм.

Саме тому:

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

Принцип "чорної скриньки" (інкапсуляція) дозволяє розглядати структуру кожної підсистеми незалежно від інших підсистем. Модуль, що є “чорною скринькою”, можна відносно вільно змінювати. Проблеми можуть виникнути лише на стику різних модулів (або модуля та оточення).

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

Саме тому Servlet – це інтерфейс: веб-контейнер нічого не знає про сервлети, для нього це якісь об'єкти, які реалізує інтерфейс Servlet – і все. Сервлети теж трохи знають про влаштування контейнера. Інтерфейс Servlet – це той контракт, той стандарт та мінімальна взаємодія, яку потрібна, щоб Java-вебзастосунки завоювали світ.

Поліморфізм — це не перевизначення методів, як іноді помилково вважають, а насамперед взаємозамінність модулів/об'єктів з однаковим інтерфейсом, або – «один інтерфейс, безліч реалізацій». Задля реалізації поліморфізму механізм успадкування не потрібен. Це важливо розуміти, оскільки успадкування взагалі, по можливості, слід уникати.

Завдяки інтерфейсам та поліморфізму досягається можливість модифікувати та розширювати код без зміни того, що вже написано (Open-Closed Principle).

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

Це як у конструкторі LEGO — інтерфейс стандартизує взаємодію та служить свого роду конектором, куди можна підключити будь-який модуль із відповідним роз'ємом.

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

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

Чим у більш загальній/абстрактній формі визначені інтерфейси і що менше обмежень вони накладають на взаємодію, тим гнучкіша система. Звідси фактично випливає ще один із принципів SOLID – Принцип поділу інтерфейсу (Interface Segregation Principle), який виступає проти "товстих інтерфейсів".

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

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

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

3. Фасад: інтерфейс модуля

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

Відповідь: якщо говорити мовою патернів проєктування, за реалізацію інтерфейсу модуля може відповідати спеціальний об'єкт — Фасад. Якщо ти викликаєш методи об'єкта, що містить суфікс Gateway (наприклад MobileApiGateway), то, швидше за все, це фасад.

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

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

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

Фасад: інтерфейс модуля

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