📐 Раздел 18 · Вопрос #18

Как рефакторить God Object (божественный объект)

Такой класс нарушает сразу все принципы SOLID (в первую очередь SRP — у него десятки причин для изменения).


🟢 Junior Level

God Object (Божественный объект) или The Blob — это один из самых распространенных антипаттернов в ООП. Это гигантский класс, который «знает и делает абсолютно всё»: концентрирует в себе бизнес-логику десятка подсистем, работу с базой данных, валидацию, отправку сообщений и генерацию отчетов.

Такой класс нарушает сразу все принципы SOLID (в первую очередь SRP — у него десятки причин для изменения).

Житейская аналогия

Представьте одного сотрудника в компании, который одновременно работает генеральным директором, главным бухгалтером, системным администратором, курьером и уборщиком:

  • Если он заболеет — вся компания мгновенно встает.
  • Очередь к нему в кабинет растягивается на километры.
  • Он постоянно путает документы, потому что не может удержать в голове 50 разнородных обязанностей.
  • Решение: нанять профильных специалистов и распределить задачи.

Симптомы God Object в Java-проекте

  1. Размер файла превышает 2000–5000 строк кода.
  2. В названии класса присутствуют суффиксы Manager, Processor, Handler, Engine или Controller.
  3. Конструктор принимает 15–20 зависимостей или содержит десятки мутабельных полей.
  4. Разработчики боятся вносить даже косметические правки из-за непредсказуемых регрессионных багов.

Базовый 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-классе. Способы устранения:

  1. Dependency Inversion (DIP): Новый класс не должен знать о старом God-классе. Он объявляет интерфейс-коллбэк того, что ему требуется, а старый класс реализует этот контракт.
  2. Event-Driven Architecture: Замена прямого вызова на публикацию доменного события. Выделенный сервис публикует OrderValidatedEvent, а старый класс слушает его через Spring @EventListener.
  3. Выделение третьего общего контекста: Совместно используемые данные выносятся в независимый OrderContext, передаваемый в оба класса по ссылке.

2. Как распилить статический God Object (например, GlobalUtils на 3000 строк static методов)?

Ответ: Статические методы невозможно мокать и внедрять через интерфейсы. План рефакторинга:

  1. Сгруппировать статические методы по доменам (StringUtils, DateUtils, SecurityUtils).
  2. Обернуть статические вызовы в экземпляры классов-сервисов с интерфейсами (например, создать бин SystemTimeProvider implements TimeProvider, который внутри вызывает System.currentTimeMillis()).
  3. Постепенно внедрять этот интерфейс через DI в потребителей, вытесняя прямые статические вызовы.
  4. Объявить устаревший класс @Deprecated с запретом на добавление новых методов через правила линтера Checkstyle.

3. Почему рефакторинг God Object технически опаснее, чем переписывание системы с нуля («Greenfield Rewrite»)?

Ответ: Переписывание с нуля кажется привлекательным, но в 90% случаев заканчивается провалом проекта («Вторая системная ошибка» Фредерика Брукса). Старый God Object за годы работы накопил в себе сотни скрытых багфиксов, обработок граничных условий интеграций и краевых сценариев законодательства, о которых не помнит ни один живой бизнес-аналитик. Постепенный рефакторинг через Strangler Fig Pattern позволяет сохранять работающую бизнес-логику в проде каждый день, проверяя изменения автоматизированными тестами, в то время как Greenfield-проект часто годами не может догнать функционал старой системы.

4. Как распилить монолитный switch(orderType) на 30 вариантов внутри God Object, если варианты зависят от внутренних полей?

Ответ: Применяется каталог Фаулера «Replace Conditional with Polymorphism»:

  1. Вводится общий интерфейс стратегии OrderProcessingStrategy с методами boolean supports(OrderType type) и void execute(OrderProcessingContext ctx).
  2. Каждая ветка case вырезается в отдельный класс с собственными инжектируемыми зависимостями.
  3. В God Object создается реестр Map<OrderType, OrderProcessingStrategy>, куда Spring автоматически складывает все зарегистрированные стратегии.
  4. Монолитный блок switch заменяется на одну строчку: strategies.get(type).execute(ctx).

🎯 Шпаргалка для интервью

Главные тезисы (30 секунд на ответ):

  • Что такое: Антипаттерн класса, знающего и делающего слишком много (LCOM > 1.0, нарушение SRP, OCP и ISP).
  • Главный паттерн распила: Strangler Fig (Фиговое дерево) — постепенная замена через фасад и делегирование без Big Bang.
  • Инструменты декомпозиции:
    1. Characterization Tests (Golden Master) — фиксация поведения legacy-кода до начала правок.
    2. LCOM4-анализ — группировка методов и полей по кластерам.
    3. Context Object — устранение разделяемого мутабельного состояния.
    4. Replace Conditional with Polymorphism (Strategy Pattern) — распил гигантских switch.
  • Производительность: Распил God Object разгружает L1 Instruction Cache и устраняет узкое горлышко единственного монитора синхронизации.

Чего избегать (Red Flags):

  • ❌ Предлагать «выкинуть старый класс и переписать заново с нуля» (высокий риск потери краевой бизнес-логики).
  • ❌ Начинать рефакторинг без предварительного покрытия тестами («рефакторинг вслепую»).
  • ❌ Заменять один God-класс на один GodUtils со статическими методами.

Связанные темы: