大家好,我是不会写代码的纯序员——Chunel Feng。在日常开发过程中,经常会涉及到迁移框架、重写逻辑等工作。新的框架兴许能解决之前一些功能的瓶颈和缺陷,但随之而来的是祖传代码和新型框架不兼容、难适配等各种各样奇怪的问题。不要问我是怎么知道的,说多了都是泪。
就拿传统流程型框架迁移到图框架来说吧,图框架可以通过上游的编排调度,很好的解决之前流程中依赖和并发的问题,但是一般都需要大家根据特定的规则来实现节点逻辑——也就是说,需要将原来的逻辑,分拆到特定的节点的特定位置——毕竟,我从来没有见过没有任何约束的图开发。
如果涉及到参数传递,还需要大家根据特定的方法,将原有的参数规整和拆分。这牵扯出一个新问题,原来代码中,各种参数都是随着接口定义而散乱在程序的各个位置,哪有这么容易统一生成、统一获取?
这个时候我们往往会想:如果有这样一种方法,能满足新功能按新框架的方式统一编排,老功能“原地迁移”,一键融入新的框架,或者等之后有空了再做整理(下次一定),那该有多好啊。
今天我们来给大家讲讲在图框架中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引入新的GParam
和GNode
类。
当然,除了run函数,还可以通过注入的方式,实现init和deinit函数功能。还可以将包含这个功能的GFunction
类,通过添加pipeline编排的方式,放到任意位置执行——就像GNode
一样。
数据传递 |
以上,我们简化了老逻辑移植和编排入CGraph框架的问题。还有一个问题,在新的工程中,老的逻辑很可能要获取其他的新模块的参数值,并且做自己的处理。
直接看图吧:lambda表达式,还可以将GFunctionPtr自身通过捕获的方式,传入表达式中,从而通过GFunction
对象(GElement
对象的子类)来读/写整个pipeline中的信息。当然,还可以通过GFunction
创建新的参数什么的,用起来跟普通的GNode
是一致的。
这样,就完成了新老功能的跨时代的对接。很适合传说中的 先实现基本功能,其他的后期优化 的场景——这话是不是很耳熟。
本章小结 |
本章内容的思路,源于我平日工作中遇到的实实在在的问题。老代码移植、逻辑兼容性等,从来都是项目开发和维护过程中,比较让人头疼的问题。上游的需求一个比一个诡异,很可能跟刚最初的架构设计格格不入。再有行业经验的程序员,也很难在刚开始的时候,做到一步到位。
图化、算子化的思路,可以很好的帮助我们抽象业务、编排逻辑、形成体系、便于后期维护和再移植。另一方面,图框架的体系强一致性,对于这种三行代码的细碎逻辑的算子,很可能算子的框架生成代码量,远多于功能代码——这显得有些小题大做了。
为此,我们在CGraph中设计了函数注入的方式,方便引入这种细碎逻辑。虽然这可能在一定程度上,破坏了整体架构的一致性,但的确为功能移植和接入,带来了很大的便利。
曾经听一位大佬在演讲中,提到了他的经验:在软件设计领域,no silver bullets, everything is a trade off
——没有银弹,所有的设计都是一种权衡。对,这以上内容就是my trade off
。
以下是我的个人微信,欢迎添加好友,以便今后随时交流。顺便提一句,上图中的IDE是CLion开启Zen模式后的效果,配合上我精心挑选的壁纸,效果非常震撼——简直无心撸码了。
[2022.01.23 by Chunel]
推荐阅读
- 纯序员给你介绍图化框架的简单实现——执行逻辑
- 纯序员给你介绍图化框架的简单实现——循环逻辑
- 纯序员给你介绍图化框架的简单实现——参数传递
- 纯序员给你介绍图化框架的简单实现——条件判断
- 纯序员给你介绍图化框架的简单实现——面向切面
- 纯序员给你介绍图化框架的简单实现——线程池优化(一)
- 纯序员给你介绍图化框架的简单实现——线程池优化(二)
- 纯序员给你介绍图化框架的简单实现——线程池优化(三)
- 纯序员给你介绍图化框架的简单实现——线程池优化(四)
- 纯序员给你介绍图化框架的简单实现——线程池优化(五)
个人信息
微信: ChunelFeng
邮箱: chunel@foxmail.com
个人网站:www.chunel.cn
github地址: https://github.com/ChunelFeng