Минимально инвазивное управление версиями API

Одна из проблем при разработке API для мобильных приложений заключается в том, что не существует одновременно приятного и надежного способа заставить их обновиться. Это означает, что в некоторых случаях ваш API должен поддерживать приложения, которым несколько лет. Мобильный API для RetailMeNot не исключение.

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

Сначала я познакомлю вас с функцией, которая формирует часть нашего ответа.

Это простая функция, которая составляет основу «карточки предложения». Функция называется «OfferCard»; у него есть три аргумента - «изображение», «заголовок» и «описание» - и он возвращает карту JSON со своими аргументами, встроенными в нее в определенных местах. OfferCard используется повсеместно в нашем приложении, и многие атрибуты были добавлены и удалены с течением времени, чтобы поддерживать изменения в мобильных приложениях с течением времени.

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

У вас, вероятно, аллергия на скопированный код, и это правильно; скопированный код, хотя это и не всегда плохо, со временем может незначительно измениться и, как правило, просто создает еще один фрагмент кода, который необходимо поддерживать.

Альтернативой является то, что новая, вторая функция вызовет старую, а затем изменит ее возвращаемое значение для выполнения требований. Я назову это методом вызова и изменения.

У этого метода также есть некоторые недостатки. Один приятный аспект заключается в том, что модификации исходной функции OfferCard будут автоматически включены в функцию OfferCard-V2, поэтому у нас нет такой же проблемы, связанной с необходимостью поддерживать ту же функциональность в нескольких местах, как мы делали при копировании и - изменить подход. Единственный главный недостаток заключается в том, что мы создали довольно загадочную функцию: вам придется немного погоняться за дикими гусями, чтобы выяснить, что возвращает OfferCard-V2, и вы получите мысленно вычислить результат, выполняя в уме все «сопутствующие» (и любые другие) вызовы. Только представьте себе этот процесс для OfferCard-V10 или OfferCard-V20! Этот метод может поддерживать только определенное количество связанных вызовов, прежде чем он станет полностью неразборчивым.

Что нам действительно нужно, так это метод, который сочетал бы эти два подхода и взял бы лучшее из обоих. Как разработчики, мы хотим, чтобы в одном месте можно было посмотреть, как будет выглядеть версия OfferCard, но мы не хотим поддерживать несколько независимых фрагментов кода, которые практически То же самое, но с небольшими изменениями от версии к версии.

Используя мощную поддержку метапрограммирования Clojure, мы можем получить обе эти функции.

Было бы идеально, если бы мы могли просто аннотировать наш код «квалификаторами версии», которые указывали бы что-то вроде «этот фрагмент кода был добавлен в версии 2» или «этот фрагмент кода был удален в версии 3». После того, как мы разметили наш код, мы могли бы запустить над ним препроцессор, чтобы проделать скучную работу, необходимую для подхода «копирование и изменение» или «вызов и изменение».

Это потребовало бы от нас в основном модификации компилятора, чего не позволяют многие языки. Однако в Clojure, используя макросы, мы можем очень легко подключиться к процессу компиляции, чтобы реализовать наши квалификаторы версии! Вот пример аннотированной функции OfferCard, идеальный вариант для того, для чего мы стремимся:

Здесь происходит несколько вещей: мы преобразовали список аргументов (изображение, заголовок, описание и нижний колонтитул) в один аргумент карты. Это похоже на аргументы ключевого слова Python; он позволяет вызывающим абонентам дополнительно указывать каждый из четырех аргументов по имени, что полезно, когда они создают версию OfferCard, для которой не нужен один из ключей, например, версия 3 в этом примере, когда мы удалили поле описания.

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

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

Все это позволяет нам динамически изменять, какую версию OfferCard мы хотим, чтобы эта функция возвращала. Например, в версии 2 будет дополнительное поле «нижний колонтитул»; в версии 3 «описание» будет удалено.

«Но как выглядит сгенерированный исходный код?» То есть «что возвращает макрос с версией?» Получается примерно следующее:

Он просто компилируется в оператор switch! Макрос с поддержкой версий реализуется путем применения техники копирования и изменения к исходному коду и помещения каждой вычисленной версии в большой оператор switch. Квалификаторы версии - это просто инструкции, которые сообщают версионному включить или удалить фрагмент исходного кода при генерации одного из вариантов переключения. (Если вам интересно, запрошенная-версия - это динамическая переменная, для которой установлена ​​любая версия, указанная мобильным клиентом в заголовках запроса.)

В приведенных выше примерах я показал квалификаторы версии только на ключах карт, но мы также можем поместить их в векторы («[..]») или в списки («(…)»), и поскольку исходный код Clojure является просто вложенные списки, это означает, что мы можем аннотировать любой произвольный код.

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

Абстракция лежит в основе большей части того, что мы, программисты, делаем, и абсолютно необходимо с легкостью создавать все более и более сложные системы. Этот макрос демонстрирует мощь метапрограммирования с помощью Clojure, которое позволяет создавать синтаксические абстракции. В этом случае для решения этой проблемы требуются способы копирования и изменения или вызова и изменения. определенное количество шаблонов - то есть определенный объем синтаксиса - для реализации, но наш макрос позволяет нам инкапсулировать эти детали (во многих контекстах любовно называемые «шаблонами проектирования») в синтаксис, который предназначен для выражения этого намерения.

Этот относительно простой макрос сделал немало тяжелой работы, чтобы значительно уменьшить объем кода и сложность команды Mobile API здесь, в RetailMeNot. Что-то подобное было бы невозможно на многих других языках.