基于动态软件体系结构的插件机制原理(C语言实现)

点击量:31

我们说软件体系结构的时候,常常说的是静态的体系结构。众所周知,静态的体系结构在运行的时候是不会发生结构上的变更的。而每当在结构上有变更的时候,比如给软件增加一个新的功能,新的模块,都需要重新编译相关的组件并部署。由于这个部署常常需要重启软件,这给一些软件的运行维护带来了极大的挑战,比如金融类的软件系统,正常情况下不能停止运行,哪怕数秒时间,否则会产生很多麻烦。而通过动态软件体系结构,使用可以“热插拔”的插件,我们就能够实现“给奔跑的汽车换零件”。

插件机制是什么

一个典型的有插件功能的系统,通常为“内核+插件”模式。举个例子,我们的电脑是可以通过USB接口来扩展很多功能的,插上U盘或者硬盘,就扩展了存储空间,插上USB网卡,就可以通过网线或WiFi上网,插上鼠标键盘,就可以操作电脑。理论上我们所运行的操作系统上的各种程序就是该操作系统的一个插件。我们在写java、python、C#等程序的时候,import或者using的那些类库,就是该编程语言的一个插件。我们在使用WordPress搭建的博客的时候,也有大量的插件可以直接安装使用,我们在玩Minecraft游戏的时候,也可以安装许多的模组来扩展游戏的可玩性。这些例子都是动态体系结构在应用中的表现。

具有插件机制的体系结构可以用如下示意图来表示:

具有插件功能的系统,在编程的时候,是面向接口编程的,而不是面向实现编程的。这种接口往往是一个通用的约定标准,这个标准规定了插件应该如何设计,以能够被主程序调用。这在面向对象编程领域中,是一种依赖反转原则。传统情况下,高层模块的实现依赖于低层模块的实现,通过依赖反转原则,高层模块就不依赖于低层模块的实现细节,只通过统一的接口标准进行调用,导致依赖关系被反转,从而使得低层模块依赖于高层模块的需求抽象。具体来说就是,我们给Windows或Linux系统进行编程,其实只是在使用该操作系统提供的标准接口进行插件的开发而已,程序的运行是由操作系统进行调度和管理的,但是我们编译的程序却依赖于该操作系统的标准,一般来说使用相关API为Windows系统开发的程序就无法运行于Linux系统上。

对于系统内核来说,其主要的功能就是:负责插件的加载、检测、初始化,负责服务的注册、调用,以及服务的管理。具体的业务的实现,则交给安装在该系统上的各种插件去处理。插件的本质在于可以在不修改程序主体的情况下,实现对软件在功能上进行扩展与加强。如果插件的接口是公开的,任何人都可以开发自己的插件,以解决自己遇到的问题,这就实现了真正意义上“即插即用”的软件开发。

C语言实现插件机制Demo

我们假定要实现这样一个支持使用插件的一个程序:输入两个整数A和B,对这两个整数进行某种运算操作,得到结果后将其输出。而且事先并不知道需要哪些运算操作,只知道该业务需要频繁处理两个整数的运算,并输出结果,而且需要随时使用不同运算方式,或者随时增删运算功能。

乙方为了不总是由于业务需求的更改,而被反复要求去修改程序,就给甲方留了一个开发接口,只要按照这种标准编写程序的模块,只要业务还是在处理两个整数的运算结果,那么就不需要反复修改代码,以适应不同的运算需求,而只需要随时替换模块即可。于是,乙方编写的main程序如下:

main.c

#include <stdio.h>
#include <dlfcn.h>
#include <string.h>

int main() {
    /*手动加载指定位置的so动态库*/

    char ch[100]={'.','/'};
    scanf("%s",&ch[2]);

    void* handle = dlopen(ch, RTLD_LAZY);
    int (*cacul)(int a, int b);
    /*根据动态链接库操作句柄与符号,返回符号对应的地址*/
    cacul = dlsym(handle, "caculate");

    int a,b;
    scanf("%d%d",&a,&b);
    int sum = cacul(a, b);
    printf(" = %d\n", sum);
    dlclose(handle);
    return 0;
}

编译命令为:

$ gcc -o main main.c -ldl

编译完成后,乙方为甲方提供了目前业务已知需要用到的基本的两个整数运算的功能加和减:

add.c

int caculate(int x, int y) {
    return (x + y);
}

sub.c

int caculate(int x, int y) {
    return (x - y);
}

同样也使用gcc进行编译:

$ gcc -shared -o add.so add.c
$ gcc -shared -o sub.so sub.c

于是我们得到了三个可执行程序文件:main、add.so和sub.so。

当需要加法计算的时候,我们就使用add.so模块,当需要减法计算的时候,我们就使用sub.so模块,如下所示:

$ ./main
add.so 5 9
 = 14

$ ./main
sub.so 7 8
 = -1

其中,在程序运行后的第一行为输入内容,第二行为输出内容。

一段时间后,甲方新增了一个业务需求,计算并输出两个整数的异或值,便自行开发可以计算异或的新模块并投入使用,代码如下:

int caculate(int x, int y) {
    return (x ^ y);
}

同样进行编译:

$ gcc -shared -o xor.so xor.c

在文件目录中,我们得到了xor.so文件。甲方就很快就自行将这一新的需求满足了,于是高高兴兴地投入使用了。此时运行的情况如下:

$ ./main
xor.so 7 8
 = 15

$ ./main
xor.so 4 4
 = 0

结论

尽管我们这里举的具体例子很简单,在实际的工程领域中,业务往往是复杂的,但是这样一个插件机制的体系结构是很常用的,这个样例的设计思想是可以揭示一个具有这样体系结构复杂事物的本质原理的。只要有设计合理的接口,遵循该标准,可以基于插件方式,从程序内核开始,扩展出一个又大又复杂的软件。而且不同的插件可以由不同的人进行开发,也大大提升了开发效率,保证了平台总体的稳定性,“即插即用”也确保了使用的便捷性和整体的可维护性。

版权声明
本博客的文章除特别说明外均为原创,本人版权所有。欢迎转载,转载请注明作者及来源链接,谢谢。
本文地址: https://blog.ailemon.me/2020/04/21/principle-of-plugin-mechanism-based-on-dynamic-software-architecture-clang-implementation/
All articles are under Attribution-NonCommercial-ShareAlike 4.0
打赏 赞(0)
微信
支付宝
微信二维码图片

微信扫描二维码打赏

支付宝二维码图片

支付宝扫描二维码打赏

留下评论

电子邮件地址不会被公开。 必填项已用*标注

2 + 17 =

如果您是第一次在本站发布评论,内容将在博主审核后显示,请耐心等待