插件系统
定义
定义
一、说明
插件系统是一组能实现多播订阅的,可中断的触发器,其主要功能就是实现类似 事件、委托的通知消息。其设计核心来自于Aspnetcore的中间件,它有着和中间件一样的使用体验,同时也有着更高的灵活性和自由度。
二、产品特点
- 简单易用。
- 易扩展。
三、产品应用场景
- 所有可以使用事件。委托的场景。
四、与事件、委托相比
4.1 优点
- 订阅的时候可以不用知道被订阅方是谁,只需要知道要订阅什么通知即可。
- 订阅可以随时中断。例如:当事件多播的时候,即使其中一个订阅方已经处理,触发也不会停止,这样会造成资源浪费。
- 订阅回调。例如:第一个订阅方,想知道本次触发的最终结果是否已被处理时,委托则做不到。而插件则可以。
- 插件可以被继承,可以被扩展。
- 插件可以被注入。
- 插件可以独立负责相关功能,实现功能独立。可模块化功能。
4.2 缺点
- 使用插件会损耗一部分性能。
五、使用
5.1 声明插件接口及事件类
public class MyPluginEventArgs : PluginEventArgs
{
public string Words { get; set; }
}
/// <summary>
/// 定义一个插件接口,使其继承<see cref="IPlugin"/>
/// </summary>
public interface ISayPlugin : IPlugin
{
/// <summary>
/// Say。定义一个插件方法,必须遵循:
/// 1.必须是两个参数,第一个参数可以是任意类型,一般表示触发源。第二个参数必须继承<see cref="PluginEventArgs"/>
/// 2.返回值必须是Task。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <returns></returns>
Task Say(object sender, MyPluginEventArgs e);
}
确定插件唯一的是插件中的方法名
,所以该方法名应该尽量能确定唯一性。
一个插件接口中,可以声明多个插件方法。但是一般建议只声明一个,这样使用方在实现接口时更加灵活,避免实现多余的接口。
5.2 实现插件接口
public class SayHelloPlugin : PluginBase, ISayPlugin
{
public async Task Say(object sender, MyPluginEventArgs e)
{
Console.WriteLine($"{this.GetType().Name}------Enter");
if (e.Words == "hello")
{
Console.WriteLine($"{this.GetType().Name}------Say");
//当满足的时候输出,且不在调用下一个插件。
//亦或者设置e.Handled = true,即使调用下一个插件,也会无效
return;
}
await e.InvokeNext();
Console.WriteLine($"{this.GetType().Name}------Leave");
}
}
public class SayHiPlugin : PluginBase, ISayPlugin
{
public async Task Say(object sender, MyPluginEventArgs e)
{
Console.WriteLine($"{this.GetType().Name}------Enter");
if (e.Words == "hi")
{
Console.WriteLine($"{this.GetType().Name}------Say");
//当满足的时候输出,且不在调用下一个插件。
//亦或者设置e.Handled = true,即使调用下一个插件,也会无效
return;
}
await e.InvokeNext();
Console.WriteLine($"{this.GetType().Name}------Leave");
}
}
public class LastSayPlugin : PluginBase, ISayPlugin
{
public async Task Say(object sender, MyPluginEventArgs e)
{
Console.WriteLine($"{this.GetType().Name}------Enter");
Console.WriteLine($"您输入的{e.Words}似乎不被任何插件处理");
await e.InvokeNext();
Console.WriteLine($"{this.GetType().Name}------Leave");
}
}
实现的插件,可以继承PluginBase
,然后实现所需的插件接口,这样能简化实现过程。但是如果该类型已经拥有基类,则直接实现所需插件接口的全部内容即可。
插件实现的方法,一般建议是常规实现,如果是显式接口实现,则可能在net framework架构上不可用。
5.3 触发插件接口
IPluginsManager pluginsManager = new PluginsManager(new Container())
{
Enable = true//必须启用
};
pluginsManager.Add<SayHelloPlugin>();
pluginsManager.Add<SayHiPlugin>();
pluginsManager.Add<LastSayPlugin>();
//订阅插件,不仅可以使用声明插件的方式,还可以使用委托。
pluginsManager.Add(nameof(ISayPlugin.Say), () =>
{
Console.WriteLine("在Action1中获得");
});
pluginsManager.Add(nameof(ISayPlugin.Say), async (MyPluginEventArgs e) =>
{
Console.WriteLine("在Action2中获得");
await e.InvokeNext();
});
while (true)
{
Console.WriteLine("请输入hello,或者hi");
pluginsManager.Raise(nameof(ISayPlugin.Say), new object(), new MyPluginEventArgs()
{
Words = Console.ReadLine()
});
}
使用委托订阅插件时,如果订阅时,不包含PluginEventArgs
,即不用显式e.InvokeNext
。如果包含,则需要显式e.InvokeNext
。
5.4 执行结果
按照上述代码代码逻辑,我们声明了一个名 为ISayPlugin
的插件接口,里面包含一个Say
的方法。然后分别创建了SayHelloPlugin
、SayHiPlugin
、LastSayPlugin
三个类去实现ISayPlugin
接口。然后将该三个类都添加至插件管理器中,然后触发Say
方法。同时传入不同的参数。
当Words=test时,SayHelloPlugin
和SayHiPlugin
均不满足处理条件,所以会将数据转至下一个插件,直到LastSayPlugin
插件。然后当LastSayPlugin
处理结束以后,处理结果又按照LastSayPlugin
、SayHiPlugin
、SayHelloPlugin
的顺序退出插件。这样,即使SayHelloPlugin
无法处理该数据,也能得知该数据最终有没有被处理。
当Words=hello时,SayHelloPlugin
满足处理条件,并且终止插件的继续传递。
请输入hello,或者hi
test
SayHelloPlugin------Enter
SayHiPlugin------Enter
LastSayPlugin------Enter
您输入的test似乎不被任何插件处理
LastSayPlugin------Leave
SayHiPlugin------Leave
SayHelloPlugin------Leave
请输入hello,或者hi
hello
SayHelloPlugin------Enter
SayHelloPlugin------Say
请输入hello,或者hi
hi
SayHelloPlugin------Enter
SayHiPlugin------Enter
SayHiPlugin------Say
SayHelloPlugin------Leave
请输入hello,或者hi
六、插件特性
【执 行顺序】 按照插件的添加顺序依次执行。
【中断传递】 当某个插件在响应时,如果设置e.Handled=true,或者没有调用下一个插件,则该数据将不会再触发后续的插件。
七、提升插件性能
7.1 插件性能测试
如下图所示,添加10个插件,并且调用10000次。即:单个插件方法被调用10w次。
分别在net6.0,netcore3.1,net4.6.1上进行测试。测试项依次为:
- DirectRun(直接调用)
- ActionRun(委托调用)
- MethodInfoRun(反射调用)
- ExpressionRun(表达式树调用)
- PluginRun(插件调用)
- PluginActionRun(插件委托调用)
实际上插件内部使用的是IL调用,所以即使调用上有迭代,性能上也和直接调用的表达式树差不多(这里插件是采用递归的方式迭代,而表达式树测试则是直接for迭代)。但是总体而言性能上是有缺失的。
但是当使用插件委托调用时,性能上会有所提升。和直接调用相比,虽然性能降低了20%。但是这已经是委托调用的极限,且没有任何动态代码的生成。这意味着在unity调用也是完全可行的。
