Содержимое
Обсуждение FizzBuzz напомнило про старую задачку на моделирование классов и взаимодействия: как замоделировать ситуацию "человек пьет кофе из кружки". Очевидно, у нас будут классы "человек" и "кружка", и, например, уровень кофе в кружке. А вот про взаимодействие вопрос: где живет метод "пить"? Класс "человек" отправляет сообщение "пить(объем кофе)" классу "кружка" ( person->cup.drink(30) )? Или человек пьет, а кружка тогда что делает? ( person.drinkFrom(cup,30) ) Я не помню, у кого я видел обсуждение этой задачи. Но пока искал, нашел прекрасную статью Грегора Хопа (соавтора "Паттернов интеграции корпоративных приложений") с иллюстрацией синхронной и асинхронной интеграции на примере Starbucks. Если вы помните, заказ в Starbucks принимают синхронно: вы не можете в процессе заказа и оплаты отойти на минуточку, а потом вернуться. Заказ и оплата выполняются в одном синхронном взаимодействии, даже если растягивается по времени. А вот приготовление выполняется асинхронно: — разные напитки требуют разного времени приготовления — разное оборудование может быть использовано параллельно — приготовление может вставать в очередь, когда оборудование занято — заказы могут быть готовы не в той последовательности, в которой были сделаны Почему так сделано? Для увеличения пропускной способности. Если у вас один человек принимает оплату, а потом сам готовит кофе — вы получаете кофе быстрее (ваш заказ взят в работу сразу же), но за вами копится очередь, и часть людей может уйти, не дождавшись. Асинхронность позволяет легко организовать горизонтальное масштабирование — можно увеличить число барист. Тем не менее, транзакция закончена только когда клиент получил напиток. В нашей гибридной архитектуре — с синхронным и асинхронным взаимодействием — транзакция становится распределенной. Тут можно увидеть несколько паттернов: — Идентификатор корреляции (Correlation ID), чтобы сопоставить приготовленный напиток и заказ. В Starbucks в качестве этого идентификатора используют имя клиента. — Обработка исключений. Когда описывают "саги", всегда пишут про компенсирующие действия. Но Хоп пишет, что есть три варианта: 1. Списание. Если транзакция завершилась неудачей, мы просто ничего не делаем. А что уже сделали — выбрасываем (списываем). Возможно, механизм исправления ошибки будет стоить дороже. Иногда у нас просто нет другой возможности, а иногда это бизнес-решение: если клиент не забрал напиток, его через какое-то время нужно вылить. 2. Повторная попытка. Если в ходе транзакции возник технический сбой, а не нарушение бизнес-правила, можно попробовать повторить действие. Чтобы не заморачиваться, можно повторить все действия (но для этого у нас должны быть идемпотентные получатели, чтобы можно было безопасно повторить все запросы, начиная с первого). Если кофемашина сломалась, нужно повторить все действия на другой. 3. Компенсирующие действия — выполнение обратных операций. Если мы совсем не смогли приготовить кофе, придется вернуть деньги. — Альтернатива — двухфазный коммит, когда кассир выступает координатором, и принимает оплату только после приготовления напитка. Это сильно задерживает выполнение транзакции, но зато система никогда не находится в несогласованном состоянии. В реальном мире это, например, счета эскроу. Что ещё можно вспомнить? Если рассмотреть другие закусочные, то можно увидеть паттерн Claim Check — когда клиент получает не финальный (тяжелый) результат, а чек, с которым он приходит за заказом. Тогда идентификатор корреляции — номер заказа, который есть на чеке. И, раз мы используем гибридный подход, можем увидеть и паттерны синхронных взаимодействий: — если у кассира сломалась касса или клиент замешкался с оплатой, можно открыть вторую кассу и перенаправить ожидающих клиентов туда — это паттерн Circuit Breaker, размыкатель. Как вариант — перестает принимать заказы на определенный вид напитка, например, если сломался капучинатор. Все паттерны взаимодействия пришли из реального мира, в конечном счете.