百度统计
一面之猿网
让这个世界,因为我,有一点点的不一样
纯序员给你介绍图化框架的简单实现——函数注入

大家好,我是不会写代码的纯序员——Chunel Feng。在日常开发过程中,经常会涉及到迁移框架、重写逻辑等工作。新的框架兴许能解决之前一些功能的瓶颈和缺陷,但随之而来的是祖传代码和新型框架不兼容、难适配等各种各样奇怪的问题。不要问我是怎么知道的,说多了都是泪。

就拿传统流程型框架迁移到图框架来说吧,图框架可以通过上游的编排调度,很好的解决之前流程中依赖和并发的问题,但是一般都需要大家根据特定的规则来实现节点逻辑——也就是说,需要将原来的逻辑,分拆到特定的节点的特定位置——毕竟,我从来没有见过没有任何约束的图开发。

如果涉及到参数传递,还需要大家根据特定的方法,将原有的参数规整和拆分。这牵扯出一个新问题,原来代码中,各种参数都是随着接口定义而散乱在程序的各个位置,哪有这么容易统一生成、统一获取?

image.png


这个时候我们往往会想:如果有这样一种方法,能满足新功能按新框架的方式统一编排,老功能“原地迁移”,一键融入新的框架,或者等之后有空了再做整理(下次一定),那该有多好啊。

今天我们来给大家讲讲在图框架中CGraph中,实现模块编排和函数注入的混合编排的方法——GAdaptor中的GFunction功能。

首先,还是照例先上CGraph链接:CGraph源码链接

抛出问题

之前写过很多文章,介绍了CGraph中的节点执行逻辑,和参数统一传递的逻辑,在这里不多赘述。今天主要讲新的函数注入的方式。

举个例子,我原先的逻辑中,包含一个int和一个string变量,功能就是将int的值,加到string值的后面。

如果规规矩矩的按照图节点注册的方式来实现,我们需要实现一个GParam的类,其中包含了一个int和一个string类型的变量。然后再实现一个GNode的类,这个节点,在init()的时候,创建GParam的对象,然后在run()的时候,取出并且修改,然后执行打印逻辑。

大概是这么几步吧,我来写几句代码哈:

/* 定义一个参数,并且实现reset逻辑 */
struct MyParam : public GParam {
    void reset() override {
        val = 0;
        str = "hello, world";
    }

    int val = 0;
    std::string str = "hello, world";
};

/* 实现包含参数传递功能的节点 */
class MyNode : public GNode {
public:
    CStatus init() override {
        CStatus status = createGParam<MyParam>("test");    // 创建一个参数
        return status;
    }

    CStatus run() override {
        auto param = getGParam<MyParam>("test");
        std::string result = param->str + std::to_string(param->val);
        printf("[%s]", result.c_str());
        return CStatus();
    }
};

int main() {
    /* 实现编排调度逻辑 */
    GPipelinePtr pipeline = GPipelineFactory::create();
    GElementPtr node = nullptr;

    pipeline->registerGElement<MyNode>(&node);
    pipeline->process();
    GPipelineFactory::clear();
    return 0;
}

虽然代码也不复杂哈,但就为了兼容这三行代码的祖传逻辑,必须在框架中增加了两个类——而且这两个类,在今后的开发中应该是不会被用到的。更要命的是,如果你不做这种改造,老功能是无法融入新框架的。

有人可能会想,我在其他已经定义好的GParam里加两个变量,然后再把功能加到一个已有节点中,再加一个变量来区分要不要执行。这样的话,就不用加新的类了。卧槽,这样下去,功能节点就不是最细粒度逻辑,而且祖传代码就是这样生成的。

解决方法

针对这种情况,CGraph提供了算子和函数混合编排的实现方式,从而方便一些零散功能的快速接入

实现的思路也比较简单,主要就是根据通过外部传入函数和参数的方式,在内部自动生成并注册对应功能的GNode类,然后根据设定的逻辑执行即可。lambda表达式,本身支持本地参数的注册传递,从而极大的简化了参数来回传的问题。

这就要用到CGraph工程里,GAdapter中的GFunction的功能了。GFunction也包含三个函数init/run/deinit,这三个函数均可从外部注入,并且支持任意类型信息的捕获。

接下来,我们来改造上面那段代码:

int main() {
    GPipelinePtr pipeline = GPipelineFactory::create();
    GElementPtr node = nullptr;

    int val = 0;
    std::string str = "hello, world";
    pipeline->registerGElement<GFunction>(&node);    // 注册GFunction类型的节点

    // 设置node节点的run方法,从外部传入val和str值
    ((GFunctionPtr)node)->setFunction(GFunctionType::RUN, [val, str] {
        std::string result = str + std::to_string(val);
        printf("%s", result.c_str());
        return CStatus();
    });    

    pipeline->process();
    GPipelineFactory::clear();
    return 0;
}

看上面这段代码,实现了跟之前完全一样的功能,仅有之前一半的行数,且不需要专门为CGraph引入新的GParamGNode类。

image.png


当然,除了run函数,还可以通过注入的方式,实现init和deinit函数功能。还可以将包含这个功能的GFunction类,通过添加pipeline编排的方式,放到任意位置执行——就像GNode一样。

数据传递

以上,我们简化了老逻辑移植和编排入CGraph框架的问题。还有一个问题,在新的工程中,老的逻辑很可能要获取其他的新模块的参数值,并且做自己的处理。

image.png

直接看图吧:lambda表达式,还可以将GFunctionPtr自身通过捕获的方式,传入表达式中,从而通过GFunction对象(GElement对象的子类)来读/写整个pipeline中的信息。当然,还可以通过GFunction创建新的参数什么的,用起来跟普通的GNode是一致的。

这样,就完成了新老功能的跨时代的对接。很适合传说中的 先实现基本功能,其他的后期优化 的场景——这话是不是很耳熟。


本章小结

本章内容的思路,源于我平日工作中遇到的实实在在的问题。老代码移植、逻辑兼容性等,从来都是项目开发和维护过程中,比较让人头疼的问题。上游的需求一个比一个诡异,很可能跟刚最初的架构设计格格不入。再有行业经验的程序员,也很难在刚开始的时候,做到一步到位。

图化、算子化的思路,可以很好的帮助我们抽象业务、编排逻辑、形成体系、便于后期维护和再移植。另一方面,图框架的体系强一致性,对于这种三行代码的细碎逻辑的算子,很可能算子的框架生成代码量,远多于功能代码——这显得有些小题大做了。

为此,我们在CGraph中设计了函数注入的方式,方便引入这种细碎逻辑。虽然这可能在一定程度上,破坏了整体架构的一致性,但的确为功能移植和接入,带来了很大的便利。

曾经听一位大佬在演讲中,提到了他的经验:在软件设计领域,no silver bullets, everything is a trade off——没有银弹,所有的设计都是一种权衡。对,这以上内容就是my trade off

以下是我的个人微信,欢迎添加好友,以便今后随时交流。顺便提一句,上图中的IDE是CLion开启Zen模式后的效果,配合上我精心挑选的壁纸,效果非常震撼——简直无心撸码了。

image

    					[2022.01.23 by Chunel]

推荐阅读


个人信息

微信: ChunelFeng
邮箱: chunel@foxmail.com
个人网站:www.chunel.cn
github地址: https://github.com/ChunelFeng

image