我如何将随机与 CQRS+Event Sourcing 一起使用

How I can use random with CQRS+Event Sourcing

我写我的项目,就是论坛的游戏Mafia。我使用 CQRS 事件溯源 + MongoDB。当游戏开始时,游戏需要给每个玩家一个随机的角色。我如何实现它,如果聚合根将应用事件,例如 "roles given",来自数据库(不是事件,现在已保存),总是会调用随机函数,这将是 return 不同的结果?

通常你会有一个命令来触发一些域行为(即分配随机角色),然后角色将保存在数据库中的一个事件中,即RoleAssigned。当玩家下次通过重播事件继续游戏时,这将保留角色。您不会在处理事件的代码中分配随机角色,它会在命令处理程序中完成,不会重播。

public void Handle<StartGameCommand>()
{
     var player = someEventSourcedPlayerRepository.GetOrCreateOrWhatever();
     player.assignRole(); // randomly assigns the role and creates event RoleAssigned
     someEventSourcedPlayerRepository.Save(); // saves events to db
}

在上面的代码中,玩家将拥有一个包含他们角色的事件。当您下次加载玩家时,他们的角色将从该事件中获取。您不要再调用 player.assignRole。

How I can realize it, if when aggregate root will be applied event ,for example, "roles given", from DB (not event, which has been saved now ) , always will be call random function, which will be return different result?

创建事件和应用事件是不同的代码路径。应用事件时不需要"thinking",因为聚合在创建事件时已经进行了思考。您在申请中需要做的就是忠实地更新您的状态。

也就是说,在应用事件时执行规则不是聚合的工作。

所以你的历史看起来像

GameStarted()
RoleAssigned(Alice, Innocent)
RoleAssigned(Bob, Innocent)
RoleAssigned(Charlie, Mafia)
...

请注意,没有什么可以随意做的;您可以重新调整游戏状态,而不必担心这些事件最初是否描述了随机分配 - 因为分配发生在过去,分配已完全确定现在

当聚合做出决定时,分配的随机位属于写入路径

void assignRole(Player p) {
    Role role = getRandomRole();
    this.apply( RoleAssigned(p, role) );
}

跟进

How should I change my code?

class GameEventHandler { 
    public void Handle(GameStartedEvent message) { 
        var randomRoles =_randomizer.GetRandomRoles(); 

        foreach(user in userAggregates) { 
            user.RoleAssign(new AssignRoleCommand(randomRoles[i] ));
        } 
    } 
}

事件处理程序中的决策代码是 "code smell";这是某个地方出了问题的线索。当聚合处理命令时,应在模型中做出选择。

对于像 Mafia 这样的游戏,您希望在其中实现角色的正确平衡,并且需要担心在分配角色时使用的变体规则,我希望 Game 聚合拥有角色分配决定。因此,随机分配将发生在 StartGameCommand 处理程序中,并且可能看起来像这样

class Game {
    public void startGame () {
        this.apply(GameStarted());

        var randomRoles =_randomizer.GetRandomRoles(); 

        foreach(userId in this.players) {
            this.apply(RoleAssigned(userId, randomRoles[i]);
        }
    }
}

我们之前看到的事件列表都在这里,作为 运行 单个命令的结果写入(这意味着所有事件都在单个事务中写入您的事件存储)。

另一方面,如果你真的需要每个用户聚合负责分配其角色,那么这里的逻辑将被分解成这样

class Game {
    public void startGame () {
        this.apply(GameStarted(this.players));
    }
}

class User {
    public void assignRole () {
        this.apply(RoleAssigned(this.id, _randomizer.getRandomRole());
    }
}

并且您的事件处理程序将在不知道任何关于随机角色的情况下将两者联系起来。

class GameEventHandler { 
    public void Handle(GameStartedEvent message) { 
        foreach(userId in message.players) { 
            dispatch(AssignRoleCommand(userId));
        } 
    } 
}

事件处理程序就像管理者,他们什么都不做,只是将工作委托给其他人