欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

手撸一套纯粹的CQRS实现

程序员文章站 2022-03-22 12:01:53
关于CQRS,在实现上有很多差异,这是因为CQRS本身很简单,但是它犹如潘多拉魔盒的钥匙,有了它,读写分离、事件溯源、消息传递、最终一致性等都被引入了框架,从而导致CQRS背负了太多的混淆。本文旨在提供一套简单的CQRS实现,不依赖于ES、Messaging等概念,只关注CQRS本身。 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上,感兴趣的朋友请移步>>

如果代码中有错误或不合适的地方,请在评论中指出,谢谢支持。

参考文档