QPlugins – 基本的原型演示程序已经出来了,欢迎大家测试并提供建议

昨天到今天,我根据自己的想法,将 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 这个插件框架的启动顺序:

  1. 在程序启动时,创建 IQPluginsManager 的全局实例。
  2. 手动注册初始加载器,初始加载器用于加载其它的加载器或者直接加载服务,具体做什么,取决于加载器自身要干嘛,QPlugins 插件框架不会操碎了心。
  3. 手动调用 PluginsManager 函数返回的全局实例地址的 Start 方法,此时会发生什么呢?如果你第二步,没有指定好加载器,什么也不会发生!如果指定的加载器,则会调用加载器的 Execute 方法来执行加载过程,然后由加载器来做实际的服务加载和注册工作。
  4. 现在插件引擎已经准备就绪,可以提供进行各项服务了。

在插件服务引擎都准备好后,我们进一步研究下如果使用服务。我们这里结合 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;
  1. 首先,我们调用 PluginsManager.RequireService 方法来查询是否存在 Services/Params/ToStream 服务,如果存在,那么,我们为自己追加了一个名为 TargetStream 的参数,然后调用服务的 Execute 方法,并将自己做为参数传递给服务。Services/Params/ToStream 服务应将这个参数的内容保存到最后一个参数中,然后 QPlugins框架再将内容从中复制到目标流中,从而完成 IQParams 对象到流的转换。反之,如果执行失败,则使用默认方法来将参数保存到流中。
  2. 如果不存在这个服务,没啥可说的,使用默认实现。

根据上述流程,假设我们在这里有同时提供 XML、MsgPack、ProtoBuf 三种不同格式的序列化服务,它们都提供Services/Params/ToStream 服务,假设注册的路径分别是:

  • Services/XML/FromParams
  • Services/MsgPack/FromParams
  • Services/ProtoBuf/FromParams

现在我们来考虑不同的情况:

  1. 三个中至少有一个注册为 Services/Params/ToStream 服务
  2. 三个都没有注册为 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 服务就好了。

下面回答下几个问题:

  1. 我能不能在插件注册、注销、执行等时间点得到相关的通知?
    能,但目前我还没加上相关的支持框架,后期会加上。
  2. 我怎么能得到服务和插件数量等信息?
    实际上,PluginsManager 同时要实现了 IQPluginServices 接口,所以可以通过它这个接口来枚举。
  3. 单实例服务和多实例服务怎么来区分?
    实际上,在这个事情,QPlugins 采用了偷懒的办法。IQPluginService 要求实现一个 NewInstance 方法,用于返回一个新的实例,如果是单实例,直接返回自身,如果是多实例,返回新实例就好了。实际上,QPlugins单元实现一个TQPluginService,默认就是单实例(直接返回自己),而要实现多实例,你只需要继承自它并返回新的服务实例就好了。
  4. 如何实现服务的延迟加载?
    实现上,服务注册时实际上只是创建了一个默认服务,但这个服务是否就是最终提供给用户的服务并不一定。因为服务在返回用户之前,是调用了这个默认的服务实例的 NewInstance 方法来给用户的,所以这里就有了一个问题:
    a. 如果服务类型是一致的,并且是单实例的,那么我们只需要简单的返回自身就好了(实际上TQPluginService的默认 NewInstance 实现就是这样子干的)。
    b. 如果服务类型是一致的,但要求是多实例的,那么我们返回一个新建的当前类型的实例就好了。
    c. 如果服务类型是不致的,实际是由另一个服务提供,那么 NewInstance 返回新的服务就好了。这个实例实际上就是在使用时才创建的,达到了延迟加载服务的目的。
  5. QPlugins 是否支持工厂模式?
    工厂模式实际上个人感觉也是蛮有争议的一个东西,工厂模式隐藏了类的细节,从而只能在一定的抽象层次上来考虑问题。凡事就是两面,毁誉可参半。QPlugins 内核不试图去区分这些东西,由于在请求服务时,QPlugins 是通过 RequireService 来请求服务的,而路由器(Router)在派发的过程中,就可以根据 RequireService 函数的参数来生成真正的实例。我们举个例子:

    PluginsManager.RequireService('Services/Human?type=man')

    这样告诉路由器生成一个类型为男人的Human实例,而反过来,我们指定:

    PluginsManager.RequireService('Services/Human?type=woman')

    就可以生成一个类型为女人的Human实例,从而实现工厂模式。当然这一切都是由路由器来支持的,IQPluginsManager 接口的 RequireService 是否支持它,如果支持它,将是由用户为其配备的路由器(Router)的能力决定。

  6. 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  我现在编写了一个简单的演示框架供大家了解和探讨设计思路,希望各位多多参与,提供更好的建议和想法。

QPluginsFramework

分享到: