昨天到今天,我根据自己的想法,将 QPlugins 插件引擎的框架给搭了下。欢迎有兴趣的朋友加入讨论和参考。
首先,QPlugins 插件框架是一个可替换的框架,所有的一切都可以被替换(All can replace),包括插件管理器自身。当然了,替换插件管理器这个有几个方法,直接实现一个新的 IQPluginsManager 的接口类型实例是最简单的,而这要求你重新编译宿主程序。如果不想编译宿主程序来替换它,那么,你有几个机会:
1、自己实现一个新的 IQPluginsManager 接口,然后调用 PluginsManager 函数返回的实例的 Replace 方法,将新的实例做为参数传递进去,那么 QPlugins 的内核将用新的 PluginsManager 来处理后续的服务请求。
2、自己实现一个新的 IQPluginsRouter 接口的服务,加入到路由表中,那么后续的查询服务时,都会走你的接口,从而替换掉了对系统的 IQPluginsManager 的访问。
3、其它的方法,作者没有想到,但一切皆有可能,不是吗?
其次,QPlugins 插件框架是一个基于服务的框架,可以说,在 QPlugins 框架中,一切皆是服务,服务即一切。默认情况下,QPlugins 将服务分成了三类:
- 加载器(Loaders)
加载器是实现了 IQPluginsLoader 和 IQPluginServcie 接口的服务。它由 PluginsManager 在启用或信用插件引擎时负责调用。 - 路由器(Routers)
路由器负责将请求的服务转发到新的服务接口上去,而程序对这个一无所知,从而构成服务使用者和服务提供者之间的更松散耦合。后面我们举一个简单的例子,看看路由器是如何影响系统的工作过程的。
- 普通服务(Services)
普通的服务都是按照树形结构组织的,QPlugins 不会人为约定这个树如何组织。我们认为如何组织这个插件树,是用户的责任,而不是 QPlugins 这个插件框架操心的问题。
好了,现在我们来了解一下 QPlugins 这个插件框架的启动顺序:
- 在程序启动时,创建 IQPluginsManager 的全局实例。
- 手动注册初始加载器,初始加载器用于加载其它的加载器或者直接加载服务,具体做什么,取决于加载器自身要干嘛,QPlugins 插件框架不会操碎了心。
- 手动调用 PluginsManager 函数返回的全局实例地址的 Start 方法,此时会发生什么呢?如果你第二步,没有指定好加载器,什么也不会发生!如果指定的加载器,则会调用加载器的 Execute 方法来执行加载过程,然后由加载器来做实际的服务加载和注册工作。
- 现在插件引擎已经准备就绪,可以提供进行各项服务了。
在插件服务引擎都准备好后,我们进一步研究下如果使用服务。我们这里结合 TQParams 的SaveToStream 方法来看:
procedure TQParams.SaveToStream(AStream: IQStream); var AService: IQPluginService; procedure DefaultHandler; var S: QStringA; L, W: Integer; P: PQCharA; begin S := qstring.Utf8Encode(AsJson); L := S.Length; P := PQCharA(S); while L > 0 do begin W := AStream.Write(P, L); Dec(L, W); Inc(P, W); end; end; begin AService := PluginsManager.RequireService('Services/Params/ToStream'); if Assigned(AService) then begin Add('@TargetStream', ptStream); if AService.Execute(Self) then // 尝试写入到流中 begin AStream.CopyFrom(Items[Count - 1].GetAsStream, 0); Delete(Count - 1); end else begin Delete(Count - 1); DefaultHandler; end; end else DefaultHandler; end;
- 首先,我们调用 PluginsManager.RequireService 方法来查询是否存在 Services/Params/ToStream 服务,如果存在,那么,我们为自己追加了一个名为 TargetStream 的参数,然后调用服务的 Execute 方法,并将自己做为参数传递给服务。Services/Params/ToStream 服务应将这个参数的内容保存到最后一个参数中,然后 QPlugins框架再将内容从中复制到目标流中,从而完成 IQParams 对象到流的转换。反之,如果执行失败,则使用默认方法来将参数保存到流中。
- 如果不存在这个服务,没啥可说的,使用默认实现。
根据上述流程,假设我们在这里有同时提供 XML、MsgPack、ProtoBuf 三种不同格式的序列化服务,它们都提供Services/Params/ToStream 服务,假设注册的路径分别是:
- Services/XML/FromParams
- Services/MsgPack/FromParams
- Services/ProtoBuf/FromParams
现在我们来考虑不同的情况:
- 三个中至少有一个注册为 Services/Params/ToStream 服务
- 三个都没有注册为 Services/Params/ToStream 服务
在第一种情况下,在调用 RequireService 时我们会发现 QPlugins 每次返回了注册的第一个服务。
在第二种情况下,不用说了,肯定返回的是空地址,不提供相关的服务。
为了更好的适应不同用户的需要,我们不推荐服务自行注册到相关服务结点下,而是通过 QPlugins 引入的路由器(Router)的概念来动态个性化设定。
假设下面的路由表存在:
Services/Params/ToStream -> Services/XML/FromParams
当我们查询 Services/Params/ToStream 服务时,我们实际上得到的将是 Services/XML/FromParams,从而实现用 XML 格式存贮 IQParams 的内容到流中。同样的,如果我们要使用 MsgPack 格式时,只需要将上面的路由表修改为:
Services/Params/ToStream -> Services/MsgPack/FromParams
即可。
那么,如果我们就是想用 XML 格式保存流怎么办?那我们直接请求 Services/XML/FromParams 服务就好了。
下面回答下几个问题:
- 我能不能在插件注册、注销、执行等时间点得到相关的通知?
能,但目前我还没加上相关的支持框架,后期会加上。 - 我怎么能得到服务和插件数量等信息?
实际上,PluginsManager 同时要实现了 IQPluginServices 接口,所以可以通过它这个接口来枚举。 - 单实例服务和多实例服务怎么来区分?
实际上,在这个事情,QPlugins 采用了偷懒的办法。IQPluginService 要求实现一个 NewInstance 方法,用于返回一个新的实例,如果是单实例,直接返回自身,如果是多实例,返回新实例就好了。实际上,QPlugins单元实现一个TQPluginService,默认就是单实例(直接返回自己),而要实现多实例,你只需要继承自它并返回新的服务实例就好了。 - 如何实现服务的延迟加载?
实现上,服务注册时实际上只是创建了一个默认服务,但这个服务是否就是最终提供给用户的服务并不一定。因为服务在返回用户之前,是调用了这个默认的服务实例的 NewInstance 方法来给用户的,所以这里就有了一个问题:
a. 如果服务类型是一致的,并且是单实例的,那么我们只需要简单的返回自身就好了(实际上TQPluginService的默认 NewInstance 实现就是这样子干的)。
b. 如果服务类型是一致的,但要求是多实例的,那么我们返回一个新建的当前类型的实例就好了。
c. 如果服务类型是不致的,实际是由另一个服务提供,那么 NewInstance 返回新的服务就好了。这个实例实际上就是在使用时才创建的,达到了延迟加载服务的目的。 - QPlugins 是否支持工厂模式?
工厂模式实际上个人感觉也是蛮有争议的一个东西,工厂模式隐藏了类的细节,从而只能在一定的抽象层次上来考虑问题。凡事就是两面,毁誉可参半。QPlugins 内核不试图去区分这些东西,由于在请求服务时,QPlugins 是通过 RequireService 来请求服务的,而路由器(Router)在派发的过程中,就可以根据 RequireService 函数的参数来生成真正的实例。我们举个例子:PluginsManager.RequireService('Services/Human?type=man')
这样告诉路由器生成一个类型为男人的Human实例,而反过来,我们指定:
PluginsManager.RequireService('Services/Human?type=woman')
就可以生成一个类型为女人的Human实例,从而实现工厂模式。当然这一切都是由路由器来支持的,IQPluginsManager 接口的 RequireService 是否支持它,如果支持它,将是由用户为其配备的路由器(Router)的能力决定。
- QPlugins 是否能够直接使用 “As 接口类型” 来请求服务?
可以,QPlugins 的 PluginsManager 可以直接 As 各种提供的服务ID,如插件注册了服务 IHumanService,那么在编码时,你就可以直接用:var AService:IHumanService; begin AService:=PluginsManager as IHumanService; ... end
来获取服务的实例,当然,你也可以用:
var AService:IHumanService; begin if Supports(PluginsManager,IHumanService,AService) then begin ... end end
这种方式来判断是否提供了这种服务,然后再执行后续的操作。
QPlugins 我现在编写了一个简单的演示框架供大家了解和探讨设计思路,希望各位多多参与,提供更好的建议和想法。