*Поломанный кран в офисе Magento и быстрое решение воплощенное в жизнь одним из инженеров — типичный Backward Compatible фикс.
Почему обратная совместимость важна
Разрабатывая фреймворк, после выпуска нескольких его версий, вы сталкиваетесь с тем, что пользователь (или в случае Magento — заказчик), который его купил/скачал/установил, а также программист, который разрабатывает собственные расширения и модули на основе фреймворка хотят чтобы апгрейд между версиями был максимально легким.
Для заказчика/пользователя процесс должен быть экономически эффективным. Затраты, потраченные на переезд на новую версию продукта, в которые входят: сам апгрейд, автоматизированное тестирование после апгрейда, регрессионное тестирование, исправление багов, которые привнесла новая версия платформы, обновление кастомизаций и сторонних модулей; должны быть минимальными.
Программист, разрабатывающий свое расширение под платформу, хочет чтобы его модуль был как можно дольше forward-compatible, и чтобы он знал заранее в каком релизе код его модуля будет поломан изменениями в системе, чтобы он мог заранее подготовить версию своего модуля совместимую с новым релизом.
В итоге разработчик фреймворка приходит к неожиданной дилемме: чем больше изменений нужно сделать (включая багфиксы), тем больше вероятность того, что будут внесены обратно несовместимые изменения, которые кого-то поломают.
Политика обратной совместимости для кода
Семантическое версионирование (SemVer) дает ответ на дилемму, помогая не отказываться ни от одной из опций (частые backward compatible релизы).
Номера версий компонентов фреймворка указываются в виде MAJOR.MINOR.PATCH, где обновление:
- MAJOR — говорит о несовместимых изменениях в API
- MINOR — говорит о том, что была добавлена обратно совместимая функциональность
- PATCH — говорит об обратно совместимом фиксе бага
Политика обратной совместимости применяется к сущностям, которые отмечены аннотацией @api в код базе.
Концепт публичного и приватного кода
Разработчики имеющие опыт работы с C++ или Java хорошо знакомы с этим концептом. Когда на выходе программа поставляется в виде .h (header) файла, содержащим описания внешних контрактов, семантику методов которых легко прочитать любым текстовым редактором и DLL файла, содержащий собранный и скомпилированный код, который тяжело прочитать и нельзя изменить.
PHP на уровне языка не предоставляет такую возможность. Поэтому «правильный» подход фреймворки, написанные на этом языке, искали давно. Например, Magento 1, как и многие другие фреймворки того времени (Symfony 1) использовали Inheritance Based API, когда фреймворк для кастомизации и расширения своих компонентов, предлагал отнаследоваться от любого из своих классов, и переопределить или расширить поведение в классе наследнике. Соответственно в Magento 1 приватные свойства и методы не использовались вообще, а Magento core разработчики обязаны были следить за двумя контрактами (Public — контракт который формируют публичные свойства, методы и константы сущностей; Protected — контракт наследования сущностей) и предотвращать добавление обратно несовместимых изменений в оба. Когда все сущности и все методы этих сущностей в код базе являются API, то добавление новых изменений и фикс багов в любом случае может кого-то поломать.
В Magento 2 в связи с этим решили следовать другому подходу — Inheritance Based API запретили для внутреннего использования, и не рекомендуют использование такого подхода для кастомизации классов фреймворка или модулей сторонними разработчиками. Запретили использование Protected модификатор доступа для атрибутов и методов классов. Основная идея в этих изменениях заключается в том, что имея только Public и Private — нам нужно следить только за одним контрактом — публичным. У нас нет контракта-наследования.
Следующим шагом было разделение кода на Публичный (аналог header файлов) — код и конструкции отмеченные аннотацией @api
и Приватный (аналог скомпилированного DLL) — код, не отмеченный аннотацией, говорящей, что это API.
Закрытый код не предполагается к использованию сторонними разработчиками. Таким образом его изменения приведут только к увеличению PATCH версии компонента, где этот код изменялся.
Изменения в Публичном коде всегда увеличивают MINOR или MAJOR версию компонента.
Мы обещаем быть обратно совместимыми для классов отмеченных @api
внутри MINOR и PATCH релизов компонентов. В случае когда нам нужно внести изменения в класс/метод отмеченный как @api
, мы отмечаем его как @deprecated и он будет удален не раньше следующего MAJOR релиза компонента.
Примеры того, что попадает под определение Публичного кода в Magento 2
- PHP интерфейсы отмеченные
@api
- PHP классы отмеченные
@api
- JavaScript интерфейсы отмеченные
@api
- JavaScript классы отмеченные
@api
- Virtual Type отмеченные
@api
- URL paths
- Консольные команды и их аргументы
- Less Variables & Mixins
- Топики очереди сообщений AMQP и их типы данных
- Декларация UI компонентов
- Layout декларация модулей
- События, которые тригерятся модулями
- Схема конфигурации, добавляемая модулями
- Структура системной конфигурации
API vs SPI (Точки Расширения)
PHP контракты в Magento могут быть использованы по-разному. Таких способов использования 3:
- API использование: методы интерфейса вызываются в PHP коде
- Service Provider Interface (SPI) использование: интерфейс может быть реализован, позволяя новой реализации расширять текущее поведение платформы
- API и SPI одновременно
Мы рассчитываем, что все вызовы к модулю будут проходить через API (сервис контракты модуля), также модули предоставляют интерфейсы, реализуя которые внешние разработчики и кастомизаторы могут предоставить альтернативную реализацию или расширить реализацию из коробки. Например, для функционала Search (поиска) есть контракт агностичный к адаптеру, который выполняет запрос. И предполагается, что этот контракт будет использоваться как API (просто вызываться в коде бизнес логики). В то время как предоставляется набор интерфейсов, для реализации поисковых адаптеров, предоставляя свои имплементации сторонний разработчик может добавить поддержку нового поискового мезханизма, например Sphinx. И это не должно поломать бизнес логику, которая использует Search API.
API и SPI не являются взаимоисключающими, поэтому мы не разделяем их отдельно в коде. SPI имеют такую же аннотацию как и API — @api
.
Но кто же тогда определяет, что есть API, а что есть SPI? — Те, кто их использует, т.е. внешние разработчики
Правила указания зависимостей
Для начала — почему модули должны по-разному зависеть друг на друга в зависимости от того как они используют другой модуль.
Представьте, что у вас есть интерфейс репозитория категории продуктов
с методами:
- get
- save
- delete
Интерфейс уже был опубликован в предыдущем релизе системы и его используют.
В какой-то момент вы понимаете, что забыли добавить в этот интерфейс метод getList для поиска категорий по указанным поисковым критериям. Если вы добавите этот метод в текущий интерфейс (и реализацию в класс, который его имплементирует) поломает ли это код, который его использует?
Если код использует интерфейс как API, т.е. просто вызывает методы get/save/delete в бизнес логике — появление нового метода не принесет проблем. Существующий код продолжит работать. Если же код модуля предоставляет альтернативную реализацию для этого интерфейса (SPI), то мы получим ошибку в процессе сборки, так как класс имплементирующий интерфейс не предоставляет реализацию для одного из его методов.
Так у нас появился отдельный сервис для поиска категорий.
В случае с удалением метода из интерфейса — обратная история, для SPI использование это не ломающие изменения, для API — это проблема.
API
Если модуль использует (вызывает) контракты, задекларированные другим модулем Magento, он должен зависеть на MAJOR версию этого модуля. И система предоставляет обратную совместимость в рамках всего мажорного релиза.
{
...
"require": {
"magento/module-customer": "~100.0", // (>=100.0 <101.0.0)
},
...
}
SPI (Точки расширения)
Если модуль предлагает свою реализацию для контрактов задекларированных другим Magento модулем он должен зависеть на MAJOR+MINOR версию этого модуля. И система предоставляет обратную совместимость в рамках минорного релиза.
{
...
"require": {
"magento/module-customer": "~100.0.0", // (>=100.0.0 <100.1.0)
},
...
}
Зависимость на приватный код
Если же модуль зависит на сущность не отмеченную как @api
, тогда модуль должен зависеть на MAJOR+MINOR+PATCH версию. И уже апгрейд на следующий патч релиз может обернуться проблемами для данного модуля.
{
...
"require": {
"magento/module-customer": "100.0.0", // (==100.0.0 <100.0.1)
},
...
}
*Данная статья является частью 1; Часть 2, которая выйдет вскоре опишет ограничения в коде, которые привносятся политикой обратной совместимости и что мы делаем, чтобы не останавливать рефакторинг следуюя BC политике и не аккумулировать технический долг из-за этих ограничений
Автор: maghamed