![image](https://habrastorage.org/getpro/habr/post_images/fe3/80a/388/fe380a3885271f9f516e8e4e62599565.png)
*Поломанный кран в офисе Magento и быстрое решение воплощенное в жизнь одним из инженеров — типичный Backward Compatible фикс.
Почему обратная совместимость важна
Разрабатывая фреймворк, после выпуска нескольких его версий, вы сталкиваетесь с тем, что пользователь (или в случае Magento — заказчик), который его купил/скачал/установил, а также программист, который разрабатывает собственные расширения и модули на основе фреймворка хотят чтобы апгрейд между версиями был максимально легким.
Для заказчика/пользователя процесс должен быть экономически эффективным. Затраты, потраченные на переезд на новую версию продукта, в которые входят: сам апгрейд, автоматизированное тестирование после апгрейда, регрессионное тестирование, исправление багов, которые привнесла новая версия платформы, обновление кастомизаций и сторонних модулей; должны быть минимальными.
Программист, разрабатывающий свое расширение под платформу, хочет чтобы его модуль был как можно дольше forward-compatible, и чтобы он знал заранее в каком релизе код его модуля будет поломан изменениями в системе, чтобы он мог заранее подготовить версию своего модуля совместимую с новым релизом.
В итоге разработчик фреймворка приходит к неожиданной дилемме: чем больше изменений нужно сделать (включая багфиксы), тем больше вероятность того, что будут внесены обратно несовместимые изменения, которые кого-то поломают.
Политика обратной совместимости для кода
![image](https://habrastorage.org/files/3f7/a54/5aa/3f7a545aa93140099abaa57d4b0c2e75.jpg)
Семантическое версионирование (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 — это проблема.
![image](https://habrastorage.org/getpro/habr/post_images/b42/361/f9f/b42361f9fcecf74f0f3a8ffb78c5c8bc.png)
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)
},
...
}
Ближайший мажорный коммерческий релиз 2.2 — Чего ждать?
В текущих версиях Magento 2.0.x и 2.1.x нельзя обойтись без зависимостей на приватный код.
Потому что у нас недостаточное покрытие
@api
для этого. Некоторые модули не имеют сервис контрактов вообще (например, wishlist). Поэтому у сторонних разработчиков нет другого выхода кроме как зависимости на непомеченные api аннотацией классы.Разработка API — самый сложный процесс в проектировании и разработке программного обеспечения. И так как мы пока не знаем когда именно закончится работа по добавлению сервис контрактов во все модули Magento мы решили в релизе 2.2 отметить
@api
все сущности, которые необходимы для написания/кастомизации модулей на мадженто сторонними программистами.Т.е. в 2.2 мы отметим все «честные контракты», т.е. если функциональность, которую предоставляет модуль реализуется хелпером или ресурс моделью. И получить данную функциональность путем вызова API модуля нельзя, то мы пометим данный хелпер как
@api
.Для примера, у нас есть ProductInterface, который определяет набор операций над сущностью продукта. И есть продуктовая модель, которая имплементирует этот интерфейс. Так как сейчас продуктовая модель наследуется от абстрактной модели и соответственно имеет контракт абстрактной модели, то мы не можем сказать сейчас, что сторонний разработчик можем полагаться только на ProductInterface, и если кто-то предоставит свою реализацию этого интерфейса, то сможет легко подменить внутреннюю реализацию мадженто (если это не будет наследник продуктовой модели). Т.е. достаточно много кода в Magento использует метод get/setData который пришел из абстрактной модели. Поэтому в 2.2 мы пометим продуктовую модель как
@api
, не смотря на то, что у нас уже есть API в виде ProductInterface.До релиза 2.2. мы считаем весь код — публичным, т.е. на все классы распространяется политика обратной совместимости. Не только на классы, помеченные
@api
. Разделение концепта на публичный и приватный начнется с 2.2 релиза.Теперь как мы определяем, что используют, а что нет.
Мы анализируем какие зависимости между модулями Magento использует внутри себя. А также мы анализируем какие зависимости используются экстеншенами на Marketplace (non api dependency). И если зависимости валидны, т.е. такой результат нельзя получить используя текущие API модуля — мы помечаем сущность как
@api
*Данная статья является частью 1; Часть 2, которая выйдет вскоре опишет ограничения в коде, которые привносятся политикой обратной совместимости и что мы делаем, чтобы не останавливать рефакторинг следуюя BC политике и не аккумулировать технический долг из-за этих ограничений