消息驱动
一、消息处理的command模式
首先设定一个消息包结构中必包含key字段,表明该消息需要由什么处理器来处理。
在aspnetcore-webapi中,通过解析http消息包中的请求url获得path,最终路由确定处理函数在controller-action中。
在有些框架中,喜欢使用command模式来确定处理函数,而处理函数被封装在独立的command类中由处理函数转化为了处理类。
我曾经一度非常迷恋这种command模式。因为对产品逻辑的轻视,我认为业务代码是非常容易变动的,在进入了command之后不需要在使用任何设计模式了。甚至应该在之后使用动态语言做脚本化来实现command的逻辑,以使线上服务能够即时更新或修复逻辑问题,后来这个问题从另一层面也就是从微服务滚动更新的方法也算是得到了一定程度的解决。
二、command模式的实现
最初的时候command模式是集成在网络框架中的,从tcp读取出数据后拆包、反序列化、处理一气呵成。后来提炼成三个模块,于是消息处理这块就得以跟网络解耦应用到其他地方成为消息驱动的命令模式。
1)实现的方式简单来说就是定义一个抽象类CommandBase,定义抽象处理函数。因为是消息驱动,所以消息必须是强类型,key->消息类型->处理类。所以CommandBase是泛型类,泛型参数为要处理的消息类型。
2)然后通过反射获取指定程序集Assembly中的所有CommandBase<TRequest>的实现类,创建实例,存入字典。
3)写一个中介者类型,实现一个派发函数,参数为消息实例,从字典中捞出实例类型对应的Command,调用处理函数。
三、几个问题点和暂行方案
整个的实现思路非常简单。现在分析下其中的几个问题。
1)首先是第一步中的CommandBase<TRequest>是个泛型抽象类。这限定了所有的Command都必须是其子类,这通常是没什么的问题。只是这个抽象类除此之外给不了什么其他的资产可以继承了,实属贫乏,更好的做法是定义为泛型接口ICommandBase<TRequest>。
2)接着是第二步中的字典,即使定义为泛型接口,那也是泛型呀,因为是泛型输入参数所以又不能协变为ICommandBase<object>,所以只能做为object存入字典吗?
第一种解决方案是还是定义一个非泛型的基础抽象类CommandBase,用object作为处理消息类型,由其子类CommandBase<TRequest>重写,转化object为泛型参数TRequest后调用泛型处理函数。只是这样又回到了问题1中。
第二种方案是不增加基类,而是使用装饰器,也就是另一个类比如CommandWrapper和它的泛型类型CommandWrapper<T>,让ICommandBase<T>作为其一个属性,将wrapper存入字典中。Wrapper实现一个消息处理函数调用其属性command的执行函数。这样基类的问题就转移给了这个包装类,而包装类是模块内部封装的,对外不可见。
3)生命周期的问题。Command应该作为单例吗?在之前的叙述里,字典中存放的已经是command实例了,所以一直是作为单例存在的。可是如果是单例的话,命令类是单例而处理函数是瞬时的,会造成一点误解,以及在实现某些需求,在注入的依赖类型的使用上会遇到一些生命周期冲突的问题。
我最终的办法是把生命周期和实例化的问题都丢给DI容器也就是IServiceCollection,也就是由使用者自己决定是通过哪种方式注入中介者类型,而Command实例也伴随着中介者类型的生命周期。在我跟我的网络处理框架集成的时候,我习惯将它的scope设定为跟网络连接的虚拟会话生命周期保持一致,这样可以在command中顺利获取到当前关联的会话。