系列文章目录
前言
软件开发中主模块与子模块之间如何进行解耦一直是一个令人头疼的问题,采用事件分发系统可以使得实现模块之间完全的解耦,但这种解决方案并不适用于解决此类问题。其关键在于事件分发系统主要设计用于处理主模块之间的同级调用或下层模块向上层模块发送事件之类粒度较大的事件。如果主模块内部也采用事件系统调用会造成事件泛滥破坏系统稳定性,并会使得模块边界不明确,内部关系模糊的问题,不遵寻高内聚准则。 另外一个不可使用事件系统的重要原因在于主模块内部的调用关系不同于主模块之间的调用关系,主模块内部调用耦合更加密切,子模块与主模块之间往往存在向上依赖关系,这种关系使得使用事件系统变的困难。为此,为了解决这种种问题必须为主模块内部调用量身打造一套专属的解决方案。
一、模块内部松耦合的重要性
为什么需要实现模块内部的松耦合设计?我们可以以汽车来进行举例说明。我们把汽车分为两个部分车身与轮胎,在这个关系中车身与轮胎共同构成汽车这一系统,其中车身为主模块提供总体控制,轮胎作为子模块为车身提供动力(服务),假如我们单独拿出轮胎它理应能够实现其功能即提供动力,而单独拿出车身则不行,因为它依赖轮胎这个组件。模块内部的松耦合设计使得子模块不必要依赖主模块而存在,其更类似于一种组件的存在形式,提高了系统的扩展性与维护性。
二、明确概念
在正式解读代码之前需要先引入几个概念。想象这样一个场景,cpu在运行某一进程过程中接收到IO设备的输入请求,此时会进入中断处理请求来进行一个外部输入的处理过程。此时是外设向cpu发出的一个通知信号而触发了处理过程。IO设备想要进行数据传输时需要先向cpu申请总线使用权,此时需要查询总线是否处于占用状态,这个过程中IO向cpu发出一个查询信号以获取总线状态。从上述例子我们可以抽象出通信其实就是模块之间信号传递的过程,而这个过程大体上又可分为两类:
1.通知:通知其他模块做某件事情,不会影响发起通知的模块自身的执行,类似于一个事件
2.查询:发起查询的模块要求被发起的一方返回一个状态,根据该状态决定下一步操作
通知与查询可以统一视为一种 信号
三、代码解读
1.子模块收发器组件
//子模块收发器组件
class SubModuleTransceiver
{
Dictionary<int, Delegate> NotifyDic = new Dictionary<int, Delegate>();//通知字典
Dictionary<int, Delegate> QueryDic = new Dictionary<int, Delegate>();//查询字典
/// <summary>
/// 发送带一个参数的通知(由子模块操作)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="signal"></param>
/// <param name="para"></param>
public void SendNotify<T>(int signal,T para)
{
if (NotifyDic.ContainsKey(signal))
{
if (NotifyDic[signal] is Action<T> notify)
{
notify?.Invoke(para);
}
else
{
Console.WriteLine($"通知号{signal}参数类型不匹配!!");
}
}
}
/// <summary>
/// 发送带参数查询(由子模块操作)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="K"></typeparam>
/// <param name="signal"></param>
/// <param name="para"></param>
/// <returns></returns>
public T SendQuery<T,K>(int signal,K para)
{
if (QueryDic.ContainsKey(signal))
{
if (QueryDic[signal] is Func<K,T> query)
{
return query(para);
}
else
{
Console.WriteLine($"查询号{signal}参数类型不匹配!!");
return default;
}
}
else
{
return default;
}
}
/// <summary>
/// 绑定通知信号(由主模块操作)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="signal"></param>
/// <param name="handle"></param>
public void BindNotify<T>(int signal,Action<T> handle)
{
NotifyDic[signal] = handle;
}
/// <summary>
/// 绑定查询信号(由主模块操作)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="K"></typeparam>
/// <param name="signal"></param>
/// <param name="handle"></param>
public void BindQuery<T, K>(int signal,Func<K,T> handle)
{
QueryDic[signal]=handle;
}
/// <summary>
/// 解绑某个通知信号(由主模块操作)
/// </summary>
/// <param name="signal"></param>
public void UnBindNotify(int signal)
{
if (NotifyDic.ContainsKey(signal))
{
NotifyDic.Remove(signal);
}
}
/// <summary>
/// 解绑某个查询信号(由主模块操作)
/// </summary>
/// <param name="signal"></param>
public void UnBindQuery(int signal)
{
if (QueryDic.ContainsKey(signal))
{
QueryDic.Remove(signal);
}
}
private void CreatNullNotify<T>(int signal)
{
Action<T> notify = (arg) => { };
NotifyDic[signal] = notify;
}
private void CreatNullQuery<T,K>(int signal)
{
Func<K,T> query=x=>default;
QueryDic[signal]=query;
}
}
此模块为核心组件,可以视为专为子模块设计的一个小型的事件收发系统。其将对于其他模块无返回值类型api的调用视为一次通知。而有返回值的调用视为一次查询,不同的通知与不同的查询通过信号码来区分。因为两者分开存储通知与查询的信号码相互独立不冲突。它提供了一些主要的接口,包括由子模块发起信号的接口以及主模块绑定信号处理过程的接口与解绑接口。采用组件化设计而非继承使得子模块有机会继承其他的组件。
如果需要扩展其他的参数类型,如无参的信号、带多个参数的信号可仿照上例代码自行扩充。
四、使用示例
1.主模块
internal class Module
{
SubModuleA subMoudleA = new SubModuleA();
SubModuleB subMoudleB = new SubModuleB();
public void Init()
{
//绑定子模块A信号处理
var transceiver = subMoudleA.transceiver;
transceiver.BindNotify<int>(SubModuleA.API1_int,API1);
transceiver.BindNotify<object>(SubModuleA.API1_object,API1);
transceiver.BindQuery<int,int>(SubModuleA.API2,API2);
//对于子模块B的调用由主模块转接
transceiver.BindNotify<int>(SubModuleA.B_API1,x=>subMoudleB.Api1(x));
transceiver.BindQuery<int,object>(SubModuleA.B_API2,x=>subMoudleB.Api2(x));
//绑定子模块B信号处理
transceiver = subMoudleB.transceiver;
transceiver.BindNotify<int>(SubModuleA.API1_int, API1);
transceiver.BindNotify<object>(SubModuleA.API1_object, API1);
transceiver.BindQuery<int, int>(SubModuleA.API2, API2);
//对于子模块A的调用由主模块转接
transceiver.BindNotify<int>(SubModuleA.B_API1, x => subMoudleA.Api1(x));
transceiver.BindQuery<int, object>(SubModuleA.B_API2, x => subMoudleB.Api2(x));
//测试
subMoudleA.Tongxin();
subMoudleB.Tongxin();
}
void API1(int a)
{
Console.WriteLine($"主模块接收通知{a}");
}
void API1(object o)
{
Console.WriteLine($"主模块接收通知{o}");
}
int API2(int a)
{
Console.WriteLine($"主模块接收查询{a}");
return 10;
}
}
主模块创建子模块,在初始化方法中对子模块的各种信号进行绑定处理。其中,对于调用其他子模块api的信号由主模块进行转发。通过这种方式统一了子模块与主模块的向上依赖、子模块与子模块之间的同级依赖。
注意:主模块内部存在重载的api,这种api可以通过分配不同的信号码加以区别
2.子模块A
class SubModuleA
{
public SubModuleTransceiver transceiver=new SubModuleTransceiver();
//定义通知事件
/// <summary>
/// int
/// </summary>
public const int API1_int = 1;//主模块api1
public const int API1_object = 2;//主模块api1
public const int B_API1 = 3;//子模块B api1
//定义查询事件 (两者信号码可以重复)
public const int API2 = 1;//主模块api2
public const int B_API2 = 2;//子模块B api2
public void Api1(int a)
{
Console.WriteLine($"子模块A接收通知{a}");
}
public int Api2(object o)
{
Console.WriteLine($"子模块B接收查询{o}");
return 20;
}
public void Tongxin()
{
//发送通知
transceiver.SendNotify(API1_int,5);//通知主模块
transceiver.SendNotify(API1_object,this);//通知主模块
transceiver.SendNotify(B_API1,5);//通知子模块B由主模块转接
//发送查询
var a= transceiver.SendQuery<int,int>(API2,5);
Console.WriteLine($"子模块A获取查询结果{a}");
var b= transceiver.SendQuery<int,object>(B_API2,this);
Console.WriteLine($"子模块A获取查询结果{b}");
Console.WriteLine();
}
}
子模块内部持有一个收发器组件,并定义相关的通知信号码与查询信号码,通过收发器组件向外界传递各种信号。注意由于并非直接调用api,发送信号的过程编辑器不会提示相关参数类型,这给编辑代码带来不便。
Tips:可以如上所示通过在信号码上添加类型声明注释的方式解决此问题
信号码的命名格式可以自定义一个协议,只要意义明确就行,这里给出我的一个示例:模块名-api-参数列表。信号码也可以使用枚举方式,具体看个人需求。
3.子模块B
class SubModuleB
{
public SubModuleTransceiver transceiver = new SubModuleTransceiver();
//定义通知事件
public const int API1_int = 1;//主模块api1
public const int API1_object = 2;//主模块api1
public const int B_API1 = 3;//子模块B api1
//定义查询事件 (两者信号码可以重复)
public const int API2 = 1;//主模块api2
public const int B_API2 = 2;//子模块B api2
public void Api1(int a)
{
Console.WriteLine($"子模块B接收通知{a}");
}
public int Api2(object o)
{
Console.WriteLine($"子模块B接收查询{o}");
return 30;
}
public void Tongxin()
{
//发送通知
transceiver.SendNotify(API1_int, 5);//通知主模块
transceiver.SendNotify(API1_object, this);//通知主模块
transceiver.SendNotify(B_API1, 5);//通知子模块B由主模块转接
//发送查询
var a = transceiver.SendQuery<int, int>(API2, 5);
Console.WriteLine($"子模块B获取查询结果{a}");
var b = transceiver.SendQuery<int, object>(B_API2, this);
Console.WriteLine($"子模块B获取查询结果{b}");
Console.WriteLine();
}
}
子模块B代码同上。
3.运行结果
运行程序,调用主模块初始化产生如下结果:
总结
上述解决方案实现了主模块内部松耦合的设计目标,但依然存在部分问题有待完善,可自行发挥加以解决。 优劣势总结如下:
优势:
1.松耦合:模块之间通过信号通信,减少了直接依赖。
2.灵活性:可以轻松添加和删除信号处理器,无需修改模块内部逻辑。
劣势:
1.api参数不再明确,编码带来不便
2.主模块需要许多额外代码逻辑来处理各个模块的信号绑定操作
适用场景:
模块内耦合严重,解耦需求迫切的情景。一般存在适度耦合的模块不推荐使用此种方式。