Несколько недель назад, еще до того, как мир остановился из-за Covid-19, я проводил некоторое обучение JavaScript и обнаружил, что мои ученики не знакомы с Fluent-интерфейсами. Я имею в виду, что все они написали какой-то jQuery, например $("button.continue").html("Next Step..."), но они никогда не осознавали, что этот стиль API был вообще вещью или как он работал под капотом.

Существует множество хорошо известных библиотек JavaScript, которые используют этот тип API (jQuery, Moment, Express или Squel, чтобы назвать несколько), и это обычный строительный блок для шаблона компоновщика, который все Java-программисты знают и любят.

Некоторые концепции

Согласно Википедии:

В разработке программного обеспечения свободный интерфейс - это объектно-ориентированный API, конструкция которого во многом основана на цепочке методов. Его цель - повысить удобочитаемость кода за счет создания предметно-ориентированного языка. Этот термин был придуман в 2005 году Эриком Эвансом и Мартином Фаулером.

Я не уверен, что Mr. Фаулер согласился бы с моим названием, но мне нравится думать об этих трех типах частей, когда я создаю Fluent API.

  • Стартовые методы. Первый звонок. Он возвращает объект с помощью цепных методов. Например, метод $ в jQuery.
  • Цепные методы. Методы, которые вы вызываете для предыдущего объекта, и в результате они возвращают объект (обычно this) с более последовательными методами.
  • Отделочные. Методы, останавливающие цепочку. Они могут возвращать любое окончательное значение. Вы не можете связать больше методов с этим значением.

Пример

Давайте запрограммируем простой Fluent API, чтобы определить REST API, работающие на Express.js.

Наш DSL определяет resource (стартовый метод) и для этого ресурса определяет, что делать с каждым действием CRUD (наши цепные методы) в терминах обработчиков Express.js. Наконец, мы можем listen для запросов (наш финишер).

Примерно так (для краткости мы ограничим пример частью CRUD, связанной с CR):

ad hoc реализация может выглядеть так:

  • Стартовый метод resource возвращает объект, который хранит каждый возможный обработчик в его собственном состоянии, и определяет методы цепочки и финишера.
  • Каждый объединяемый в цепочку метод (read, create) обновляет внутреннее состояние и возвращает ссылку на тот же объект (this), позволяя создавать новые связанные вызовы.
  • Финишер listen создаст приложение Express, установив все маршруты ресурсов и команды, используя сохраненные обработчики и запустив службу.

Обратите внимание, что это всего лишь одно из возможных решений. Вы можете создать Express app в resource и сохранить его в состоянии. Каждый объединяемый в цепочку метод будет определять обработчик маршрута непосредственно на app, и listen, ну, это просто заставит Express app слушать.

Также можно использовать замыкания и избегать использования устрашающего ключевого слова this (вот суть с альтернативной версией).

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

Извлечение рисунка "стартер-цепочка-финишер".

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

После этого мы можем провести рефакторинг нашего DSL:

Довольно элегантно, как цепочки и финишеры создаются с использованием одного и того же кода. В спецификации цепные элементы - это функции, которые возвращают undefined, в то время как финишеры возвращают какое-то значение. Я хотел бы поблагодарить автора кода, на котором основан этот фрагмент. Думаю, это был Реджинальд Брейтуэйт (в его книге JavasScript Allongé). Хотя не уверен 🤔. Если знаете, расскажите, пожалуйста, в комментариях.

Обратите внимание, что ваша спецификация DSL не может использовать стрелочные функции, потому что они не могут быть привязаны, а строка 4 dslify не будет работать должным образом.

Расширение API

Подумайте, как мы можем расширить наш предыдущий API для поддержки более чем одного ресурса.

Мы можем заставить наши цепочки возвращать разные объекты или сохранять более сложное состояние, чтобы создать иллюзию нахождения в области действия / изменения области действия:

В предыдущем примере каждый resource вызов «создаст» новую область.

Вложенные API-интерфейсы Fluent

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

Кроме того, необходимость в области видимости затрудняет обработку внутреннего состояния.

Но что, если мы используем результат плавной цепочки в качестве аргумента другого вызова, то есть, что если мы вложим плавные API-интерфейсы.

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

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

Новая реализация проста:

Замечание о неизменности

Все примеры, показанные в этой истории, изменяют внутреннее состояние объекта. Такая мутация может привести к нежелательным ошибкам. Взгляните на следующий код, который использует Moment.js для управления датами.

Каждый объединяемый в цепочку метод в Moment изменяет исходный объект, поэтому на строку 6 влияет операция из строки 5. Не то, что вы, вероятно, ожидали.

Изменяемые DSL заставляют пользователя определять всю цепочку сразу. Если вам нужно что-то более безопасное и удобное для рефакторинга, вам следует использовать неизменяемый DSL, где каждый метод в цепочке возвращает другой объект (возможно, копию).

Сравните предыдущий пример с этим, используя Luxon, который является неизменяемым:

В нашем примере с REST изменчивость не должна быть проблемой, но помните об этом. Я оставил читателю задачу улучшить этот пример, сделав DSL неизменяемым.

Подводя итоги

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

У Fluent API есть плюсы и минусы. Добавьте их в свой набор инструментов и используйте их с умом, только когда они подходят для задачи.