[C#] 命令总线模式
清水栞 人气:01 高内聚、低耦合
虽然已经毕业很多年了,但依然总是能记得,《软件工程》这门课的老师总是强调 "高内聚,低耦合"。
这些年,在架构方面的技术发展方向,目标就是不断的拆分代码。
其本质就是 —— 提高内聚性、降低耦合度。
2 我们要拆分什么?
为了降低程序的耦合度,我们总是把代码拆分的更细小。
- 我们拆分业务,得到微服务。
- 我们拆分出 表现 - 控制 - 模型,得到了 MVC。
- 我们拆分出 交互 - 逻辑 - 存储,得到了三层模式。
每一次的拆分,都是对单个职责的细化。
现如今,我们的代码基本上是这样子的:(下面的例子是由用户角度出度,以数据方向顺序来举例的 )
- 前台是由 webpack 打包的单页面应用
- 利用 ajax 将请求发送到 WebApi 上
- WebApi 根据 http 请求调度相应的 Controller
- 我们在 Controller 层调用相应的 Service
- Service 调用相应的 Repository 、Dao 或是其它什么数据持久层
我们根据不同的关注点,拆分成了不同的层次:前台、Controller 层、Service 层、等等。。。。
2.1 前台
我们其实可以把前台看作一个独立的应用,它其实同样需要拆分,这里我就不细谈了。
2.2 Controller 层
无论是哪种的 Web 服务器、无论哪种开发语言,都会将接收的 Http 请求,转换成为某种固定的数据结构。
笼统的说,接下来程序要做的事情,就是拿着这个数据结构,根据里面数据的不同,去找一个 Controller 去执行。
这个 Controller 就是这个 Http 请求的接受者。
流程大致如下:
- 将 Http 请求封闭成一个 固定的数据结构
- 根据预设的规则,分析这个 固定的数据结构 ,创建一个 Controller
可以看出来,无论是哪种情况,它们在调用 Controller 前,都是同一个数据结构。
2.3 Service 层
大家也许并没有留意这个细节,
Service 的调用是一种同时要求知道 数据结构 和 接受者 的过程。
1 public class SomeController 2 { 3 private readonly ISomeService someService; 4 5 public SomeController(ISomeService someService) 6 { 7 this.someService = someService; 8 } 9 10 [HttpPost("Create")] 11 public SomeDto CreateSomething(SomethingDto parameters) 12 { 13 return this.someService.Create(new Something() 14 { 15 A = paraeters.A 16 // and so on 17 }); 18 } 19 }
从上面的代码可以看出来,当我们意图创建一个 Something 的时候,我们需要知道三件事
- 我们需要使用哪个 Service
- 我们需要调用 Service 上的哪个方法
- 我们需要以何种数据结构调用 Service 的方法
再对比 Http - Controller 之间的关系,就会发现不同了。
Controller 调用 | Service 调用 | |
---|---|---|
数据结构 | 始终相同 | 不同的 Service,基本都不一样 |
数据接受方 | 不需要知道 | 需要知道 |
接受方的方法 | 不需要知道 | 需要知道 |
很明显,Controller 调用方法更好,它的调用方法几乎不需要依赖太多的信息。
我们把这种调用的方式,称为 命令总线。
2 命令总线
命令总线 就像一个黑盒子,你要做的就是把命令放进去,然后结果就弹出来了。
至于一个命令如何去找到它的处理者,那肯定是有一定 规则。
有了 命令总线,有些事情就发生了变化。
不再需要问这个功能在哪个 Service 上了,只要知道这个功能用哪个命令就行了。
实际研发过程中,总有一些功能,放哪个 Service 上都可以,也都不完全合适。
但是把它看过一个命令,就不再会有任何的不合适了。
3 Reface.CommandBus
Reface.CommandBus 这是一个基于 .NetFramework 4.6.1 开发的 命令总线 工具库。
你可以用这个工具库创建命令,并且透明化的执行这些命令并得到结果。
3.1 相关地址
- https://www.nuget.org/packages/Reface.CommandBus/
- https://github.com/ShimizuShiori/Reface.CommandBus
3.2 使用方法
3.2.1 创建命令
所有的命令都必须实现 ICommand 这个无内容的接口。
广义上的命令,是一个开放的,无约束的数据结构。
但是在某些特殊的使用场景下,我们可以使用统一的数据结构作为命令的载体,更可以使用统一的数据结构作为响应的载体。
public class ACommand : ICommand { // 这里可以编写你需要的参数 }
3.2.2 创建命令处理器
命令处理器需要实现 ICommandHandler<T> 接口,其中泛型 T 就是待处理的 Command 的类型。
public class ACommandHandler : ICommandHandler<ACommand> { public object Handler(ACommand command) { return "A"; } }
3.3.3 将处理器注册到总线中
将处理器注册到总线中,是通过向 处理器工厂 实现的,当前版本中,有两个 处理器工厂 类型
- DefaultCommandHandlerFactory,这是一个基于硬编码注册的工厂
- ConfigurationCommandHandlerFactory,这是一个基于读取 .config 文件进行注册的工厂,Github 上有关于配置的方法,比较简单。
你也可以定制自己的 处理器工厂 完成更好的从注册到创建过程。
这里还有一个扩展库 [ Reface.CommandBus.IntegrateAutofac ] ,是于 Autofac 集成后,以程序集为单位进行 处理器 注册的实现,可以通过 nuget 安装得到。
3.3.4 创建 ICommandBus 实例
目前库内自带的是 DefaultCommandBus 实例
ICommandBus bus = new DefaultCommandBus(factory); // facotry 就是 ICommandHandlerFactory 的实例 ACommand cmd = new ACommand(); string result = bus.Dispatch<ACommand, string>(cmd);
result 就是执行结果。
通过这种模式对命令的调用,可以让命令发起方只需要关心使用哪个命令和返回的结果类型就可以了。
在比较小的应用领域,可以统一命令结构和响应结构。
加载全部内容