大家好,我是祯民。很长时间没有更文了,这段期间一直在忙业务和 AIGC 技术建设上的事情,遇到一个很值得和大家分享的改造,内容不复杂但很有借鉴意义,为便于大家理解,全文会尽量以讲故事和类比的形式来介绍。废话不多说我们直接进入正题。
背景 - AIGC 的法务要求
事情的起因是这样的,最近 AIGC 对各行各业都产生了一些影响,很多团队都在尝试将业务与大模型融合,我也在负责一些和这个相关的技术建设,比如说 AI 自动生成单元测试,AI 辅助搭建 eslint plugin 之类的,载体都是以 vscode 插件的形式。
因为模型本身服务并不是部署在中国,所以出于对代码安全的保护,法务同学对我的技术建设就有了一些要求和限制,比如仓库密级要达到要求,敏感词过滤,人员加白等等,经过这些加白确认通过的仓库和同学才能正常使用插件。
为了能支持法务同学的这个需求,所以之前我定义了一个加白类,简单来说可以理解成下面的伪代码
typescript
class Limit {
// ... 一些变量
async validateAvailable() {
// ... 校验入口
}
// ... 其他的一些不会直接调用的 private 函数
}
这个 Limit 类会向外暴露一个异步的 validateAvailable
函数,然后在需要使用的场景,我们需要调用它并且得到通过后才能进行下一步的操作,类似下面的调用
typescript
const generateUnitTest = vscode.commands.registerCommand(
'chatgpt---ui-unit-test.generate-unit-test',
uri => {
const filePath = `/${uri.path.substring(1)}`;
const limit = new Limit();
limit.validateAvailable().then(() => {
// 做你本来要做的事情
});
}
);
这个过程用图来表示就是下面的样子
导火索 - 多个应用方需要接入 & 政令可能频繁产生 breaking change
这个实现其实没啥问题,因为之前我只有一个功能类,所以包一层就实现了,很方便代码看上去也还行。但是随着功能的增加,比如后续我可能还要支持单元测试的校验,解释等功能,这些功能也需要加白,那么就都需要进行上面的那一步操作。
除繁琐外,这个流程其实还存在一个很大的问题,因为插件类还是插件的直接下游,这个关系就像政府机构(法务)下发相关政令要求到公司(插件),公司(插件)为落实政策,开发了一个 sdk 要求每个个体(插件类)按规定接入,看上去是不是还 ok?
但是实际上,这个公司(插件)并没有一个传达机制,当这个政令(法务)改变的时候,有可能某天 limit 类进行了 breaking change,不再暴露这个函数,或者需要更多的函数来辅助限制时,插件需要对每个引用了这个类的逻辑都进行统一的修改,改造起来其实是很低效的,而且容易遗漏。
应用方(插件类)表示原来我才是小丑,扔了一个 sdk 就甩手掌柜了~
担起责任 - 提供工厂来加工产品
在这个场景下,比起这样的实现我们更适合使用工厂模式,因为所有的插件类都需要接入加白逻辑,加白逻辑就像是一个工厂,产品进入工厂后,才能真正落地销售。
相比 sdk 的模式,工厂模式有几个好处:
- 调用上更加简洁明了,应用方不需要关注我应该调用加白类中的哪个函数,做什么样的操作,他需要的只有将源码类投入工厂中,然后使用工厂加工出来的新类即可。
- 后续如果法务政令变了,不需要应用方来接入改造,只需要工厂本身调整流程,在生产产品的过程中影响流水线即可
到这里大家应该已经理解工厂模式的种种好处了,我们来看看怎么对进行改造
typescript
export default class ValidateFactory {
private productClass;
constructor(productClass: any) {
this.productClass = productClass;
}
async create(...args: any[]>): Promise<any[]> {
let productObj: any = null;
const available = await this.validateAvailable();
if (available) {
productObj = new this.productClass(...args);
}
return productObj;
}
// ... 其他校验逻辑和函数
}
在上面的例子中,我们提供了一个 create 函数,这里面我们会完成所有的加白逻辑,如果校验通过,我们将根据外部透传进来的类来创建一个对象进行返回,这样就能做到所有的加白逻辑都收敛到加白类中了。
调用的话也非常简单,如下例子
typescript
const generateUnitTest = vscode.commands.registerCommand(
'chatgpt---ui-unit-test.generate-unit-test',
uri => {
const filePath = `/${uri.path.substring(1)}`;
const generateTestFactory = new ValidateFactory<typeof GenerateUnitTest>(GenerateUnitTest);
generateTestFactory.create().then((generateTestProduct) => {
if (generateTestProduct) {
generateTestProduct.generateUnitTest(filePath);
}
});
}
);
但到这里还没有完,因为我们的类型都没有定义,所以得到的 generateTestProduct
是没有类型提示的,我们不知道工厂吐出来给我们的是啥,也不知道该怎么用它,除非我们深入工厂内部,了解工厂到底是怎么做的。
说白了,我们他喵的买了三无产品!
类型编程 - 坚决抵制三无产品
对类型编程还不熟悉的同学可以看看 《一文吃透 TypeScript 类型编程(含精选类型体操编程题)》
三无产品那是绝对不能忍的,万一哪一天生产的是清洁剂,给当肥宅快乐水喝了那不是完了~我们来梳理一下这个工厂函数中类型的关系:
productClass
是一个类create
的入参需要匹配productClass
的构造函数入参类型create
的返回需要匹配productClass
的构造函数返回类型
梳理到这里就相当简单了,我们来看看最终成品
typescript
type ConstructorInstanceType<T extends new (...args: any[]) => any> =
T extends new (...args: any[]) => infer R ? R : any;
// 白名单工厂类
export default class ValidateFactory<T extends new (...args: any[]) => any> {
private productClass;
constructor(productClass: T) {
this.productClass = productClass;
}
async create(...args: ConstructorParameters<T>): Promise<ConstructorInstanceType<T> | null> {
let productObj: ConstructorInstanceType<T> | null = null;
const available = await this.validateAvailable();
if (available) {
productObj = new this.productClass(...args);
}
return productObj;
}
// ... 其他校验函数
}
效果也是相当哇塞,终于不再是三无产品了,使用过程中也会有相关的提示
小结
到这里这个故事就讲完了,工作本身虽然是螺丝钉,但是愿意去思考发现的话,还是会有很多可以优化,苦中作乐的事情的。有问题的同学,也欢迎在评论区提问交流~