基于 golang interface 特性衍生的插件化处理

作者: adm 分类: go 发布时间: 2022-02-10

在设计程序的许多应用场景中我们会遇到大体分为三个阶段的任务流。

第一、入口

一个或多个入口,等待阻塞的、或者主动请求方式的。

==============================

比如任务流需要接受来自于 HTTP 和 FTP 的应用请求,后续还有可能增加别的方式的接受请求。

第二、处理

多个入口可以对应一个处理程序,也可以对应多个处理程序。

==================================

比如 HTTP 和 FTP 的多个入口程序需要对应一个和多个处理逻辑,同样也面临着增加处理程序的扩展性问题。

第三、出口

多个处理程序或者一个处理程序对应多个出口或者一个出口。

==================================

比如报警方式有邮件报警、微信报警等等,后续还有可能增加短信报警等等。

事实上可以把绝大部分需求可以抽象成上述思维。

那么对于这类问题我们采取的扩展性要求是,拓展功能时改动少量原有代码或者干脆不改动原有代码。在不考虑软件重构层面,当新入职员工接手上位大哥的代码时,我想最头疼的就是增加需求,因为你不知道更改代码会带来什么隐藏 BUG。

我们总是理想可以用”插件化”的思想来适应需求,写好框架,然后分门别类,每一个抽象出来的角色是一个大的容器,然后每次向这个容器中插一个个的插件,来一个需求,做一个插件插进去。来两个需求,做两个插件插一双进去。

更”过分”的要求是插进去插件必须有一个按钮来控制是不是启用该插件,理由就是拔插一次插件太耗时了(对应代码中的注释 N 多行)。

总之一切的一切我们想要的效果就是每一个插件独立工作,互不相应,增加可拓展性。这样带来的弊端就是可能有一部分的冗余代码,但是这样也比交接出去工作让人家天天在背后黑你要强的多吧。

那首先我们先用 golang 来造出一个需求容器。

/*
 * 需求容器
 *
**/
package need

import (
    "fmt"
)

// 我是存储插件的容器
var Plugins map[string]Need

// 每次导入容器都会执行的代码
func init() {
    /* 设定容器的容量 */
    Plugins = make(map[string]Need)
}

// 设定插在此容器的插件的样子
type Need interface {
    /* 它必须告诉容器它是否生病 */
    Flag() bool
    /* 它必须得有启动方法 */
    Start()
}

// 启动这个容器中所有的插件
func Start() {
    for name, plugin := range Plugins {
        /* 查看插件是否是启用状态 */
        if plugin.Flag() {
            go plugin.Start()
            fmt.Printf("启动插件 %s\n", name)
        } else {
            fmt.Printf("%s 插件生病了\n", name)
        }
    }
}

// 插件做完之后必须得插入到容器中
func Regist(name string, plugin Need) {
    Plugins[name] = plugin
}

这个需求容器对插入自己的插件有要求!

  1. 必须告诉容器插件本身是不是正常的
  2. 必须提供你插件本身的工作内容
  3. 必须与容器进行连接

如果缺少 1,那么就无法实现插件生病就放病假。

如果缺少 2,你说你不说你是干啥的容器怎么敢收你,万一是做不好的事咋办~

如果缺少 3,得与容器签订合同,绑定劳动关系。

按照上述要求,我们来实现一个插件一号。

/*
 * 插件一号
 *
 */
package plugin_1

import (
    "fmt"

    "need"
)

func init() {
    p_1 := plugin_1{}
    /* 与容器进行连接 */
    need.Regist("plugin_1", p_1)
}

type plugin_1 struct {
}

// 告诉插件我没生病
func (this plugin_1) Flag() bool {
    return true
}

// 告诉插件我是干啥的
func (this plugin_1) Start() {
    fmt.Println("我是酱油一号~")
}

现在是容器也有了,插件也有了,但是没电跑不起来啊。。。,我们现在再用 golang 给他提供个电源。

package main

import (
    "need"
    _ "need/plugin_1"
)

func main() {
    /* 电源只关心容器的启动,不关心容器的插件 */
    need.Start()
}

运行电源。

$ go run main.go
我是酱油一号~
启动插件 plugin_1

假设现在又来了两个需求,我们在如法炮制加入两个插件。

插件二号。

/*
 * 插件二号
 *
 */
package plugin_2

import (
    "fmt"

    "need"
)

func init() {
    p_2 := plugin_2{}
    /* 与容器进行连接 */
    need.Regist("plugin_2", p_2)
}

type plugin_2 struct {
}

// 告诉插件我没生病
func (this plugin_2) Flag() bool {
    return true
}

// 告诉插件我是干啥的
func (this plugin_2) Start() {
    fmt.Println("我是酱油二号~")
}

插件三号。

/*
 * 插件三号
 *
 */
package plugin_3

import (
    "fmt"

    "need"
)

func init() {
    p_3 := plugin_3{}
    /* 与容器进行连接 */
    need.Regist("plugin_3", p_3)
}

type plugin_3 struct {
}

// 告诉插件我没生病
func (this plugin_3) Flag() bool {
    return true
}

// 告诉插件我是干啥的
func (this plugin_3) Start() {
    fmt.Println("我是酱油三号~")
}

启动电源:

# go run main.go 
我是酱油三号~
启动插件 plugin_3
我是酱油二号~
启动插件 plugin_2
我是酱油一号~
启动插件 plugin_1

我们可以模拟一下插件二号生病了。更改插件二号的 true 为 false 再次启动电源。

$ go run main.go    
我是酱油三号~
启动插件 plugin_3
plugin_2 插件生病了
我是酱油一号~
启动插件 plugin_1

整个程序看起来就是这个样子的。

你也可以把它做成这样子的,不比担心电源负载过高。

合理的运用可以增加很好的拓展性,但是会增加不少的冗余代码(不要在意那些细节啦~)。

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!