-
-
Notifications
You must be signed in to change notification settings - Fork 7
Description
1. Контекст и Проблема
В проекте классы моделей (например, msVendor), наследуемые от xPDOSimpleObject, стали чрезмерно раздутыми. Это происходит из-за того, что в них непосредственно встроена побочная логика, которая должна выполняться при различных действиях с объектом (save, remove и т.д.).
- Пример: При удалении производителя (
msVendor) необходимо также сбросить привязку к нему у всех связанных товаров. - Проблема 1: Классы моделей становятся большими и сложными для поддержки.
- Проблема 2: Поведение объекта жёстко зашито в коде. Его невозможно изменить или расширить без прямого редактирования файла модели, что нарушает принцип открытости/закрытости.
- Проблема 3: Предыдущая попытка вынести логику в
VendorServiceпривела к смешиванию ответственности. В сервисе оказались как методы, вызываемые из модели (например,removeVendor, который лишь сбрасывает связи), так и методы для использования контроллерами. Это создает путаницу для разработчиков.
2. Предлагаемое Решение
Внедрить шаблон проектирования Наблюдатель (Observer) для обработки событий жизненного цикла моделей.
Ключевые компоненты:
-
Промежуточный базовый класс
ms3Object:- Изменяем цепочку наследования:
class msVendor extends ms3Object→abstract class ms3Object extends xPDOSimpleObject. - В этом классе будут переопределены методы
save(),remove()и др., где будут вызываться методы-«хуки» наблюдателя.
- Изменяем цепочку наследования:
-
Интерфейс и абстрактный класс Наблюдателя:
- Создадим интерфейс
ms3ObjectObserverInterfaceс методами:beforeSave(),afterSave(),beforeRemove(),afterRemove()и т.д. - Для удобства создадим абстрактный класс
ms3ObjectObserverс пустой реализацией этих методов (Stub).
- Создадим интерфейс
-
Конкретный наблюдатель для модели:
- Создадим класс
VendorLifecycleHandler(илиVendorObserver), реализующий интерфейс/наследующий абстрактный класс. Именно сюда будет перенесена вся побочная логика. - Пример: Логика очистки связей товаров при удалении производителя переместится в метод
beforeRemove()илиafterRemove()классаVendorLifecycleHandler.
- Создадим класс
-
Внедрение Зависимостей:
- В классе модели
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. Заключение
Предлагаемое решение является сильным архитектурным улучшением, которое делает код чище, гибче и лучше подготовленным к будущему развитию. Оно соответствует современным подходам в разработке ПО.
Рекомендация: Несмотря на наличие некоторых минусов, польза от внедрения этого паттерна значительно перевешивает риски. Для их минимизации следует:
- Снабдить систему четкой документацией.
- Реализовать базовый класс
ms3Objectтак, чтобы он мог работать без наблюдателя (вызовы к нему были безопасными). - Провести ревью первых реализованных наблюдателей, чтобы выработать единый стандарт их наполнения.
Это изменение заложит прочный фундамент для будущей архитектуры проекта.