Як рефакторити God Object (божественний об'єкт)
Такий клас порушує одразу всі принципи SOLID (у першу чергу SRP — у нього десятки причин для зміни).
🟢 Junior Level
God Object (Божественний об’єкт) або The Blob — це один із найпоширеніших антипатернів в ООП. Це гігантський клас, який «знає і робить абсолютно все»: концентрує в собі бізнес-логіку десятка підсистем, роботу з базою даних, валідацію, відправку повідомлень та генерацію звітів.
Такий клас порушує одразу всі принципи SOLID (у першу чергу SRP — у нього десятки причин для зміни).
Життєва аналогія
Уявіть одного співробітника в компанії, який одночасно працює генеральним директором, головним бухгалтером, системним адміністратором, кур’єром та прибиральником:
- Якщо він захворіє — вся компанія миттєво зупиняється.
- Черга до нього в кабінет розтягується на кілометри.
- Він постійно плутає документи, тому що не може втримати в голові 50 різнорідних обов’язків.
- Рішення: найняти профільних фахівців та розподілити завдання.
Симптоми God Object у Java-проєкті
- Розмір файлу перевищує 2000–5000 рядків коду.
- У назві класу присутні суфікси
Manager,Processor,Handler,EngineабоController. - Конструктор приймає 15–20 залежностей або містить десятки мутабельних полів.
- Розробники бояться вносити навіть косметичні правки через непередбачувані регресійні баги.
Базовий 4-кроковий сценарій розпилу
Крок 1: [Тестовий каркас] ──> Покрити старий God Object тестами
Крок 2: [Групування] ──> Виписати методи за бізнес-ролями (Кластери)
Крок 3: [Extract Class] ──> Винести групу методів в окремий клас
Крок 4: [Фасад / Проксі] ──> Старий клас тимчасово делегує виклики новому
🟡 Middle Level
Алгоритм декомпозиції: Від моноліту до чистих сервісів
Розпил багатотисячного класу не можна робити революційним методом («переписати все з нуля за вихідні»). Застосовується покроковий еволюційний рефакторинг за Мартіном Фаулером:
Крок 1. Характеризаційні тести (Characterization Tests)
Неможливо безпечно рефакторити те, у чому ви до кінця не впевнені. Пишуться тести «чорної скриньки» (Golden Master / Approval Tests), що фіксують поточну поведінку системи для ключових сценаріїв, включаючи специфічні граничні випадки.
Крок 2. Матриця зв’язності полів і методів (LCOM-аналіз)
Складається таблиця, що показує, які методи звертаються до яких полів:
Поле: dbUrl templateEngine smsApiKey taxRates
Метод:
calculateTax() - - - [X] ──> Кластер 1: TaxCalculator
sendSms() - - [X] - ──> Кластер 2: SmsNotificationService
saveOrder() [X] - - - ──> Кластер 3: OrderRepository
renderPdf() - [X] - - ──> Кластер 4: InvoicePdfRenderer
Крок 3. Введення інтерфейсів клієнтів (ISP)
Щоб код, що викликає, не залежав від усієї махіни, виділяються вузькі інтерфейси:
public interface OrderPersistence { void save(Order order); }
public interface InvoiceGenerator { byte[] generatePdf(Order order); }
Клас GodOrderManager тимчасово оголошується як implements OrderPersistence, InvoiceGenerator.
Крок 4. Патерн Strangler Fig (Фігове дерево / Задушення)
Нова функціональність виноситься в новий самостійний Spring-бін (TaxCalculator). Старий клас GodOrderManager інжектує цей новий бін і просто перенаправляє виклик:
@Service
public class GodOrderManager {
// Новий винесений сервіс
private final TaxCalculator taxCalculator;
public BigDecimal calculateTax(Order order) {
return taxCalculator.calculateTax(order); // ДЕЛЕГУВАННЯ
}
}
Клієнти почергово переводяться на прямий виклик TaxCalculator. Коли в GodOrderManager не залишиться логіки, він безболісно видаляється.
🔴 Senior Level
Глибокі проблеми розпилу: Спільний стан і транзакції
1. Проблема Shared Mutable State: Введення Context Object
У God Object методи часто обмінюються даними через внутрішні приватні поля:
// Антипатерн усередині God Object:
private Order currentOrder;
private User currentUser;
private BigDecimal calculatedDiscount;
public void step1() { this.calculatedDiscount = ...; }
public void step2() { send(this.currentUser, this.calculatedDiscount); }
При розпилі на незалежні класи поля більше не можуть бути спільними. Рішення: Вводиться незмінний об’єкт контексту (Parameter Object / Context Object):
public record OrderProcessingContext(Order order, User customer, BigDecimal discount) {}
Контекст передається як параметр у конвеєр викликів (Pipeline Pattern).
2. Руйнування транзакційних меж (@Transactional)
У монолітному God Object метод processAll() був захищений єдиною транзакцією @Transactional:
// Усередині God Object:
@Transactional
public void process() {
orderRepo.save();
inventoryRepo.decrementStock();
userRepo.updateLoyaltyPoints();
billingRepo.charge();
}
Коли ці операції розносяться за незалежними мікросервісами або сервісами з різними джерелами даних:
- Єдина транзакція БД більше не працює.
- Застосовується патерн Saga (оркестрація компенсувальних транзакцій): якщо списання в
billingRepoне вдалося, оркестратор викликає компенсувальні крокиinventoryRepo.rollbackStock().
3. Апаратний ефект: Усунення деградації CPU Cache та JIT
У God Object (4000+ рядків байткоду):
- Методи перевищують ліміти інлайнінгу C2 (
MaxInlineSize = 35байткодів за замовчуванням). JIT не може оптимізувати гарячі ділянки. - Розділення God Object на невеликі класи (до 300 рядків) різко підвищує відсоток влучання в L1 Instruction Cache (I-Cache) процесора.
- Рознесення гарячих змінних по різних об’єктах у купі повністю усуває взаємну інвалідацію кеш-ліній (False Sharing).
4 Tricky Questions
1. Що робити, якщо при спробі вирізати шматок коду з God Object виникає циклічна залежність між старим і новим класом?
Відповідь:
Циклічна залежність (GodClass <---> ExtractedService) виникає, коли новому класу потрібні методи або поля, що залишилися в старому God-класі.
Способи усунення:
- Dependency Inversion (DIP): Новий клас не повинен знати про старий God-клас. Він оголошує інтерфейс-колбек того, що йому потрібно, а старий клас реалізує цей контракт.
- Event-Driven Architecture: Заміна прямого виклику на публікацію доменної події. Виділений сервіс публікує
OrderValidatedEvent, а старий клас слухає його через Spring@EventListener. - Виділення третього спільного контексту: Спільно використовувані дані виносяться в незалежний
OrderContext, що передається в обидва класи за посиланням.
2. Як розпиляти статичний God Object (наприклад, GlobalUtils на 3000 рядків static методів)?
Відповідь: Статичні методи неможливо мокати та впроваджувати через інтерфейси. План рефакторингу:
- Згрупувати статичні методи за доменами (
StringUtils,DateUtils,SecurityUtils). - Огорнути статичні виклики в екземпляри класів-сервісів з інтерфейсами (наприклад, створити бін
SystemTimeProvider implements TimeProvider, який усередині викликаєSystem.currentTimeMillis()). - Поступово впроваджувати цей інтерфейс через DI у споживачів, витісняючи прямі статичні виклики.
- Оголосити застарілий клас
@Deprecatedіз забороною на додавання нових методів через правила лінтера Checkstyle.
3. Чому рефакторинг God Object технічно безпечніший, ніж переписування системи з нуля («Greenfield Rewrite»)?
Відповідь: Переписування з нуля здається привабливим, але в 90% випадків закінчується провалом проєкту («Друга системна помилка» Фредеріка Брукса). Старий God Object за роки роботи накопичив у собі сотні прихованих багфіксів, обробок граничних умов інтеграцій та крайових сценаріїв законодавства, про які не пам’ятає жоден живий бізнес-аналітик. Поступовий рефакторинг через Strangler Fig Pattern дозволяє зберігати працюючу бізнес-логіку в продакшені щодня, перевіряючи зміни автоматизованими тестами, у той час як Greenfield-проєкт часто роками не може наздогнати функціонал старої системи.
4. Як розпиляти монолітний switch(orderType) на 30 варіантів усередині God Object, якщо варіанти залежать від внутрішніх полів?
Відповідь: Застосовується каталог Фаулера «Replace Conditional with Polymorphism»:
- Вводиться загальний інтерфейс стратегії
OrderProcessingStrategyз методамиboolean supports(OrderType type)таvoid execute(OrderProcessingContext ctx). - Кожна гілка
caseвирізається в окремий клас із власними інжектованими залежностями. - У God Object створюється реєстр
Map<OrderType, OrderProcessingStrategy>, куди Spring автоматично складає всі зареєстровані стратегії. - Монолітний блок
switchзамінюється на один рядок:strategies.get(type).execute(ctx).
🎯 Шпаргалка для інтерв’ю
Головні тези (30 секунд на відповідь):
- Що це таке: Антипатерн класу, що знає і робить занадто багато (LCOM > 1.0, порушення SRP, OCP та ISP).
- Головний патерн розпилу: Strangler Fig (Фігове дерево) — поступова заміна через фасад і делегування без Big Bang.
- Інструменти декомпозиції:
- Characterization Tests (Golden Master) — фіксація поведінки legacy-коду до початку правок.
- LCOM4-аналіз — групування методів і полів за кластерами.
- Context Object — усунення спільного мутабельного стану.
- Replace Conditional with Polymorphism (Strategy Pattern) — розпил гігантських
switch.
- Продуктивність: Розпил God Object розвантажує L1 Instruction Cache та усуває вузьке горлечко єдиного монітора синхронізації.
Чого уникати (Red Flags):
- ❌ Пропонувати «викинути старий клас і переписати заново з нуля» (високий ризик втрати крайової бізнес-логіки).
- ❌ Починати рефакторинг без попереднього покриття тестами («рефакторинг наосліп»).
- ❌ Замінювати один God-клас на один
GodUtilsзі статичними методами.