Простая, знакомая стратегия создания более устойчивых интерфейсных приложений

За последние несколько лет работы над производственными приложениями на JavaScript я заметил тревожную, но распространенную закономерность: первоклассные модели не указываются явно. Под «первоклассными моделями» я подразумеваю основные сущности, которые охватывают большую часть данных вашего приложения. Например, в системе инвентаризации книжного магазина, вероятно, будут такие сущности, как Книги, Авторы и, возможно, Клиенты. Основные цели системы инвентаризации - отображать и позволять пользователю взаимодействовать с этими моделями. Несмотря на важность подобных моделей, я редко видел, чтобы они были явными во внешних приложениях. Код большинства приложений, над которыми я работал, выглядит примерно так:

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

Это кажется довольно простым, но обратите внимание, что в этой системе нет никаких предложений о структуре book. Из компонента мы можем сделать вывод, что в книге есть title, author и quantity, что немного полезно, но оставляет нам много потенциальных вопросов:

  • Какие еще свойства book, которые мы можем использовать в будущих изменениях или дополнениях? Есть ли свойство ISBN? Можем ли мы легко узнать только имя автора?
  • Какие свойства books на самом деле используются нашим приложением?
  • Какие места нам нужно обновить, если API изменяет структуру book?

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

Внесение изменений без явных моделей

Допустим, API хочет внести изменения, чтобы разделить единственную строку author на две строки: authorFirstName и authorLastName. Чтобы поддержать это изменение, нам нужно обновить наш BookPage шаблон следующим образом:

Мы обновили подписку с простого рендеринга book.author на объединение book.authorFirstName и book.authorLastName, соблюдая новый API и сохранив существующее поведение нашего приложения.

Легко, правда? Не так быстро! Где еще используется book.author? Поскольку это система инвентаризации книжного магазина, есть вероятность, что мы также используем author во множестве других шаблонов. Теперь нам нужно выследить и обновить каждое вхождение. Ой, это совсем не весело! В чем дело? Если API внесло только одно изменение, зачем нам нужно вносить столько изменений во всем приложении?

Основная причина проблемы - сцепление. Сначала это может показаться неочевидным, но способ структурирования приведенного выше кода (как и многие производственные JS-приложения, которые я видел), шаблон React тесно связан с ответом от серверного API. Изменить API серверной части сложно, не нарушив случайно шаблон интерфейса. Добавление или изменение шаблона внешнего интерфейса затруднено без консультации с контрактом API серверного интерфейса. Когда у вас есть явный интерфейс, такой как интерфейс прикладного программирования, последнее, что вам нужно, - это высокая степень связи. Назначение интерфейса - разделить две концепции.

Так почему, черт возьми, так много внешних приложений JavaScript так тесно связаны со своими внутренними API? Это потому, что JavaScript часто бывает слишком удобным (концепция, которую я почерпнул из прекрасного выступления Сэма Джонса). Когда конечная точка API возвращает JSON, ответ уже имеет формат, с которым приложение JavaScript может работать напрямую, поэтому очень легко быстро создать приложение, работающее от начала до конца. В этом привлекательность JavaScript: его удобство.

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

Явные модели спешат на помощь

Как и в случае с бэкэндом, я утверждаю, что нам нужно начать использовать более явные модели в наших приложениях на JavaScript. Приложения, написанные на зрелых серверных языках, таких как C #, никогда не столкнутся с вопросами, подобными тем, которые я поставил выше, потому что на этих языках все должно быть явным. Хотя я не говорю, что JavaScript должен когда-либо быть таким деспотичным или самоуверенным, мы все же можем использовать эти концепции там, где это уместно, потому что они отлично подходят для больших, долгоживущих приложений. JavaScript хорош тем, что дает нам гибкость в принятии решения, когда использовать эти методы и как лучше всего реализовать их для данного сценария.

Возвращаясь к нашему примеру с инвентарем, давайте посмотрим, как ситуация улучшится, добавив явную модель для наших книг:

В приведенном выше коде мы добавили очень явный класс Book с четкими свойствами. Затем мы «десериализуем» JSON, который получаем от API, в Book объектов. Сначала это может показаться слишком многословным, но теперь давайте применим изменение API, которое мы обсуждали ранее (разделение свойства author), к нашему новому коду:

В этом случае вместо обновления шаблона мы обновили класс Book, чтобы он принимал свойства authorFirstName и authorLastName, но сохранял их в одном свойстве author, сохранив исходный контракт с моделью книги в нашем приложении. Таким образом, наш шаблон BookPage и все остальные шаблоны в нашем приложении менять не нужно! API внес одно изменение, и мы смогли внести ровно одно изменение в наше приложение. Это в лучшем случае низкая связь. Красивый!

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

Создание явных моделей

Как я намекал ранее, JavaScript оставляет нам вопрос о том, насколько тяжело создавать эти модели. Вы фанат объектно-ориентированного программирования? Затем вы можете писать супертяжелые классы с геттерами и сеттерами и целыми девятью ярдами. Вы предпочитаете что-то более худощавое и подлое? Затем вы можете просто скопировать нужные вам свойства из ответа API в новые объекты. Приведенный выше пример находится где-то в середине этого спектра. На практике я лично ошибался в использовании бережливых моделей (особенно когда я экспериментирую с более функциональным стилем написания JavaScript). Например, взгляните на этот реальный код из одного из приложений LeagueSide, которое выполняет поиск лиг через конечную точку API:

Обратите внимание, как этот пример демонстрирует еще одну замечательную вещь о явном моделировании и «десериализации»: мы можем легко преобразовать свойства Ruby snake_case (например, zip_code) в предпочтительные для JavaScript camelCase (например, zipCode). Явность настолько сильно отделяет интерфейс от API, что нам больше не нужно, чтобы API был именно тем, чего хочет интерфейс; интерфейс может формировать и изменять ответ так, как он считает нужным!

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