Статьи

Достижение модульной архитектуры с пересылкой декораторов

Эта статья была рецензирована Юнесом Рафи и Кристофером Питтом . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!


По мере того как ваше веб-приложение становится больше, вы, безусловно, начинаете задумываться о разработке гибкой модульной архитектуры, которая должна обеспечивать высокую степень расширяемости. Существует много способов реализации такой архитектуры, и все они основаны на фундаментальных принципах: разделение задач, самостоятельность, сочетаемость частей приложения.

Существует один подход, который редко встречается в программном обеспечении PHP, но может быть эффективно реализован — он включает использование собственного наследования для обеспечения управляемого исправления программного кода; мы называем это «Экспедитор-экспедитор».

Картинка для внимания

Введение в концепцию

В этой статье мы рассмотрим реализацию подхода Forwarding Decorator и его плюсы / минусы. Вы можете увидеть работающее демонстрационное приложение в этом репозитории GitHub . Также мы сравним этот подход с другими известными, такими как перехватчики и исправление кода.

Основная идея состоит в том, чтобы рассматривать каждый класс как сервис и модифицировать этот сервис, расширяя его и обращая цепочку наследования путем компиляции кода. Если мы построим систему на основе этой идеи, любой модуль сможет содержать специальные классы (они будут помечены как-то отдельно от обычных классов), которые могут наследоваться от любого другого класса и будут использоваться где угодно вместо исходного объекта.

Сравнение оригинальных и скомпилированных классов

Вот почему он называется « Пересылка декораторов»: они оборачиваются вокруг исходной реализации и направляют модифицированный вариант на передний план для использования вместо него.

Преимущества такого подхода очевидны:

  • модули могут расширять практически любую часть системы, любой класс, любой метод; Вам не нужно планировать точки расширения заранее.
  • несколько модулей могут изменять одну подсистему одновременно.
  • Подсистемы слабо связаны и могут быть обновлены отдельно.
  • Система расширения основана на знакомом подходе наследования.
  • Вы можете контролировать расширяемость, создавая private методы и final классы.

С большой властью приходит большая ответственность, поэтому недостатки:

  • вам придется реализовать какую-то систему компиляции (подробнее об этом позже)
  • разработчики модулей должны соблюдать общедоступный интерфейс подсистем и не нарушать принцип подстановки Лискова ; в противном случае другие модули сломают систему.
  • Вы должны быть предельно осторожны при изменении открытого интерфейса подсистем. Существующие модули обязательно сломаются и должны быть адаптированы к изменениям.
  • дополнительный компилятор усложняет процесс отладки: вы больше не можете запускать XDebug для исходного кода, после любого изменения кода следует запускать компилятор (хотя это может быть смягчено, даже проблема XDebug)

Как можно использовать эту систему?

Пример будет таким:

 class Foo { public function bar() { echo 'baz'; } } 
 namespace Module1; /** * This is the modifier class and it is marked by DecoratorInterface */ class ModifiedFoo extends \Foo implements \DecoratorInterface { public function bar() { parent::bar(); echo ' modified'; } } 
 // ... somewhere in the app code $object = new Foo(); $object->bar(); // will echo 'baz modified' 

Как это может быть возможно?

Достижение этого потребовало бы некоторой магии. Мы должны предварительно обработать этот код и скомпилировать некоторые промежуточные классы с обратным графом наследования, чтобы исходный класс расширил декоратор модуля следующим образом:

 // empty copy of the original class, which will be used to instantiate new objects class Foo extends \Module1\ModifiedFoo { // move the implementation from here to FooOriginal } 
 namespace Module1; // Here we make a special class to extend the other class with the original code abstract class ModifiedFoo extends \FooOriginal implements \DecoratorInterface { public function bar() { parent::bar(); echo ' modified'; } } 
 // new parent class with the original code, every inheritance chain would start from such file class FooOriginal { public function bar() { echo 'baz'; } } 

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

По сути, компилятор должен взять список всех классов системы и для каждого отдельного класса без декоратора найти все дочерние DecoratorInterface которые реализуют DecoratorInterface . Он создаст граф декоратора, удостоверится, что он является ациклическим, отсортирует декораторы в соответствии с алгоритмом приоритета (подробнее об этом позже) и создаст промежуточные классы, где цепочка наследования будет обращена вспять. Исходный код будет извлечен в новый класс, который теперь является родительским для цепочки наследования.

Звучит довольно сложно, да?

К сожалению, это действительно сложно, но такая система позволяет гибко комбинировать модули, и модули смогут модифицировать буквально любую часть системы.

Что если существует несколько модулей для модификации одного класса?

В тех случаях, когда должно действовать несколько классов декораторов, мы можем поместить их в результирующую цепочку наследования в соответствии с их приоритетом. Приоритет можно настроить с помощью аннотаций. Я настоятельно рекомендую использовать аннотации Doctrine или некоторые файлы конфигурации. Посмотрите на этот пример:

 class Foo { public function bar() { echo 'baz'; } } 
 namespace Module1; class Foo extends \Foo implements \DecoratorInterface { public function bar() { parent::bar(); echo ' modified'; } } 
 namespace Module2; /** * @Decorator\After("Module1") */ class Foo extends \Foo implements \DecoratorInterface { public function bar() { parent::bar(); echo ' twice'; } } 
 // ... somewhere in the app code $object = new Foo(); $object->bar(); // will echo 'baz modified twice' 

Здесь, Decorator\After annotation можно использовать для размещения другого модуля-декоратора дальше по цепочке наследования. Компилятор будет анализировать файлы, искать аннотации и создавать промежуточные классы для получения этой цепочки наследования:

Составленная диаграмма классов с аннотацией

Кроме того, вы можете реализовать Decorator\Before (чтобы повысить класс декоратора), Decorator\Depend (чтобы включить класс декоратора только в том случае, если другой модуль присутствует или отсутствует). Такое подмножество аннотаций было бы в значительной степени полным, чтобы составить любую требуемую комбинацию модулей и классов.

Но как насчет хуков или исправления кода? Это лучше?

Как и у декораторов , каждый из подходов имеет свои преимущества и недостатки.

Например, хуки (основанные на каком-то шаблоне Observer ) широко используются в WordPress и многих других приложениях. Их преимущества заключаются в наличии четко определенного API-интерфейса расширения и прозрачного способа регистрации наблюдателя. В то же время у них есть проблема ограниченного числа точек расширения и неопределенного времени выполнения (сложно зависеть от результата других хуков).

Исправление кода тривиально для начала, но часто считается очень опасным, так как это может привести к неразборчивому коду и часто очень трудно объединить несколько исправлений файла или отменить изменения. Для исправления системы может потребоваться собственный DSL для управления изменениями. Сложные модификации потребуют глубоких знаний о системе.

Вывод

Шаблон Forwarding Decorators — это, по крайней мере, интересный подход, который можно использовать для решения проблемы достижения модульной расширяемой архитектуры программного обеспечения PHP при использовании знакомых языковых конструкций, таких как наследование или область выполнения, для управления расширяемостью.

Некоторые приложения уже включают описанную концепцию, в частности, OXID eShop использует нечто очень похожее. Чтение их документации разработчиков довольно забавно, у этих людей есть чувство юмора! Другая платформа, программное обеспечение X-Cart 5 для электронной коммерции, использует концепцию именно в описанной выше форме — ее код был взят за основу для статьи. В X-Cart 5 есть сторонние расширения, которые изменяют поведение системы, но не нарушают возможности обновления ядра.

Такая концепция может быть трудна для реализации, и есть некоторые проблемы с отладкой приложения, но их невозможно преодолеть, если вы потратите некоторое время на настройку компилятора. В следующей статье мы рассмотрим, как создать оптимальный компилятор и автозагрузчик и использовать фильтры PHP Stream, чтобы включить пошаговую отладку с помощью XDebug для исходного кода. Будьте на связи!

Если у вас есть какие-либо другие советы и рекомендации, которыми вы хотели бы поделиться — сообщите нам об этом в разделе комментариев ниже. Также любые вопросы приветствуются.