基于Lua的可扩展架构设计
首先,这个架构是一定程度上模仿Eclipse的,并做了大量简化,所以模仿得并不像,不过不得不说最原始的灵感和很多想法是从它那里来的。
支撑起整个可扩展架构是一个类,大概名叫pluginsRegistry的类。这个类一般可以使用singleton模式,它做一些与插件相关的最基础最底层的事。比如,在宿主程序(这里一般是一个C++程序)启动时,它负责扫描指定的一个目录(包括子目录)下所有文件,找出其中的plugin.xml文件,也就是插件描述文件。它并不一定要是xml来描述,也可以是用Lua来描述,毕竟Lua最早的用途就是作为配置文件的,而且这个文件并不会被程序动态修改,Lua也够用,但要注意的是名字空间污染的问题,一个办法是每个描述文件都使用完全相同的结构和变量命名,但每次都单独启动一个Lua解释器来解析。一般说来一个描述文件中声明一个插件,也就是一些基本的插件信息,包括插件id、插件扩展点和插件对应脚本文件路径这三项最主要的不可或缺的信息,以及一些诸如被扩展点,版本号,依赖关系,版权信息等等不那么重要的信息。扫描到一个插件后,这个类会把插件id,脚本文件路径对应着保存起来,并把该插件添加到指定的被扩展点上。所谓扩展点,其实准确一点说,应该是被扩展点代表一个插件会被激活(其中一部分)的时机。被扩展点一般也是由其他插件提供的,也就是说其他插件会在某个它认为合适的时刻,激活所有注册到被扩展点下的所有插件。所以插件描述文件中很重要的一点是,需要声明自己是扩展了哪个被扩展点,不然就永远不可能被激活。由此可以看出,另外一个需要注意的是,一个插件一般会依赖于另外一些插件,至少是依赖于提供它要扩展的被扩展点的插件。一个插件必须要指明它要扩展的被扩展点,这是别的插件提供的,另外它也可以提供自己的被扩展点,让另外的插件来扩展自己的功能,当然这不是必须的。为了说起来不那么拗口,在描述文件中,一般我称前者为扩展点,称后者为被扩展点,这是把同一类事物在一个所属中以其不同的功能或角色来区别而得来的。再回到这个类的功能上来,除了前面讲的这几点功能外,它还应该能向外界提供一个服务,通过该服务外界可以根据指定的被扩展点查询到所有注册在该被扩展点下的插件,理论是希望只是通过一个id就能唯一标识一个插件。这样每当时机合适时,提供被扩展点的插件就可以通过这个服务来做一些事情,最常见的是通知所有该被扩展点下的插件即可,那些插件自己应该清楚这时应该做些什么事情,还有种情况是,提供被扩展点的插件应该可以从中区分出一些插件,而不是全部,来通知。一个很常见的应用场景,主菜单,可以是一个菜单项对应一个插件,这时每当用户点击了菜单项,那么只要激活该菜单项对应的那个插件即可,而不是所有主菜单下所有的插件都要激活一遍。
通过前面的阐述,可以知道,这个类需要一个清晰简单的接口,这样才可以比较方便地既给C++代码提供服务,又给Lua脚本提供服务。尽管Lua从设计上就考虑了很多跟其他语言交互的情形,但这里我还是得小心一点,少使用一些只有某种语言特有的一些元素或特性。
最后,以一个实例来简单叙述一下这个架构是怎么真正让一些功能运作起来的。就还是以主菜单为例,主菜单也是一个插件,当然,它是用C++还是用Lua实现的我们不关心。重要的是主菜单插件首先有一个自己的id,并提供了一个被扩展点,假设叫view.ui.menu,这时假设那个类已经把所有扫描到的插件正确地分析过了,这个被扩展点下已经有一些插件了,比如file_open,file_save等等。主菜单插件在自己被初始化时,调用那个类来得到所有view.ui.menu下的插件,并依次激活这些插件的get_caption方法,根据这些个方法返回的内容,作为菜单项的标题,逐个添加菜单项,并把菜单项的id和它对应的插件(可能就是用插件id来表示)保存起来,当用户点击一个菜单项时,主菜单插件可以得知用户点击的菜单项的id,并从之前保存的信息中得到它对应的插件id,以此来再调用那个类来激活唯一对应的那个插件,那个插件知道用户是点击了菜单点,就执行一些操作,比如打开文件等等。
大致的流程就是这样。