1. Ієрархічна декомпозиція
Ніколи не варто одразу починати писати класи вашої програми. Спершу її треба спроєктувати. Проєктування має закінчитись продуманою архітектурою. І щоб отримати цю архітектуру, тобі потрібно послідовно виконати декомпозицію системи.
Декомпозицію треба проводити ієрархічно – спочатку систему розбивають на великі функціональні модулі/підсистеми, що описують її роботу в загальному вигляді. Потім отримані модулі аналізуються більш детально і поділяються на підмодулі чи об'єкти.
Перед тим як виділяти об'єкти, розділіть систему на основні смислові блоки хоча б подумки. У невеликих програмах зазвичай це дуже просто: пари рівнів ієрархії буває цілком достатньо, оскільки система спочатку ділиться на підсистеми/пакети, а пакети діляться на класи.

Ця думка не така банальна, як здається. Наприклад, у чому полягає суть такого поширеного "архітектурного шаблону" як Модель-Вид-Контролер (MVC)?
Всього лише у відділенні представлення від бізнес-логіки. Спочатку будь-який застосунок користувача ділиться на два модулі – один відповідає за реалізацію самої бізнес логіки (Модель), а другий – за взаємодію з користувачем (Інтерфейс користувача або Подання).
Потім з'ясовується, що модулі повинні взаємодіяти, для цього в них додають Контролер, завдання якого – керувати взаємодією модулів. Також у мобільній (класичній) версії MVC до нього додають патерн Спостерігач, щоб View міг отримувати події з моделі та змінювати відображені дані в реальному часі.
Типовими модулями верхнього рівня, отриманими в результаті першого поділу системи на найбільші складові частини, якраз і є:
- Бізнес-логіка;
- Користувальницький інтерфейс;
- База даних;
- Система обміну повідомленнями;
- Контейнер об'єктів.
При першому розбитті зазвичай вся програма розділяється на 2-7, максимум – 10 частин. Якщо розбити на більшу кількість частин, потім виникне бажання їх згрупувати, і ми отримаємо знову ж таки 2-7 модулів верхнього рівня.
2. Функціональна декомпозиція
Розподіл на модулі/підсистеми найкраще проводити з тих завдань, які вирішує система. Основне завдання розбивається на складові підзавдання, які можуть вирішуватися/виконуватися автономно, незалежно один від одного.
Кожен модуль повинен відповідати за вирішення якоїсь підзадачі та виконувати відповідну їй функцію. Крім функціонального призначення модуль характеризується також набором даних, необхідних для виконання його функції, тобто:
Модуль = Функція + Дані, необхідні її виконання.
Якщо декомпозиція на модулі виконана правильно, взаємодія з іншими модулями (що відповідають інші функції) буде мінімальною. Воно може бути, але його відсутність не повинна бути критично важливою для вашого модуля.
Модуль – це не довільний шматок коду, а окрема функціонально осмислена та закінчена програмна одиниця (підпрограма), яка забезпечує вирішення деякого завдання та в ідеалі може працювати самостійно або в іншому оточенні та бути перевикористовуваною. Модуль має бути якоюсь "цілісністю, здатною до відносної самостійності в поведінці та розвитку". (Крістофер Александер)
Отже, грамотна декомпозиція грунтується передусім на аналізі функцій системи та необхідних для виконання цих функцій даних. Функції в цьому випадку – це не функції класу та модулі, адже це не об'єкти. Якщо у тебе в модулі лише пара класів, ти перестарався.
3. Сильна та слабка зв'язність
Дуже важливо не перестаратися із розбиттям на модулі. Якщо дати новачкові монолітний Spring-застосунок і попросити розбити його на модулі, він винесе кожен Spring Bean в окремий модуль і вважатиме, що його робота закінчена. Але це не так.
Головним критерієм якості декомпозиції є те, наскільки модулі сфокусовані на вирішенні своїх завдань та є незалежними.
Зазвичай це формулюють так: "Модулі, отримані в результаті декомпозиції, повинні бути максимально пов'язані всередині (high internal cohesion) і мінімально пов'язані один з одним (low external coupling)."
High Cohesion, висока сполученість або "згуртованість" усередині модуля, говорить про те, що модуль сфокусований на вирішенні однієї вузької проблеми, а не займається виконанням різнорідних функцій або незв'язаних між собою обов'язків.
Сполученість – cohesion, характеризує ступінь, в якій завдання, що виконуються модулем, пов'язані одне з одним.
Наслідком High Cohesion є принцип єдиної відповідальності (Single Responsibility Principle — перший із п'яти принципів SOLID), згідно з яким будь-який об'єкт/модуль повинен мати лише один обов'язок і не має бути більше однієї причини для його зміни.
Low Coupling, слабка пов'язаність, означає, що модулі, на які розбивається система, повинні бути, по можливості, незалежні або слабо пов'язані одна з одною. Вони повинні мати можливість взаємодіяти, але при цьому якнайменше знати одна про одну.
Кожен модуль не повинен знати, як влаштовано інший модуль, якою мовою він написаний і як працює. Часто для організації взаємодії таких модулів використовують якийсь контейнер, до якого ці модулі завантажуються.
При правильному проєктуванні, при зміні одного модуля не доведеться виправляти інші, або ці зміни будуть мінімальними. Чим слабша зв'язаність, тим легше писати/розуміти/розширювати/фіксити програму.
Вважається, що добре спроєктовані модулі повинні мати такі властивості:
- Функціональна цілісність і завершеність – кожен модуль реалізує одну функцію, але реалізує добре і повністю, модуль самостійно виконує повний набір операцій для реалізації своєї функції.
- Один вхід і один вихід – на вході програмний модуль отримує певний набір вихідних даних, виконує змістовну обробку та повертає один набір результатних даних, тобто реалізується стандартний принцип IPO – вхід->процес->вихід.
- Логічна незалежність — результат роботи програмного модуля залежить від вихідних даних, але не залежить від роботи інших модулів.
- Слабкі інформаційні зв'язки з іншими модулями – обмін інформацією між модулями має бути, по можливості, мінімізований.
Новачку дуже складно зрозуміти, як знизити зв'язаність модулів ще сильніше. Частково це усвідомлення приходить із досвідом, частково – після читання розумних книг. Але найкраще допомагає аналіз архітектур існуючих програм.
4. Композиція замість успадкування
Грамотна декомпозиція – це своєрідне мистецтво і складне завдання для більшості програмістів. Простота тут оманлива, а помилки коштують дорого.
Буває, що виділені модулі сильно зчеплені один з одним, і їх не вдається розробляти незалежно. Або не зрозуміло за яку функцію кожен із них відповідає. Якщо ти стикаєшся зі схожою проблемою, швидше за все розбиття на модулі неправильне.
Завжди має бути зрозуміло, яку роль виконує кожний модуль. Найнадійніший критерій того, що декомпозиція робиться правильно, це якщо модулі виходять самостійними та цінними підпрограмами, які можна використати у відриві від решти програми (а отже, їх можна повторно використовувати).
Під час декомпозиції системи бажано перевіряти її якість. Для цього постав собі питання: "Яке завдання виконує кожен модуль?", "Наскільки модулі легко тестувати?", "Чи можливо використовувати модулі самостійно або в іншому оточенні?", "Як сильно зміни в одному модулі позначаться інших?”.
Намагайся робити так, щоб модулі були гранично автономними. Як вже сказано раніше, це є ключовим параметром правильної декомпозиції. Тому проводити її потрібно таким чином, щоб модулі спочатку слабко залежали один від одного. Якщо це в тебе вийшло, ти молодець.
Якщо ні, також не все втрачено. Є ряд спеціальних технік та шаблонів, що дозволяють додатково мінімізувати та послабити зв'язки між підсистемами. Наприклад, у випадку MVC для цієї мети використовувався шаблон "Спостерігач", але можливі інші рішення.
Можна сміливо сказати, що техніки зменшення зв'язаності входять до основного “інструментарію архітектора”. Але необхідно розуміти, що йдеться про всі підсистеми і послаблювати пов'язаність потрібно на всіх рівнях ієрархії, тобто не тільки між класами, але також між модулями на кожному ієрархічному рівні.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ