Skip to content

Рефакторинг логики жизненного цикла моделей через шаблон Наблюдатель (Observer) #41

@createit-ru

Description

@createit-ru

1. Контекст и Проблема

В проекте классы моделей (например, msVendor), наследуемые от xPDOSimpleObject, стали чрезмерно раздутыми. Это происходит из-за того, что в них непосредственно встроена побочная логика, которая должна выполняться при различных действиях с объектом (save, remove и т.д.).

  • Пример: При удалении производителя (msVendor) необходимо также сбросить привязку к нему у всех связанных товаров.
  • Проблема 1: Классы моделей становятся большими и сложными для поддержки.
  • Проблема 2: Поведение объекта жёстко зашито в коде. Его невозможно изменить или расширить без прямого редактирования файла модели, что нарушает принцип открытости/закрытости.
  • Проблема 3: Предыдущая попытка вынести логику в VendorService привела к смешиванию ответственности. В сервисе оказались как методы, вызываемые из модели (например, removeVendor, который лишь сбрасывает связи), так и методы для использования контроллерами. Это создает путаницу для разработчиков.

2. Предлагаемое Решение

Внедрить шаблон проектирования Наблюдатель (Observer) для обработки событий жизненного цикла моделей.

Ключевые компоненты:

  1. Промежуточный базовый класс ms3Object:

    • Изменяем цепочку наследования: class msVendor extends ms3Objectabstract class ms3Object extends xPDOSimpleObject.
    • В этом классе будут переопределены методы save(), remove() и др., где будут вызываться методы-«хуки» наблюдателя.
  2. Интерфейс и абстрактный класс Наблюдателя:

    • Создадим интерфейс ms3ObjectObserverInterface с методами: beforeSave(), afterSave(), beforeRemove(), afterRemove() и т.д.
    • Для удобства создадим абстрактный класс ms3ObjectObserver с пустой реализацией этих методов (Stub).
  3. Конкретный наблюдатель для модели:

    • Создадим класс VendorLifecycleHandler (или VendorObserver), реализующий интерфейс/наследующий абстрактный класс. Именно сюда будет перенесена вся побочная логика.
    • Пример: Логика очистки связей товаров при удалении производителя переместится в метод beforeRemove() или afterRemove() класса VendorLifecycleHandler.
  4. Внедрение Зависимостей:

    • В классе модели msVendor будет объявлено свойство, указывающее на ключ наблюдателя в DI-контейнере: public string $observerKey = 'ms3_vendor_observer';.
    • Базовый класс ms3Object будет через этот ключ получать экземпляр своего наблюдателя и вызывать соответствующие хуки.

Упрощенная схема работы:

// 1. Модель
class msVendor extends ms3Object {
    public string $observerKey = 'ms3_vendor_observer';
}

// 2. Базовый класс
abstract class ms3Object extends xPDOSimpleObject {
    public function save($cacheFlag = false) {
        $this->getObserver()->beforeSave($this); // Хук "до сохранения"
        $result = parent::save($cacheFlag);
        if ($result) {
            $this->getObserver()->afterSave($this); // Хук "после сохранения"
        }
        return $result;
    }
}

// 3. Наблюдатель
class VendorLifecycleHandler extends ms3ObjectObserver {
    public function afterRemove(xPDOObject &$object) {
        // Очищаем привязки товаров к удаленному вендору
        $this->modx->exec("UPDATE ms2_product SET vendor = 0 WHERE vendor = {$object->get('id')}");
    }
}

3. Преимущества

  • ✅ Разделение ответственности: Модель отвечает только за данные и их базовую валидацию, а вся бизнес-логика жизненного цикла вынесена в отдельные, узкоспециализированные классы.
  • ✅ Гибкость и расширяемость: Поведение модели можно кардинально менять, просто подменяя реализацию наблюдателя через контейнер зависимостей (DI), без touches к коду самой модели.
  • ✅ Чистота кода: Классы моделей становятся компактными и понятными. Классы-наблюдатели легко находить и поддерживать.
  • ✅ Предсказуемость архитектуры: Разработчикам четко понятно, где находится логика жизненного цикла. Это решает проблему смешивания кода в сервисах.
  • ✅ Следование принципам SOLID: Решение напрямую соответствует принципам Open/Closed и Single Responsibility.

4. Потенциальные Минусы и Риски

  • ❌ Сложность отладки: Поток выполнения становится менее линейным. Чтобы понять, что происходит при сохранении объекта, придется смотреть и в модель, и в базовый класс, и в наблюдатель. Может усложнить дебаг для новичков в проекте.
  • ❌ Скрытые зависимости: Поведение модели теперь сильно зависит от внешнего по отношению к ней класса (Observer). Если наблюдатель не будет зарегистрирован в DI, поведение модели может измениться молча. Нужна четкая документация или, что лучше, fallback-реализация в ms3Object.
  • ❌ Легко переусердствовать: Может возникнуть соблазн помещать в наблюдатель всю логику, даже ту, что по своей природе должна быть в модели (например, простую валидацию полей). Важно выработать конвенцию, что именно принадлежит Observer.
  • ❌ Производительность (микро-оптимизация): Появление дополнительного слоя абстракции и вызовов к DI-контейнеру добавит микросекунды к времени выполнения операций save/remove. В 99% случаев это некритично, но для массовых операций стоит иметь в виду.
  • ❌ Миграция: Необходимо переписать все существующие модели, перенести логику и настроить DI. Это объемная работа, требующая тщательного тестирования.

5. Заключение

Предлагаемое решение является сильным архитектурным улучшением, которое делает код чище, гибче и лучше подготовленным к будущему развитию. Оно соответствует современным подходам в разработке ПО.

Рекомендация: Несмотря на наличие некоторых минусов, польза от внедрения этого паттерна значительно перевешивает риски. Для их минимизации следует:

  1. Снабдить систему четкой документацией.
  2. Реализовать базовый класс ms3Object так, чтобы он мог работать без наблюдателя (вызовы к нему были безопасными).
  3. Провести ревью первых реализованных наблюдателей, чтобы выработать единый стандарт их наполнения.

Это изменение заложит прочный фундамент для будущей архитектуры проекта.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions