Идеальные веб-приложения должны отображать последнее состояние видимой информации. Это ограничение требует, чтобы клиентские приложения знали, что изменяется после операции пользователя; таким образом, любые возможные устаревшие данные могут быть обновлены. Тот факт, что клиент должен знать, что сервер обновляет при каждой бизнес-операции, представляет собой форму согласия между ними.

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

Проблема синхронизации

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

  1. Пользователь запрашивает загрузку страницы или раздела пользовательского интерфейса.
  2. Веб-служба опрашивает несколько конечных точек, чтобы получить информацию, которую должен видеть пользователь.
  3. Ответы от веб-службы хранятся локально в SPA. Это локальное хранилище может находиться вне компонентов пользовательского интерфейса, как показано на рис. 1, или внутри них. Независимо от подхода, у вас всегда будет локальное состояние в веб-приложении.
  4. Компоненты пользовательского интерфейса считывают локальное состояние и на его основе составляют различные разделы пользовательского интерфейса с информацией.
  5. Консолидированная страница создается и отображается пользователю.

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

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

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

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

async cancelFlight(flightNumber) {
  await externalService.cancelFight(flightNumber)
  refreshFlightStatus()
  refreshStatusHistory()
  refreshOperationCosts()
  refreshAnyOtherInfoAffected()
}

Таким образом, в общем случае существует отношение «многие ко многим» между операциями и ресурсами, поступающими от службы. Это отношение вытекает из определения бизнес-процесса, который выполняет операция.

Главное в этом то, что если UI-приложения хотят всегда правильно отображать актуальное состояние своей видимой информации, они должны закодировать это отношение в своем исходном коде, чтобы знать, что должно быть обновлено впоследствии. если есть бизнес-процесс, который запускается операцией O1, и такая операция обновляет ресурсы {R1, R2, R3}, а ее пользовательский интерфейс показывает информацию об этих трех ресурсах, их необходимо будет обновить после операции O1.

Таким образом, проблема возникает из-за того, что приложение пользовательского интерфейса должно знать об этом сопоставлении между операциями и ресурсами, на которые влияют такие операции. Как указывалось ранее, это отношение рождается из реализации бизнес-процесса, а поскольку бизнес-процессы реализованы на уровне сервиса, логично определить это отношение в сервисе, где живет вся бизнес-логика. Однако теперь приложениям пользовательского интерфейса необходимо знать это бизнес-сопоставление, чтобы их интерфейсы оставались актуальными после операции. Результатом этого является соединение или форма Connascence между двумя далекими друг от друга кодовыми базами: сервисом и приложением пользовательского интерфейса.

Как и в случае любой формы связи, это влияет на гибкость системы программного обеспечения для поддержки новых изменений. Если изначально система содержит операцию O1, которая обновляет ресурсы {R1, R2, R3}, но позже требует, чтобы система обновлялась так, как O1 обновляет также R4; недостаточно обновить бизнес-уровень в службе, чтобы поддержать это изменение. Также необходимо обновить приложение пользовательского интерфейса, чтобы обновить R4 после операции O1. Если это не сделать, то получится типичная ошибка: «После операции O1 раздел R4 не обновляется, а только после того, как пользователь вручную обновит страницу».

Еще хуже может случиться, что операция больше не изменяет ресурс, который был ранее обновлен. Например, следуя нашему последнему примеру, предположим, что систему необходимо обновить, например, O1 теперь обновляет ресурсы {R1, R3}. Обратите внимание, что в список обновлений O1 не добавлен новый ресурс, поэтому в приложении пользовательского интерфейса нет дополнительных обновлений. Однако R2 больше не обновляется операцией O1, поэтому было бы легко забыть об этом ненужном обновлении R2 в коде, в конце концов, это не приведет к ошибке. Представьте себе такие изменения в нынешних начинающих компаниях, которым необходимо ежедневно заново изобретать себя и, таким образом, постоянно обновлять свои бизнес-процессы. После долгой работы приложения пользовательского интерфейса не было бы необычным иметь кодовую базу с ненужным кодом обновления, разбросанным по всем файлам, на что разработчики обычно реагируют: «Почему это существует, если оно ничего не делает? ».

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

Для облегчения чтения последующей информации я буду использовать с этого момента термин «синхронная связь» для обозначения типа связи, описанного ранее.

Анализ связи глазами Connascence

В Интернете можно найти следующее определение Сознания:

  • «Connascence — это метрика качества программного обеспечения и таксономия для различных типов связи».
  • «В программной инженерии два компонента считаются сосуществующими, если изменение одного требует изменения другого для поддержания общей корректности системы».

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

При анализе Сознания, чтобы определить, как оно влияет на гибкость, полезно основывать анализ на трех его свойствах:

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

Эти три свойства вместе взятые вносят свой вклад в вес Сознания точно так же, как вектор получает свои величины в трехмерном пространстве.

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

Прочность на «синхронной» связи

Connascense предоставляет набор категорий силы связи. Не все формы связи производят одинаковые отрицательные эффекты в системе. Одни формы Сознания хуже других, поэтому уменьшение Силы Сознания означало бы преобразование сильных форм в слабые формы.

Если вы следовали моему предыдущему совету и читали о Connascense, то вы знаете описание этих типов Connascense. Основываясь на том, что я описал в предыдущем разделе, я отношу «синхронную» связь к категории осознания алгоритма. Реализация операции в сервисе означает реализацию алгоритма, который удовлетворяет потребности с точки зрения продукта. Изменение этого алгоритма будет означать потенциальное изменение информации, которую воспринимает пользователь, поэтому приложение пользовательского интерфейса должно знать об изменении.

как вы можете видеть на предыдущей диаграмме, понимание алгоритма не так уж и плохо. Его улучшение означало бы преобразование его в Connascence of Convention (вы также можете найти его под названием Connascence of Meaning), Type или Name.

Степень на «синхронной» связи

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

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

Локальность на «синхронном» соединении

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

Синхронная связь имеет высокую локальность. Связь существует между двумя отдельными приложениями, приложением пользовательского интерфейса и его службой. Как упоминалось ранее, наличие коннасценции с высокой локальностью не является большой проблемой, когда ее сила слаба. Хотя «синхронная» связь не имеет сильной формы совпадения, она также не имеет слабой формы; помните, мы определяем это как Сознание алгоритма. Следовательно, есть возможности для улучшения путем уменьшения силы этого высоколокального совпадения, чтобы сделать его «приемлемым» и, таким образом, уменьшить его сложность.

Уменьшение локальности «синхронной» связи

Что, если бы приложению пользовательского интерфейса не нужно было знать, что обновлять после операции? что, если вместо этого служба отвечает за сообщение пользовательскому интерфейсу, что было изменено после операции? Именно такой подход я хотел бы использовать. Перенося в сервис знания о том, «что обновлять после операции», мы уменьшаем локальность обсуждаемой муфты и тем самым уменьшаем ее сложность. Для этого сервису придется изменить способ ответа на запросы операций. Повторно используя наш предыдущий пример операции O1, которая обновляет ресурсы {R1, R2, R3}, потенциальным ответом может быть следующее:

REQUEST POST
URL: https://my-service.com/api/any/path/operation-O1
body: { ...payload }
RESPONSE
body: {
  "result": { ...operation-related-info },
  "side-effects": [
    https://my-service.com/api/any/path/R1-uuid,
    https://my-service.com/api/any/path/R2-uuid,
    https://my-service.com/api/any/path/R3-uuid,
    .
    .
    .
  ]
}

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

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

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

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

Соображения в службе

Новая информация, которая должна быть возвращена в ответе от Сервиса, важна только для UI-приложений. Как я объяснял ранее, это способ имитации реактивности, когда в службе запускается действие, изменяющее состояние. Это означает, что реализация и связанные с ней накладные расходы должны быть только для API, используемых в приложениях пользовательского интерфейса. Не реализуйте это на основном бизнес-уровне вашего приложения, иначе накладные расходы повлияют на любого другого клиента, которому это не нужно. Вместо этого реализация должна находиться в части уровня API вашего сервиса. Реализация может происходить как агрегированная служба, которая находится между клиентами и сервером, поскольку работает GraphQl.

Может ли коммуникация на основе событий решить проблему?

Асинхронная передача событий полезна в приложениях реального времени. Этот подход не относится к таким приложениям. Если другой пользователь обновляет текущую информацию, которую я вижу, я, возможно, не захочу, чтобы эти обновления выглядели плавно. Описанный нами тип проблемы связан с отображением фактического состояния системы после операции пользователя. Показать актуальное состояние системы в любой момент времени — совсем другая задача. Кроме того, связь, управляемая событиями, увеличивает сложность системы по сравнению с обычной схемой запроса/ответа. Не все системы требуют такой сложности.

Резюме

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

Позже мы используем Connascence, чтобы проанализировать связь и найти способ уменьшить ее ущерб для гибкости системы. Наш анализ показал, что мы можем уменьшить сложность «синхронной» связи, уменьшив ее локальность.

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

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