TLDR; сохранить идентификатор SMS в самом событии.
Основным принципом источника событий является «идемпотентность». События являются идемпотентными, а это означает, что обработка их несколько раз будет иметь тот же результат, что и при однократной обработке. Команды являются «неидемпотентными», что означает, что повторное выполнение команды может иметь разные результаты для каждого выполнения.
Тот факт, что агрегаты идентифицируются по UUID (с очень низким процентом дублирования), означает, что клиент может генерировать UUID вновь созданных агрегатов. Менеджеры процессов (также известные как «саги») координируют действия между несколькими агрегатами, прослушивая события, чтобы отдавать команды, так что в этом смысле менеджер процессов также является « клиент». Поскольку диспетчер процессов выдает команды, его нельзя считать «идемпотентным».
Одним из решений, которое я придумал, является включение UUID SMS, которое скоро будет создано, в событие PasswordResetRequested
. Это позволяет диспетчеру процессов создавать SMS только в том случае, если он еще не существует, что обеспечивает идемпотентность.
Пример кода ниже (псевдокод C++):
// The event indicating a password reset was successfully requested.
class PasswordResetRequested : public Event {
public:
PasswordResetRequested(const Uuid& userUuid, const Uuid& smsUuid, const std::string& passwordResetCode);
const Uuid userUuid;
const Uuid smsUuid;
const std::string passwordResetCode;
};
// The user aggregate root.
class User {
public:
PasswordResetRequested requestPasswordReset() {
// Realistically, the password reset functionality would have it's own class
// with functionality like checking request timestamps, generationg of the random
// code, etc.
Uuid smsUuid = Uuid::random();
passwordResetCode_ = generateRandomString();
return PasswordResetRequested(userUuid_, smsUuid, passwordResetCode_);
}
private:
Uuid userUuid_;
string passwordResetCode_;
};
// The process manager (aka, "saga") for handling password resets.
class PasswordResetProcessManager {
public:
void on(const PasswordResetRequested& event) {
if (!smsRepository_.hasSms(event.smsUuid)) {
smsRepository_.queueSms(event.smsUuid, "Your password reset code is: " + event.passwordResetCode);
}
}
};
Есть несколько замечаний по поводу приведенного выше решения:
Во-первых, хотя существует (очень) низкая вероятность того, что SMS UUID могут конфликтовать, это действительно может произойти, что может вызвать несколько проблем.
Связь с внешней службой запрещена. Например, если пользователь «bob» запрашивает сброс пароля, который генерирует SMS UUID «1234», то (возможно, 2 года спустя) пользователь «frank» запрашивает сброс пароля, который генерирует тот же SMS UUID «1234», процесс менеджер не поставит SMS в очередь, потому что думает, что оно уже существует, поэтому Фрэнк его никогда не увидит.
Неверная отчетность в модели чтения. Поскольку существует повторяющийся UUID, сторона чтения может отображать SMS, отправленное «bob», когда «frank» просматривает список SMS, отправленных ему системой. Если повторяющиеся UUID были сгенерированы в быстрой последовательности, возможно, что «frank» сможет сбросить пароль «bob».
Во-вторых, перемещение генерации SMS UUID в событие означает, что вы должны сообщить агрегату User
о функциях PasswordResetProcessManager
(но не о самом PasswordResetManager
), что увеличивает связанность. Однако связь здесь слабая, поскольку User
не знает, как ставить SMS в очередь, а только то, что SMS должно ставиться в очередь. Если бы класс User
сам отправлял SMS, вы могли бы столкнуться с ситуацией, когда событие SmsQueued
сохраняется, а событие PasswordResetRequested
нет, что означает, что пользователь получит SMS, но сгенерированный код сброса пароля не будет сохранен на пользователя, поэтому ввод кода не приведет к сбросу пароля.
В-третьих, если событие PasswordResetRequested
генерируется, но система дает сбой до того, как PasswordResetProcessManager
сможет создать SMS, то SMS в конечном итоге будет отправлено, но только при повторном воспроизведении события PasswordResetRequested
(что может произойти через много времени в будущем). Например, «возможная» часть окончательной согласованности может быть далеко.
Приведенный выше подход работает (и я вижу, что он также должен работать в более сложных сценариях, таких как OrderProcessManager
, описанный здесь: https://msdn.microsoft.com/en-us/library/jj591569.aspx). Тем не менее, я очень хочу услышать, что другие люди думают об этом подходе.
person
magnus
schedule
14.12.2015