手撸一套纯粹的CQRS实现
关于cqrs,在实现上有很多差异,这是因为cqrs本身很简单,但是它犹如潘多拉魔盒的钥匙,有了它,读写分离、事件溯源、消息传递、最终一致性等都被引入了框架,从而导致cqrs背负了太多的混淆。本文旨在提供一套简单的cqrs实现,不依赖于es、messaging等概念,只关注cqrs本身。
cqrs的本质是什么呢?我的理解是,它分离了读写,为读写使用不同的数据模型,并根据职责来创建相应的读写对象;除此之外其它任何的概念都是对cqrs的扩展。
下面的伪代码将展示cqrs的本质:
使用cqrs之前:
customerservice
void makecustomerpreferred(customerid) customer getcustomer(customerid) customerset getcustomerswithname(name) customerset getpreferredcustomers() void changecustomerlocale(customerid, newlocale) void createcustomer(customer) void editcustomerdetails(customerdetails)
使用cqrs之后:
customerwriteservice
void makecustomerpreferred(customerid) void changecustomerlocale(customerid, newlocale) void createcustomer(customer) void editcustomerdetails(customerdetails)
customerreadservice
customer getcustomer(customerid) customerset getcustomerswithname(name) customerset getpreferredcustomers()
query
查询(query): 返回结果,但是不会改变对象的状态,对系统没有副作用。
查询的实现比较简单,我们首先定义一个只读的仓储:
public interface ireadonlybookrepository { ilist<bookitemdto> getbooks(); bookdto getbyid(string id); }
然后在controller中使用它:
public iactionresult index() { var books = readonlybookrepository.getbooks(); return view(books); }
command
命令(command): 不返回任何结果(void),但会改变对象的状态。
命令代表用户的意图,包含业务数据。
首先定义icommand接口,该接口不含任何方法和属性,仅作为标记来使用。
public interface icommand { }
与command对应的有一个commandhandler,handler中定义了具体的操作。
public interface icommandhandler<tcommand> where tcommand : icommand { void execute(tcommand command); }
为了能够封装handler的定位,我们还需要定一个icommandhandlerfactory:
public interface icommandhandlerfactory { icommandhandler<t> gethandler<t>() where t : icommand; }
icommandhandlerfactory的实现:
public class commandhandlerfactory : icommandhandlerfactory { private readonly iserviceprovider serviceprovider; public commandhandlerfactory(iserviceprovider serviceprovider) { this.serviceprovider = serviceprovider; } public icommandhandler<t> gethandler<t>() where t : icommand { var types = gethandlertypes<t>(); if (!types.any()) { return null; } //实例化handler var handler = this.serviceprovider.getservice(types.firstordefault()) as icommandhandler<t>; return handler; } //这段代码来自diary.cqrs项目,用于查找command对应的commandhandler private ienumerable<type> gethandlertypes<t>() where t : icommand { var handlers = typeof(icommandhandler<>).assembly.getexportedtypes() .where(x => x.getinterfaces() .any(a => a.isgenerictype && a.getgenerictypedefinition() == typeof(icommandhandler<>))) .where(h => h.getinterfaces() .any(ii => ii.getgenericarguments() .any(aa => aa == typeof(t)))).tolist(); return handlers; }
然后我们定义一个icommandbus,icommandbus通过send方法来发送命令和执行命令。定义如下:
public interface icommandbus { void send<t>(t command) where t : icommand; }
icommandbus的实现:
public class commandbus : icommandbus { private readonly icommandhandlerfactory handlerfactory; public commandbus(icommandhandlerfactory handlerfactory) { this.handlerfactory = handlerfactory; } public void send<t>(t command) where t : icommand { var handler = handlerfactory.gethandler<t>(); if (handler == null) { throw new exception("未找到对应的处理程序"); } handler.execute(command); } }
我们来定一个新增命令createbookcommand:
public class createbookcommand : icommand { public createbookcommand(createbookdto dto) { this.dto = dto; } public createbookdto dto { get; set; } }
我不知道这里直接使用dto对象来初始化是否合理,我先这样来实现
对应createbookcommand的handler如下:
public class createbookcommandhandler : icommandhandler<createbookcommand> { private readonly iwritablebookrepository bookwritablerepository; public createbookcommandhandler(iwritablebookrepository bookwritablerepository) { this.bookwritablerepository = bookwritablerepository; } public void execute(createbookcommand command) { bookwritablerepository.createbook(command.dto); } }
当我们在controller中使用时,代码是这样的:
[httppost] public iactionresult create(createbookdto dto) { dto.id = guid.newguid().tostring("n"); var command = new createbookcommand(dto); commandbus.send(command); return redirect("~/book"); }
ui层不需要了解command的执行过程,只需要将命令通过commandbus发送出去即可,对于前端的操作也很简洁。
该实例的完整代码在github上,感兴趣的朋友请移步>>
如果代码中有错误或不合适的地方,请在评论中指出,谢谢支持。