最近我看到 Udi Dahan 的一篇post,谈论聚合不应该凭空创建,而应该从其他聚合创建聚合,因为这样可以更好地捕获域。对我来说似乎有点合乎逻辑,但我似乎正在努力解决如何实现这一点并理解应该在哪里引发事件。
我正在创建一个排行榜应用程序。在我的域中,只有管理员可以创建排行榜。我正在尝试了解如何以干净的方式最好地捕获此规则,以便可以突出显示该域,同时确保尊重不变性。到目前为止,我已经决定管理员和排行榜将成为聚合根(如果您不这么认为,请告知)。
以下是我迄今为止在 Typescript 中得到的内容。
const User extends AggregateRoot { /*whatever*/}
class Adminstrator extends User {
// blah blah
createLeaderboard(name) {
return Leaderboard.Create(name);
}
}
class Leaderboard extends AggregateRoot {
public id;
public name;
private create(id: LeaderboardId, name: LeaderboardName) {
const leaderboardCreatedEvent = new LeaderboardCreatedEvent(id, name);
this.applyNewEvent(leaderboardCreatedEvent);
}
static Create(id: LeaderboardId, name: LeaderboardName): Leaderboard {
const params: ConstructorCreateParams = {
type: ConstructorType.Create,
id,
name
}
return new Leaderboard(params);
}
private constructor(params) {
if (params.type === ConstructorType.Create) {
this.create(params.id, params.name);
} else {
this.load(params.events);
}
}
}
class CreateLeaderboardCommandHandler {
constructor(adminstratorRepo, leaderboardRepo)
execute(command) {
const admin = this.adminRepo.get(command.userId);
const leaderboard = admin.createLeaderboard(command.name)
leaderboardRepo.save(leaderboard);
}
}
关于我整理的实施的几个问题:
非常感谢您的意见。
创作模式很奇怪。
哪里是强制执行只有管理员才能创建排行榜这一事实的最佳位置。我的方法好不好呢。它会在排行榜的构造函数中吗?
我相信要认识到的关键是你在这里面对的是一个分支;当你的数据模型看起来像这样时,你想要一种结果,当你的数据模型看起来像那样时,你想要一种不同的结果。
一旦您认识到存在一个分支,您就需要考虑您希望该分支在代码中的出现程度有多明显。这可能意味着有一个快乐的路径和一个隐式的异常路径,它可能意味着有两个回调,它可能意味着返回一个“或类型”,......
说明该想法的一种非稳健方法是想象管理存储库返回一个 promise
this.adminRepo.get(command.userId).then((admin) -> {
const leaderboard = admin.createLeaderboard(command.name)
leaderboardRepo.save(leaderboard);
}).catch( /* handler for the not-an-admin case */ )
另一种可能性是分支属于更上游的地方 - 我们运行一些协议来检查命令的来源是否经过授权,然后再将该信息传递给域模型进行处理。 (即使在这种情况下,您可能需要“冗余”检查,因为防御性编程?这是一路权衡。)
在上面的示例中,管理员聚合正在执行某些操作(即创建排行榜),但我们保留了 Leaderboard 事件(即 LeaderboardCreatedEvent)。这样好吗?这是反模式吗?
在一般情况下,这是值得怀疑的——在你的情况下可能没问题。
有两个候选问题:
最终会得到一个损坏的数据模型,因为您“遵循了实体建模域的规则”,而这些规则是在您无法在您的环境中重现的理想条件下开发的(没有丢失的消息,没有重复的消息) ,没有并发写入...)
拥有一个鲁棒的实现,但设计不令人满意,安全地更改它的成本也很高(因为模块边界选择不当,或者代码掩盖了真正发生的事情,或者因为同一核心指令有太多不同的变体。 ...)