Pull to refresh

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

PHP *System Analysis and Design *Designing and refactoring *Development for e-commerce *Magento *
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)
    },
    ...
}


Ближайший мажорный коммерческий релиз 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 политике и не аккумулировать технический долг из-за этих ограничений
Tags:
Hubs:
Total votes 12: ↑10 and ↓2 +8
Views 8K
Comments Comments 17