При разработке высоконагруженных веб-приложений для лучшего масштабирования часто применяется такой принцип, как CQRS. Он гласит, что метод должен быть либо командой, выполняющей какое-то действие, либо запросом, возвращающим данные, но не одновременно и тем, и другим. Другими словами, вопрос к системе не должен менять ответ. Более формально, возвращать значение можно только чистым, не имеющим побочных эффектов методам.
Но для хорошего масштабирования разделения API на чтение/запись недостаточно. Нужно разделить и базы данных, с которыми это API работает. Тут нам на помощь приходит EventSourcing. Он предлагает нам хранить всем события системы в одной базе данных, назовем ее EventStore, а все остальные базы данных и таблицы строить уже на ее основе.
Сочетание CQRS и EventSourcing очень сильно развязывает нам руки в плане балансировки нагрузки внутри системы, количестве ее узлов, количестве вспомогательных баз данных, использовании кеширования и прочего, но одновременно усложняет логику работы приложения и привносит множество ограничений.
В этой статье мы рассмотрим один из нюансов проектирования клиентской части для такой системы — оптимистические обновления в UI.
Для фронтенда возьмем модные React и Redux. Кстати, Redux и EventSourcing — очень близкие по духу технологии.
Оптимистичные обновления пользовательского интерфейса и так непросто реализовать, а CQRS и EventSourcing еще сильнее усложняют задачу.
Как же это должно работать? Давайте разберемся пошагово.
-
Отправляем команду и, не дожидаясь ответа, диспатчим оптимистичный event в Redux Store. Оптимистичный event будет содержать ожидаемые результаты сервера. Также на этом шаге мы запоминаем текущее состояние данных, которые event будет менять.
-
Ждем результата отправки команды. Если команда не прошла, диспатчим event, откатывающий оптимистичное обновление, на основе данных, которые запомнили на первом шаге. Если все хорошо, то ничего не делаем.
- Ждем, когда на клиент из шины прилетит настоящий event. Когда это случилось, откатываем оптимистическое обновление и применяем настоящий event.
Как это будет выглядеть на практике:
| Успех | Провал |
|---|---|
![]() |
![]() |
![]() |
![]() |
Код оптимистического обновления опишем как Middleware к Redux Store:
const optimisticCalculateNextHashMiddleware = (store) => {
const tempHashes = {};
const api = createApi(store);
return next => action => {
switch (action.type) {
case SEND_COMMAND_UPDATE_HASH_REQUEST: {
const { aggregateId, hash } = action;
// Save the previous data
const { hashes } = store.getState()
const prevHash = hashes[aggregateId].hash;
tempHashes[aggregateId] = prevHash
// Dispatch an optimistic action
store.dispatch({
type: OPTIMISTIC_HASH_UPDATED,
aggregateId,
hash
});
// Send a command
api.sendCommandCalculateNextHash(aggregateId, hash)
.then(
() => store.dispatch({
type: SEND_COMMAND_UPDATE_HASH_SUCCESS,
aggregateId,
hash
})
)
.catch(
(err) => store.dispatch({
type: SEND_COMMAND_UPDATE_HASH_FAILURE,
aggregateId,
hash
})
);
break;
}
case SEND_COMMAND_UPDATE_HASH_FAILURE: {
const { aggregateId } = action;
const hash = tempHashes[aggregateId];
delete tempHashes[aggregateId];
store.dispatch({
type: OPTIMISTIC_ROLLBACK_HASH_UPDATED,
aggregateId,
hash
});
break;
}
case HASH_UPDATED: {
const { aggregateId } = action;
const hash = tempHashes[aggregateId];
delete tempHashes[aggregateId];
store.dispatch({
type: OPTIMISTIC_ROLLBACK_HASH_UPDATED,
aggregateId,
hash
});
break;
}
}
next(action);
}
}
Вживую, как всё работает, можно посмотреть тут:
Оптимистичные обновления в UI могут сильно улучшить отзывчивость вашего приложения. Хотя использовать их нужно с умом и большой осторожностью. В ряде случаев они могу привести к потере данных и усложнить понимание пользовательского интерфейса. Например, оптимистичный лайк под фотографией это хорошо, а оптимистичная форма оплаты — плохо. Так что не наломайте дров. Удачи!





Комментариев нет:
Отправить комментарий