СоХабр закрыт.

С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.

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

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 политике и не аккумулировать технический долг из-за этих ограничений

комментарии (17)

+1
saggid ,  

Спасибо за статью, полезная информация)

0
dab512 ,  
На картинке типичный костыль.
0
maghamed ,  
в этом и была идея — показать костыль, кстати он и выглядит буквально как костыль. Ввиду больших ограничений, которые привносит обратная совместимость иногда сделать «фикс», качество кода которого будет высокое, и мы не будем наращивать технический долг — очень теяжело. Во второй части стать будут конкретные примеры с кодом и как они должны решаться. И будет эволюция ремонта этого крана, так как в офисе у нас было несколько попыток его починить
0
Zolg ,  
какую обратную совместимость должен обеспечивать кран в офисе, что вместо замены на новый его чинят подручными средствами в несколько итераций ?!?!
0
maghamed ,  
не воспринимайте слишком буквально, это аллегория. Но если вам интересно.
— первый вариант починки дело рук программистов
— второй дело рук водопроводчика, который пытался починить кран
— третий — водопроводчик поменял кран на новый, так как не смог нормально починить его на второй итерации
–2
errogaht ,  
не понял, ведь magento Это просто CMS причём тут фреймворк? не слышал о таком фреймворке
0
maghamed ,  
Magento 2 это два фреймворка если быть точным:
— компонентный фреймворк общего использования
— e-commerce фремворк, который построент на нашем фреймворке (пункут 1).

Просто CMS Magento назвать нельзя, не смотря на то, что CMS в самой мадженто есть
0
maghamed ,  
Я отвечу на комментарий, который юзер donvictorio оставил, а потом удалил:
именно поэтому 2.0 и 2.1 ветки несовместимы друг с другом и разрабатываются параллельно?

Релизы 2.0 и 2.1 являются коммерческими релизами платформы, а не релизами компонентов. Они вполне могут быть несовместимы между собой. Хотя между 2.0 и 2.1 несовместимости были минимальны.
Здесь об этом можно почитать подробней

Такая основная идея связи между версиями платформы и версиями компонентов
image

И когда 2.1 релизилась Magento публиковала список обратно несовметимых измениний для 2.1
0
donvictorio ,  
я не удалял, его отклонили…
0
maghamed ,  
тогда старанно, у меня он просто пропал, когда я начал писать ответ.
0
Frank ,  
Двоякое впечатление от статьи. Звучит всё красиво, вот только разрабатывать модули под М2 стало намного проблематичнее. Буду рад, если ошибаюсь.

Практика показывает, что без зависимости от приватного кода сделать можно далеко не всё. Особенно это касается JS части. А значит прощай, обратная совместимость для многих модулей. В этом случае, я так понимаю, единственный выбор — либо держать несколько веток для модуля, либо забивать на старые версии Magento. Первый вариант, мягко говоря, не очень удобен, а второй нереален, если вспомнить, как разработчики в одном из обновлений поломали всю мультисайтовость и потом долго тянули с переносом bugfix из develop ветки. При этом, в М1 с версии 1.4 на моей памяти никогда не было такого, что функционал разительно менялся в рамках одной и той же мажорной версии. Бывало, что закрывали какой-то спорный функционал, но ты всегда мог выпустить новую версию модуля, которая бы работала и на старых версиях тоже. В М2 — не уверен.

Поэтому, допустим, мы всё таки будем держать несколько веток модуля и при каждом багфиксе обновлять их все. Как организовать удобную доставку модуля клиенту исходя из его версии Magento? Казалось бы, ответ очевиден — Magento Marketplace. Нам надо только расписать какие версии модуля подходят для каких версий М2 и Marketplace/composer сам решит, что отдать клиенту. Но в Marketplace ещё надо попасть. А это, как оказалось, в отличие от Magento Connect, совсем нетривиальная задача, решение которой от разработчика вообще мало зависит. Выложить модуль — не меньше месяца ожидания. Выложить обновление модуля — не меньше. Не хочешь использовать Marketplace, ставь себе Satis или какой-нибудь другой аналог packagist для приватных модулей — может, и не суперсложно, но это ещё одна вещь, в которой нужно разобраться.

В результате получается, что по сравнению с разработкой модулей для М1, разработка для М2, которая и так занимает значительно больше времени, обрастает ещё и дополнительными накладными временными расходами, которые далеко не каждый разработчик может себе позволить.
0
maghamed ,  
В текущих версиях 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
0
isxam ,  
Мы анализируем какие зависимости между модулями Magento использует внутри себя. А также мы анализируем какие зависимости используются экстеншенами на Marketplace (non api dependency). И если зависимости валидны, т.е. такой результат нельзя получить используя текущие API модуля — мы помечаем сущность как api

как устроен этот процесс? я так понимаю зависимости вы собираете в автоматическом режиме, а что с определением валидности?

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

Как вы определяете (на основе данных с маркетплейса) сущности, которые необходимо пометить тегом api?
0
maghamed ,  
да, зависимости на сущности не отмеченные @api собрали в автоматическом режиме.

После чего списки зависимостей были переданы командам, которые отвечают за конкретные модули.
И команды вместе с командным архитектором проходятся по этому списку в мануальном режиме (анализирую код и зависимости) отмечая какие зависимости являются валидными (т.е. Magento не предоставляет альтернативных API) и для них добавляются @api
аннотации, а какие — нет (для которых API аннотация добавлена не будет)
0
maghamed ,  
Теперь по поводу разработки.
Фактически каждая коммерческая версия Magento — 2.0.*, 2.1.*, 2.2.* это отдельный продукт.
Поэтому имеет смысл иметь отдельные ветки и версии вашего экстеншена под каждую из мажорных коммерческих веток Magento.
+1
Frank ,  
Про мажорные версии я понимаю.

До релиза 2.2. мы считаем весь код — публичным, т.е. на все классы распространяется политика обратной совместимости. Не только на классы, помеченные api. Разделение концепта на публичный и приватный начнется с 2.2 релиза.


Просто до этой фразы мне из статьи не было понятно, что это только начиная с 2.2. И что если модуль зависит от приватного кода, то ветки придётся держать не только по мажорным версиям, но и по патч версиям, которые меняют то, от чего модуль зависит. Но если это только с 2.2 и если там действительно можно будет обойтись без зависимостей от приватного кода, то отлично.
0
maghamed ,  
Более того, мы введем концепт публичного/приватного кода с 2.2 и соответсвенно обратно несовместимые изменения сможем вводить со следующим мажорным релизом, т.е. с 2.3

Мы стараемся никого не поломать нашим ближайшим релизом