- PVSM.RU - https://www.pvsm.ru -
Планируя архитектуру своего будущего веб-приложения, разработчику полезно подумать о его расширяемости заранее. Модульная архитектура приложения может обеспечить хорошую степень расширяемости. Существует довольно много способов, как такую архитектуру реализовать, но все они сходны в своих фундаментальных принципах: разделение понятий, самодостаточность, взаимная сочетаемость всех компонентов.
Однако есть один подход, который именно в PHP можно встретить довольно редко. Он включает использование нативного наследования и позволяет патчить код «более лучше»(с). Мы называем этот способ “Forwarding Decorator”. Нам он представляется достаточно эффективным, и, кстати, эффектным тоже, хотя последнее не так важно в продакшене.
Как автор оригинальной англоязычной статьи "Achieving Modular Architecture with Forwarding Decorators [1]", опубликованной на SitePoint, я представляю вам авторскую версию перевода. В ней я сохранил изначально задуманный смысл и идею, но постарался максимально улучшить подачу.
В данной статье мы рассмотрим реализацию подхода с использованием Forwarding Decorator, а также его плюсы и минусы. Сравним данный подход с другими хорошо известными альтернативами, а именно — с использованием хуков, патчингом кода или DI (dependency injection). Для наглядности есть демо-приложение вот в этом репозитории GitHub [2].
Основная идея состоит в том, чтобы рассматривать каждый класс как сервис, и модифицировать этот сервис посредством наследования и реверсирования цепочки наследников при компиляции кода.
В системе, основанной на такой идее, в любом модуле можно создать специальный класс-декоратор (отмеченный особым образом). Такой класс получит поля и методы другого класса через механизм наследования, но после компиляции будет везде использоваться вместо оригинального класса.
Собственно, поэтому мы и называем такие классы Forwarding decorators: эти декораторы являются надстройкой над исходной реализацией, однако выдвигаются вперед в местах использования.
Преимущества такого подхода очевидны:
Однако свои недостатки у этого подхода тоже есть:
Подобный способ расширения системы — в некотором смысле промежуточное решение между прямым патчингом кода (low-level, никаких правил игры, god mode, greatest power but with greatest responsibility и т.д.) и архитектурой на основе плагинов, с четким определением того, каким может быть плагин, какие подсистемы и как он может изменятьпредоставлять. Система декораторов позволяет хорошо решать некоторый диапазон задач, но это вовсе не серебряная пуля и не идеальный способ организовать модульность.
Вот пример:
class Foo {
public function bar() {
echo 'baz';
}
}
namespace Module1;
/**
*
Это класс особого декоратора, его отметкой служит DecoratorInterface (примечание переводчика: также можно использовать аннотации, конфиги и проч)
*/
class ModifiedFoo extends Foo implements DecoratorInterface {
public function bar() {
parent::bar();
echo ' modified';
}
}
// ... где-то в коде приложения
$object = new Foo();
$object->bar(); // will echo 'baz modified'
Как так вышло? Это уличная магия ) Мы разворачиваем цепочку наследования вспять. Исходный класс — без внутреннего кода. В результате компиляции мы препроцессим код так, что содержимое исходного класса уходит в отдельный класс, который будет новым родителем для цепочки:
// пустой код исходного класса, который будет использоваться, чтобы инстанцировать новые объекты
class Foo extends Module1ModifiedFoo {
// move the implementation from here to FooOriginal
}
namespace Module1;
// Здесь мы создаем особый класс, который будет расширять другой класс с исходным кодом
abstract class ModifiedFoo extends FooOriginal implements DecoratorInterface {
public function bar() {
parent::bar();
echo ' modified';
}
}
// Новый родительский класс с исходным кодом. Все цепочки наследования будут начинаться с него
class FooOriginal {
public function bar() {
echo 'baz';
}
}
Если кратко, то в приложение встраивается компилятор, который строит промежуточные классы, и autoloader, который будет загружать эти промежуточные классы вместо исходных.
А теперь немного подробнее. Компилятор строит список всех классов, используемых в системе, и для каждого класса, который не является декоратором, находит все подклассы, которые будут его декорировать с помощью DecoratorInterface. Он создает дерево декораторов, проверяет, нет ли там циклов, сортирует декораторы по их приоритету об этом подробнее далее) и строит промежуточные классы, где цепочка наследования будет развернута в обратную сторону. Исходный код преобразуется в новый класс, который станет новым родительским классом для цепочки наследования.
Звучит сложно. Так оно и есть, это действительно сложная комплексная система. Однако она позволяет очень гибко комбинировать модули, и с помощью этих модулей вы можете модифицировать абсолютно любую часть вашего приложения.
В случае, если в игру вступает несколько декораторов одновременно, они попадают в цепочку декорирования согласно их приоритету. Приоритет можно задать с помощью аннотаций (мы пользуемся DoctrineAnnotations) или конфигов.
Рассмотрим пример:
class Foo {
public function bar() {
echo 'baz';
}
}
namespace Module1;
class Foo extends Foo implements DecoratorInterface {
public function bar() {
parent::bar();
echo ' modified';
}
}
namespace Module2;
/**
* @DecoratorAfter("Module1")
*/
class Foo extends Foo implements DecoratorInterface {
public function bar() {
parent::bar();
echo ' twice';
}
}
// ... где-то в коде приложения
$object = new Foo();
$object->bar(); // вывод 'baz modified twice'
В данном примере аннотация DecoratorAfter используется, чтобы поставить декоратор другого модуля Module 1 перед модулем Module 2. Компилятор проанализирует файлы, учтет аннотации и построит промежуточный класс с такой цепочкой наследования:
Также можно использовать такие аннотации:
Данного набора аннотаций (Before, After, Depend) абсолютно достаточно для построения любой комбинации модулей и классов.
Есть! Для наглядности я подготовил демку приложения, она находится вот в этом репозитории GitHub [2]. Это написанное на PHP приложение имеет модульную архитектуру, и модули могут подмешивать код без рекомпиляции. При этом модули можно добавлять и удалять, но в этом случае рекомпиляция уже понадобится. Более детально все это описано в readme [4] файле.
Есть и совсем «боевые» примеры. На рынке уже есть несколько программных продуктов, которые используют такой подход. В частности, нечто очень похожее используется в OXID eShop. Кстати, у них прикольный стиль изложения в блоге [5]. Еще в одной платформе, X-Cart 5, данный подход реализован именно в той форме, в которой я его описал — код X-Cart 5 [6] даже был взят за основу для этой статьи. Это позволило создать очень гибкое решение для электронной коммерции, которое можно расширять настолько, насколько хватит фантазии разработчика (или денег заказчика =)), и при этом не ломать последующие апгрейды ядра.
Как и подход с Forwarding Decorators, использование хуков и патчинг “в лоб” имеют свои плюсы и минусы.
Зависимости удовлетворяют некому интерфейсу и являются законченной реализацией некой функциональности. Через систему расширения можно подменять одну реализацию зависимости на другую исходя из текущей конфигурации системы.
Реализации могут быть наследованными от базовых или же декорированными в классическом смысле декоратора — как в Symfony 2, например, как описано здесь [7]. Проблема такой архитектуры в том, что весь код должен строиться c использованием DI-style получения зависимостей. Отличие от описанной в статье системы в том, что forwarding decorator позволяет подменять классы абсолютно прозрачно во всех точках использования.
Помимо этого, непонятно, как организовать композицию нескольких модулей, расширяющих один и тот же сервис — придется писать отдельную систему, т. к. популярные IoC-контейнеры никак не разрешают данную проблему (это находится вне области ответственности таких библиотек).
Forwarding-декораторы — это подход, по меньшей мере заслуживающий внимания. Он может использоваться для решения проблемы разработки расширяемой модульной архитектуры приложений на языке PHP. При этом будут использоваться знакомые конструкции, такие как наследование или область видимости полей/методов/классов.
Реализация такого концепта — задача нетривиальная, возможны сложности с отладкой, но они преодолимы при условии, что вы потратите некоторое время на должную настройку компилятора.
Если будет интерес к данному материалу, в следующей статье я напишу, как сделать оптимальный компилятор с автозагрузчиком и использовать потоковые фильтры (PHP Stream filters), чтобы включить пошаговый дебаггинг исходного кода через XDebug. Интересно? Дайте об этом знать в комментариях. А еще я буду рад вашим вопросам, советам и конструктивной критике.
Автор: Alex_Soloviev
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/255550
Ссылки в тексте:
[1] Achieving Modular Architecture with Forwarding Decorators: https://www.sitepoint.com/achieving-modular-architecture-with-forwarding-decorators/
[2] вот в этом репозитории GitHub: https://github.com/sitepoint-editors/decorators-demo
[3] принцип подстановки Лисков: https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B8%D0%BD%D1%86%D0%B8%D0%BF_%D0%BF%D0%BE%D0%B4%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B8_%D0%91%D0%B0%D1%80%D0%B1%D0%B0%D1%80%D1%8B_%D0%9B%D0%B8%D1%81%D0%BA%D0%BE%D0%B2
[4] readme: https://github.com/sitepoint-editors/decorators-demo/blob/master/README.md
[5] прикольный стиль изложения в блоге: https://oxidforge.org/en/developing-email-templates-for-oxid-eshop.html
[6] X-Cart 5: https://www.x-cart.com/features.html
[7] как описано здесь: http://symfony.com/doc/current/service_container/service_decoration.html
[8] Источник: https://habrahabr.ru/post/328970/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox
Нажмите здесь для печати.