CQRS實踐(3): Command執(zhí)行結(jié)果的返回
上篇隨筆討論了CQRS中Command的一種基本實現(xiàn)。
面對UI中的各種命令,Controller會創(chuàng)建相應(yīng)的Command對象,然后將其交給CommandBus,由CommandBus統(tǒng)一派發(fā)到相應(yīng)的CommandExecutor中去執(zhí)行,我們的ICommandBus的接口聲明如下:
public interface ICommandBus
{
void Send<TCommand>(TCommand cmd) where TCommand : ICommand;
}
當(dāng)在實際項目中應(yīng)用CQRS時,我們會發(fā)現(xiàn)上面的做法存在一個問題:有時候我們希望Command在執(zhí)行完后返回一些結(jié)果,但上面的Send方法返回void,也就意味著我們沒有辦法得到執(zhí)行結(jié)果。我們以一個用戶注冊的例子來說明。
數(shù)據(jù)庫中用戶表(User)的定義為User (Id, Email, NickName, Password),括號中的為字段,其中Id為varchar(36)的Guid字符串。
注冊用戶的RegisterCommand代碼如下:
public class RegisterCommand : ICommand
{
public string Email { get; set; }
public string NickName { get; set; }
public string Password { get; set; }
public string ConfirmPassword { get; set; }
public RegisterCommand()
{
}
}
假設(shè)在注冊后需要將新注冊用戶的Id存到Session中,那Controller的實現(xiàn)就變得有點糾結(jié):
[HttpPost]
public ActionResult Register(RegisterCommand command)
{
CommandBus.Send(command);
Session["CurrentUserId"] = ???
return Redirect("/");
}
上面的???處要怎么寫?我們無法直接得到新注冊用戶的Id,因為Id只有在Command執(zhí)行時才會生成。
所以只能在CommandBus.Send(command)的下一行添加一個查詢:
[HttpPost]
public ActionResult Register(RegisterCommand command)
{
CommandBus.Send(command);
var newUserId = Query<User>().OrderByDescending(u => u.Id).Select(u => u.Id).First();
Session["CurrentUserId"] = newUserId;
return Redirect("/");
}
這樣雖可以勉強(qiáng)解決問題,但比較繁瑣,而且也無法保證在Send之后查詢之前的這一小片時間里不會有其它新用戶產(chǎn)生。于是,我們開始反思Command的設(shè)計......
解決方案1
這個方案也正是Sharp Architecture所采用的,即添加一個帶兩個泛型參數(shù)的ICommandExecutor<TCommand, TResult>接口,這樣我們就有了兩個ICommandExecutor接口:
public interface ICommandExecutor<TCommand>
where TCommand : ICommand
{
void Execute(TCommand cmd);
}
// 這是新添加的帶有兩個泛型參數(shù)的接口
public interface ICommandExecutor<TCommand, TResult>
where TCommand : ICommand
{
// Execute方法返回值變成TResult
TResult Execute(TCommand cmd);
}
為了適應(yīng)這種變化,我們也需要相應(yīng)修改ICommandBus的接口:
public interface ICommandBus
{
void Send<TCommand>(TCommand cmd) where TCommand : ICommand;
// 這個Send方法的返回值是TResult
TResult Send<TCommand, TResult>(TCommand cmd) where TCommand : ICommand;
}
看起來不錯,現(xiàn)在對于注冊用戶的例子,我們只要調(diào)用第二個Send方法即可:
[HttpPost]
public ActionResult Register(RegisterCommand command)
{
var newUserId = CommandBus.Send<RegisterCommand, string>(command);
Session["CurrentUserId"] = newUserId;
return Redirect("/");
}
但如果我們仔細(xì)看看,會發(fā)現(xiàn)這是一個非常糟糕的設(shè)計!Controller的開發(fā)人員怎么知道RegisterCommand執(zhí)行完會返回結(jié)果?怎么知道返回的是string而不是int?Controller中可以寫成CommandBus.Send(command),也可以寫成CommandBus.Send<RegisterCommand, int>(command),也可以寫成CommandBus.Send<RegisterCommand, string>(command),同樣是發(fā)送RegisterCommand命令,這三種調(diào)用全都可以編譯通過,但是只有第三個才不會在運(yùn)行時出現(xiàn)問題。
漸漸的,我們就會變成每調(diào)用一次CommandBus.Send()方法就要去查看對應(yīng)的CommandExecutor是怎么實現(xiàn)的,這就讓Command和CommandExecutor相分離的設(shè)計變得一點意義都沒有。所以我們需要尋求其它的解決方案。
PS: Sharp Architecture中的ICommandHandler對應(yīng)本文中的ICommandExecutor,ICommandProcessor對應(yīng)本文中的ICommandBus,但我覺得它的ICommandProcessor的取名也太容易讓人誤解了,單從名字上看,誰能分清楚ICommandHandler和ICommandProcessor的區(qū)別?
解決方案2
其實這個方案非常簡單:在Command對象中添加一個ExecutionResult的屬性(這個屬性要放在具體的Command類中,不要放于ICommand接口中)。如上面的用戶注冊的例子,我們可以添加一個RegisterCommandResult的類,然后將RegisterCommand改成如下所示:
public class RegisterCommand : ICommand
{
public string Email { get; set; }
public string NickName { get; set; }
public string Password { get; set; }
public string ConfirmPassword { get; set; }
// 亮點在這里
public RegisterCommandResult ExecutionResult { get; set; }
public RegisterCommand()
{
}
}
// 亮點在這里
public class RegisterCommandResult
{
public string GeneratedUserId { get; set; }
}
在調(diào)用CommandBus.Send()之前,我們完全不用理會這個ExecutionResult屬性,對于Controller的開發(fā)人員來說,他只要知道在Command執(zhí)行完后,ExecutionResult的值就會被賦上,如果沒有,那就是CommandExecutor的bug。
而我們的RegisterCommandExecutor就可以改成(User類的構(gòu)造函數(shù)會調(diào)用Id = Guid.NewGuid().ToString()對自己的Id進(jìn)行賦值):
class RegisterCommandExecutor : ICommandExecutor<RegisterCommand>
{
public IRepository<User> _repository;
public RegisterCommandExecutor(IRepository<User> repository)
{
_repository = repository;
}
public void Execute(RegisterCommand cmd)
{
var service = new RegistrationService(_repository);
var user = service.Register(cmd.Email, cmd.NickName, cmd.Password);
// 亮點在這里
cmd.ExecutionResult = new RegisterCommandResult
{
GeneratedUserId = user.Id
};
}
}
然后Controller就很簡單了:
[HttpPost]
public ActionResult Register(RegisterCommand command)
{
CommandBus.Send(command);
// 亮點在這里
Session["CurrentUserId"] = command.ExecutionResult.GeneratedUserId;
return Redirect("/");
}
這個方案和第一個方案的關(guān)鍵區(qū)別就在于,RegisterCommand中定義的ExecutionResult屬性可以讓開發(fā)人員清楚的知道這個屬性會在Command執(zhí)行完后被賦上合適的值。對于一個Command,如果開發(fā)人員在其中找到類似ExecutionResult這樣的屬性,他就知道這個Command執(zhí)行完后會返回執(zhí)行結(jié)果,并且結(jié)果是以賦值的形式賦給Command中的ExecutionResult屬性,若Command中沒有發(fā)現(xiàn)ExecutionResult這樣的屬性,那開發(fā)人員便知道這個Command執(zhí)行完不會返回執(zhí)行結(jié)果。
PS: 因為本例中User.Id采用的是Guid字符串,它可以在創(chuàng)建User對象時立刻生成,所以下載中的代碼可以跑得還不錯,但如果User.Id是使用SQL Server的自增長int類型,那就跑不了了,因為UnitOfWork是在Command執(zhí)行完后才Commit的,所以,要處理自增Id的情況,我們需要稍微修改相應(yīng)代碼,比如將IUnitOfWork實例作為參數(shù)傳給ICommandExecutor.Execute()方法,并把IUnitOfWork的提交轉(zhuǎn)交給CommandExecutor負(fù)責(zé),然后在對RegisterCommand.ExecutionResult屬性賦值前先調(diào)用IUnitOfWork.Commit()方法,這樣便可以解決問題。
到目前為止,我們所討論的Command都是同步執(zhí)行的,如果Command被設(shè)計為異步執(zhí)行,那本文所討論的內(nèi)容便可以直接忽略。
如果系統(tǒng)的性能可以滿足需求,同步Command無疑是最好的。
浙公網(wǎng)安備 33010602011771號