👉腾小云导读
从探索模型驱动开发开始,我一直在思考一个问题:“软件,是否可以用更简单、更人性化的方式生成”,ChatGPT 给我了一个肯定的回答。
我们此前根据领域模型在生成代码方面进行了一些探索,希望用建模时间高倍率置换编码时间。随着代码工具的不断完善,效率提升越来越难,因为模型是抽象的而实现是具体的,模型所承载的信息并不足以直接生成代码,一定需要“人”来补充信息,这部分工作工具无法替“人”来完成。
直到体验了 ChatGPT,在震惊于它强大的能力同时,我们也就“如何将 ChatGPT 引入我们的代码生成工具来提升研发效能”进行了思考,并且快速搭建了一些 Demo 验证效果。
“Talk is cheap. Show me the code”,先看效果:
视频中类图、分析序列图来源于 UMLChina
这里演示了工具基于领域模型生成代码的流程,在第3到5步工具集成了一个基于 ChatGPT 接口实现的插件,该插件自动提取模型中的中文类名、成员变量名、成员函数中文名,然后将中文名以及翻译用途、命名风格输入到 ChatGPT 得到翻译结果,并自动填充回工具,最后生成代码。
这里仅仅是简单地使用了 ChatGPT 的翻译能力,却给我们带来了巨大的提升,想象一下一个项目数十个类名、数百个成员变量名以及函数名需要根据中文翻译为英文,有些词还要使用翻译软件翻译后再根据使用用途(类名使用名词或者名词短语、方法名使用动词)转换词性,然后调整为大驼峰或者下划线连接等风格,这是多么无趣和繁琐的工作,而现在只需要一键填充,然后做微小调整即可。
通过会话将代码上下文信息输入到 ChatGPT,它基于这些信息完善、编辑代码,例如 Copilot 插件就是该模式。测试发现 ChatGPT 生成代码片段的质量比较高且比较稳定。
该模式和模式一的区别是代码是“工具”将 ChatGPT 生成的“代码片段”进行组织,最终形成完整的软件。
将自然语言转换为 DSL ,然后基于 DSL 生成代码或者软件,这种模式和方案二的区别是 ChatGPT 不直接生成代码,代码是由工具根据 ChatGPT 生成的 DSL 生成。ChatGPT 生成 DSL 相对稳定,这种模式生成的代码质量相对前两种模式更加可靠。
|
|
读到这里也许会有疑问,这明显就是模式二和模式三的结合,为什么要分两次让 ChatGPT 生成代码呢?我下面用一个案例进行详细解释。
int 泊位::来车(){
// 1、取值班人员
排班(时间).取值班人员(值班人员序号);
if(失败){
打印日志
返回失败错误码
}
return 0
}
|
{
"return_type":"int",
"function_name":"ArriveCar",
"param":[],
"impl":[
{
"entity":"Scheduling",
"function":"GetShiftPersonnel",
"return_type":"int",
"param":[
{
"type":"int",
"name":"number"
}
]
}
]
}
事实上需要配置的信息远远不止这些,而且完成这些工作的知识都在“人脑”中,只能人来完成, 构建可以生成的代码的 DSL 并不简单。当然我们可以通过一些方法来减少人的工作:例如将填空题修改为选择题(大多数配置都是勾选操作而不用输入文字)、总结最佳实践添加默认选择项(例如成员变量默认不生成Get函数)等等,然而始终有一部分工作是繁琐、重复、低效且需要人来完成的,例如上述步骤中的中文根据使用情景不同翻译为不同词性、不同格式的英文单词。而这部分工作就需要引入 ChatGPT 来完成,由人来翻译500个中文词可能需要50分钟(10个/分钟),而让 ChatGPT 来翻译仅需要几秒钟。
经过第一步从模型提取信息、第二步将信息转换为生成代码所需要的 DSL ,这时候我们就可以生成代码了,下面是我们生成的代码目录的一个案例:
我们打开上述目录中的头文件、PROTO 文件不仅满意的点了点头。但当我们点开泊车类的 .cpp 文件见到下面内容不仅吐槽:生成的代码并不能直接运行!
// 来车
int ParkingSpace::ArriveCar() {
//// MDD-TAG-BEGIN:[flow][slot-ArriveCar][函数实现]
int ret = 0;
// 取值班人员
Scheduling scheduling (/*请填充参数*/);
ret = scheduling.GetShiftPersonnel(number);
if (ret != 0) {
LOG_VERR("--->>错误事件名<<---", ret, "GetShiftPersonnel_ERR");
return ret;
}
return ret;
//// MDD-TAG-END:[flow][slot-ArriveCar]
}
|
(图源网络)
上图中标红的部分“模型和引擎共同决定了应用的实现程度和扩展性”。本文不是讲领域建模的内容,因此建模知识这里不做讨论,我们重点讨论引擎的设计。补充一下系统的架构分层如下:
协议栈: 定义代码生成引擎输入的格式化结构,可以的话这个结构可以作为规范标准供各种低代码平台通用使用,当然如果能够做到这点就能解决掉低无代码平台的互联互通问题(难而正确的事),是对整个行业有利的事情。
代码生成引擎:对协议栈的实现,定义了代码生成的模版,将输入的数据进行处理后根据需求填入到不同的模板中,生成 C++、TS 等代码。
// 来车
int ParkingSpace::ArriveCar() {
//// MDD-TAG-BEGIN:[flow][slot-ArriveCar][函数实现]
int ret = 0;
// 取值班人员
Scheduling scheduling (time(nullptr));
ret = scheduling.GetAttendant(number_);
if (ret != 0) {
LOG_VERR("GetAttendant", ret, number_);
return ret;
}
return ret;
//// MDD-TAG-END:[flow][slot-ArriveCar]
}
我们评估如果按照这样的思路,实现代码生成工具可以节省90%以上的工作量,大约剩下的10%是如下工作:
代码走查:上面已经说了 ChatGPT 生成的代码只是相对稳定,因此生成的代码默认使用/*和*/包裹注释掉,必须经过研发确认代码正确且调整完毕后才可以删除注释投入使用。
代码完善:ChatGPT 无法生成100%的代码,例如上一个接口产生了一个中间结果缓存起来,该接口会使用,考虑到 ChatGPT 的性能、插件实现的复杂性、OPENAI 接口收费价格等因素,我们不可能把上一个接口的信息拼接到Prompt 。这部分逻辑研发直接编写代码所付出的成本远远低于使用 ChatGPT 所需成本。
单元测试:ChatGPT 写单元测试的质量远远超出我的想象,它会考虑边界条件等等各种因素,如果是针对基础库生成的测试用例代码几乎不需要修改就可以直接使用,但是如果是控制类等业务代码的测试用例却不能使用,例如上述接口中的 JScode 是小程序产生,毫无疑问 ChatGPT 无法构造出这样的参数,是 Mock 还是调用可测试性接口获取还需要研发根据实际情况做出调整。
无论如何,由于业务本身的复杂性,我们不可能寄希望于使用一个工具生成所有代码,一定有一部分代码是工具所无法完成的,依然需要人来参与。
但是这部分工作可以通过 Copilot 、IDE 插件等工具来辅助提效。
-End-
原创作者|邬俊杰
技术责编|张晋铭