最新评论
水言木 2012-04-05 18:16
[quote]Leder:非常感谢,对CQRS理解又多了一些~[/quote]
互相讨论,互相学习 :)
水言木 2012-04-05 16:36
[quote]Leder:
Session["CurrentUserId"] = ???
这部分是查出来的,不是通过Command返回,呵呵,所以在设计Command的时候确实是没不必要加复杂的返回值。
CQRS模式是把命令查询职责分开,命令部分只是完成增删改之类的操作,通过储存命令并通过事件模式异步处理真正的数据库。通过查询(链接真正的数据库或快照)查询处理后的结果。
如果理解错了望指正。[/quote]
Command最理想状态是不返回值,像文中的例子,如果需求如此,那就不得以要让他返回一些东西,这时候就只能尽量让返回的东西越简单越好。Command返回执行结果对整个架构的主要影响是:Command将不能异步执行。
水言木 2012-04-05 16:33
[quote]Leder:
Session["CurrentUserId"] = ???
这部分是查出来的,不是通过Command返回,呵呵,所以在设计Command的时候确实是没不必要加复杂的返回值。
CQRS模式是把命令查询职责分开,命令部分只是完成增删改之类的操作,通过储存命令并通过事件模式异步处理真正的数据库。通过查询(链接真正的数据库或快照)查询处理后的结果。
如果理解错了望指正。[/quote]
命令不只是完成增删改,其实也可以认为命令是最终客户可理解的一个操作,比如修改密码,发布产品,修改价格,等等。这些名词都是客户可以直接理解的,这些可以分别对应一个Command。
你说的储存命令,不知是否笔误?在CQRS中是不保存命令的,如果使用Event Sourcing,那会保存事件(非命令)。
这里我认为要分两种情况:
1. 不使用Event Sourcing(事件溯源):
这即是本文中例子的情况,Event Sourcing并不是实现CQRS所必须的技术,而且Event Sourcing一般比较难,故这一系列文章的例子都是用最简单的CQRS,这样对未接触过CQRS的人比较好入手:)。不用Event Sourcing,则不会去保存事件,这样我们的数据库就会至少有两个,一个是存储领域模型的数据库(下面称Write DB),另一个是用于查询的数据库(Read DB),Command执行时,会触发一些事件,而这些事件的某些订阅者会负责将相关数据同步到Read DB。
2. 使用Event Sourcing:
这种情况中,将不存在1中的Write DB,取而代之的是Event Store,所以这种情况中将不会像原来那个保存Product、Order等,而是保存每个被触发的事件。而这些事件的某些订阅者,将会负责把相关数据同步到Read DB,这和1是一样的。
文中返回命令执行结果(新创建的用户的Id)并不是查询,那个Id是直接生成的,我们只是要让UI有能力去知道这个生成的ID是什么。
[b]这个ID不适合用一个独立的查询去做,因为没有办法保证查询到正确的值。[/b]这个我文中也有提到,因为在执行完命令的那一刹那,完全有可能有其它人注册了,那我们再查询Id只会查询到其它人的Id,这就直接影响到程序的正确性了。
如果说当执行完命令后界面要显示一个最新用户列表,那这时候就要按你说的利用查询去做这个事。
Leder 2012-04-05 16:08
Session["CurrentUserId"] = ???
这部分是查出来的,不是通过Command返回,呵呵,所以在设计Command的时候确实是没不必要加复杂的返回值。
CQRS模式是把命令查询职责分开,命令部分只是完成增删改之类的操作,通过储存命令并通过事件模式异步处理真正的数据库。通过查询(链接真正的数据库或快照)查询处理后的结果。
如果理解错了望指正。
水言木 2012-03-30 11:33
[quote]Leder:
@水言木
好像你的二、三个都是正确的吧。
int ret=CommandBus.Send<RegisterCommand, int>(registerCommand);
string ret=CommandBus.Send<RegisterCommand, string>(registerCommand);
其实这种方式也是有约束的,约束在于你的XXXCommondExecutor实现ICommandExecutor<TCommand, TResult>的时候已经指定了返回类型。
我看了Tiny Library的Command部分只返回bool,他的持久化是由事件部分去完成,好...[/quote]
其实还有第三种方案:让RegisterCommand去实现ICommand<string>,即添加ICommand<TResult>的接口,它同样也是空接口。但我感觉这种实现比较不优雅,没有文中的第二种方案来的简洁明了。
水言木 2012-03-30 11:31
[quote]Leder:
@水言木
好像你的二、三个都是正确的吧。
int ret=CommandBus.Send<RegisterCommand, int>(registerCommand);
string ret=CommandBus.Send<RegisterCommand, string>(registerCommand);
其实这种方式也是有约束的,约束在于你的XXXCommondExecutor实现ICommandExecutor<TCommand, TResult>的时候已经指定了返回类型。
我看了Tiny Library的Command部分只返回bool,他的持久化是由事件部分去完成,好...[/quote]
补充一点,嘿嘿。
这个约束的问题在于,对于Controller的开发人员来讲,他并不知道应该把它约束成string,因为CommandExecutor可能在重构过程中改成返回一个对象,不仅包含了UserId,而且还包含了其它一些信息,这样Controller的代码还是不变,而且编译不会出错,但在运行时,Controller中就会有找不到CommandExecutor的异常,这类bug不好调试。而第二种方案,我们用命名和属性的方式直接指明了返回值是RegisterCommandResult,如果将来修改了这个返回值类型,那Controller就会编译错误,第二种方案将返回值类型直接绑定到了Command上,而第一种不是。第二种方案也可以说是通过更好的命名来消除开发中的歧义。
水言木 2012-03-30 11:24
[quote]Leder:
@水言木
好像你的二、三个都是正确的吧。
int ret=CommandBus.Send<RegisterCommand, int>(registerCommand);
string ret=CommandBus.Send<RegisterCommand, string>(registerCommand);
其实这种方式也是有约束的,约束在于你的XXXCommondExecutor实现ICommandExecutor<TCommand, TResult>的时候已经指定了返回类型。
我看了Tiny Library的Command部分只返回bool,他的持久化是由事件部分去完成,好...[/quote]
如果用第一种方案,RegisterCommandExecutor将会实现ICommandExecutor<RegisterCommand, string>接口(因为User.Id是string类型,而我们希望这个Command的返回值是UserId,所以第二个泛型参数用string),所以,对于一和二,CommandBus会找不到相应的CommandExecutor,只有用第三种调用时,才会找到CommandExecutor并执行。
CommandExecutor返回一些值来表示Command执行是成功还是失败是没有问题的,本文讨论的问题是"执行结果"(如例子中的UserId)的返回,执行结果会因Command的不同而不同。
Leder 2012-03-30 11:08
@水言木
好像你的二、三个都是正确的吧。
int ret=CommandBus.Send<RegisterCommand, int>(registerCommand);
string ret=CommandBus.Send<RegisterCommand, string>(registerCommand);
其实这种方式也是有约束的,约束在于你的XXXCommondExecutor实现ICommandExecutor<TCommand, TResult>的时候已经指定了返回类型。
我看了Tiny Library的Command部分只返回bool,他的持久化是由事件部分去完成,好像是这样,还在研究中。。。
水言木 2012-03-30 09:21
[quote]Leder:
我昨天写了一下返回值的代码,使用的是第一种方案,有个不好就是不能设置返回空值类型(void),从这种CQRS实现来说(因为反射后一个CommandExecutor只能对应一个Command,所以不存在多个返回类型的可能)第二种方法也是不错的。
关注博主的事件驱动。[/quote]
嘿嘿,感谢关注。
感觉第一种方案的最大问题在于,返回什么类型是由调用者来决定的,比如文中的例子,Controller中可以写成:
CommandBus.Send(registerCommand)
也可以写成
CommandBus.Send<RegisterCommand, int>(registerCommand)
也可以写成
CommandBus.Send<RegisterCommand, string>(registerCommand)
这些全都可以编译通过,但是只有第三个是正确的。一旦这样的代码多起来,代码就会变得难以理解和维护。
Leder 2012-03-30 08:22
我昨天写了一下返回值的代码,使用的是第一种方案,有个不好就是不能设置返回空值类型(void),从这种CQRS实现来说(因为反射后一个CommandExecutor只能对应一个Command,所以不存在多个返回类型的可能)第二种方法也是不错的。
关注博主的事件驱动。
水言木 2012-03-28 11:40
[quote]dax.net:
@redhat_gg
我之前开发了一个CQRS的案例系统,地址是:[url=http://tlibcqrs.codeplex.com]http://tlibcqrs.codeplex.com[/url],有兴趣可以去了解一下。[/quote]
嘿嘿,曾经研究这个时还请教过你一些问题:D
现在我也临时写了份代码放上去了。感觉tlibcqrs对于没接触过CQRS的人来说跨度还有点大,比如使用了Event Sourcing等,本系列打算从最简单的CQRS入手实现一个基本例子,然后再考虑是否研究高级主题 :P
PS: 不知dax大虾对文中“业务逻辑”那一段我的疑问有何看法?期待你的建议
水言木 2012-03-28 11:37
[quote]我想我是青蛙:
一直觉得这套架构把程序的开发搞的过于复杂。。。
可能比较适合大型网站吧。。。个人看法[/quote]
如果是小网站或小系统,就不合适使用CQRS,可能也不适合使用DDD,小型系统用快速开发的方式做有时可能还更好。要应用CQRS,最基本的得有相对复杂的业务逻辑。
水言木 2012-03-28 11:35
[quote]redhat_gg:所有代码都贴上 ,或者提供下载研究可能更好[/quote]
谢谢建议,已添加代码下载,3Q :)
dax.net 2012-03-28 10:30
@redhat_gg
我之前开发了一个CQRS的案例系统,地址是:[url=http://tlibcqrs.codeplex.com]http://tlibcqrs.codeplex.com[/url],有兴趣可以去了解一下。