Политика обратной совместимости при разработке фреймворка на примере Magento 2. Часть 1

в 9:25, , рубрики: backward compatibility, Magento, magento 2, php, semantic versioning, Анализ и проектирование систем, версионирование, обратная совместимость, Проектирование и рефакторинг, Разработка под e-commerce, рефакторинг

image
*Поломанный кран в офисе Magento и быстрое решение воплощенное в жизнь одним из инженеров — типичный Backward Compatible фикс.

Почему обратная совместимость важна

Разрабатывая фреймворк, после выпуска нескольких его версий, вы сталкиваетесь с тем, что пользователь (или в случае Magento — заказчик), который его купил/скачал/установил, а также программист, который разрабатывает собственные расширения и модули на основе фреймворка хотят чтобы апгрейд между версиями был максимально легким.

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

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

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

Политика обратной совместимости для кода

image

Семантическое версионирование (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

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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js