当前位置: 首页 > 科技观察

不要发明一个低代码的DSL以DRY

时间:2023-03-12 20:03:08 科技观察

名词的名义杀死你的同事解释一下标题的意思。DRY的意思是Dont'RepeatYourself,即当你看到重复的代码时,你必须消除重复。例如,我们发现增删改查这四个操作有共同的参数。我们只需要定义一次就可以从django.dbimportmodelsclassAuthor(models.Model):name=models.CharField(max_length=100)title=models.CharField(max_length=3)birth_date得到四个接口和完整的前后端行为=模型。DateField(blank=True,null=True)fromdjango.contribimportadminclassAuthorAdmin(admin.ModelAdmin):exclude=('birth_date',)这样的东西有时被称为low-code,有时被称为DSL。用过django的同学都知道,上面的代码就是DjangoAdmin。这些工具的特点是80%的功能可以在1%的时间内完成。当我第一次开始使用它们时,我称之为未来。然而NealFord发现了“#last10%规则”,即最后10%的代价很大,用户总是想要100%的功能。这是为什么?不假思索的复制粘贴代码,一个文件写1000行,是不是好程序员的特征?我们有理想有追求,有错吗?理想没有错,只是方法错了。别误会,我不站在你这边。我绝对是一个铁杆语法糖制造商。只是有时候,人们需要跳出原有的思维习惯,才能意识到认知的盲点。误区一:一厢情愿的抽象我在《代码写得不好,不要总觉得是自己抽象得不好》已经说过了。业务逻辑,界面外观,很多时候都是产品经理说了算。说你们程序员只是产品经理手中的钢笔是不恰当的。这虽然让人难以接受,但确实是大多数人真实的日常生活。当我们看到两个地方相似时。不要一厢情愿地提取公共代码并消除重复。首先要和产品经理达成一致,在业务上要一致。当一个产品有一群产品经理时,他们或他们之间往往想法不一致,同一个列表过滤功能可能想出各种各样的方法。这时候就需要借助UI设计师等角色进行水平对齐。总之,首先要压住需求源头。而不是需求的下游,使用可重用的抽象代码来覆盖底线。这就是为什么《领域驱动开发》需要客户、产品经理、程序员多沟通,多形成共识的原因。误区二:在调用栈上找不到自己的代码。很多人会把像DjangoAdmin这样的CRUD代码生成归结为代码生成。但其实问题不在于生成代码,问题在于“在调用栈上找不到自己的代码”:当我们看到抛出异常,然后逐行查找在堆栈跟踪中,我们找不到我们编写的代码。代码。为什么会出现这样的现象呢?假设一开始我们写了三个方法import{f1_impl,f2_impl,f3_impl}from'some-lib';functionf1(){f1_impl(arg1,arg2);}functionf2(){f2_impl(arg1,arg3);}functionf3(){f3_impl(arg1,arg4);}我们可以看出用户需要写三个方法,f1/f2/f3这是代码量。而且每个地方都要重复传参数arg1。那么我们应该使用DRY的名字来简化代码import{f1_impl,f2_impl,f3_impl}from'some-lib';consttheModel={arg1,arg2,arg3,arg4}functionf1(){f1_impl(theModel.arg1,theModel.arg2);}functionf2(){f2_impl(theModel.arg1,theModel.arg3);}functionf3(){f3_impl(theModel.arg1,theModel.arg4);}这里我们提取一个公共的全局theModel来定义所有参数。那么f1、f2、f3的行为都是模型驱动的。所以看起来用户不需要写f1/f2/f3,他们只需要这样写:具体执行起来。以后如果技术需要升级,只需要升级框架,业务逻辑不用动。这种“让用户在调用栈上找不到自己的代码”的缺点是什么?很容易找不到它影响的配置项、参数。可以在框架代码中的任何地方。在编写代码的位置和实际生成行为的位置之间没有可以在编译时跟踪的符号依赖关系。当然,这是所有可变数据的问题。写到哪里,读到哪里,对读到的地方有什么影响,都是未知数。程序员的日常就是搞这些幺蛾子。当然,处理这样的问题是非常熟悉的。但这并不意味着它是免费的。这样的间接性越多,代码就越难阅读。这里还有第二个问题,模型包含f1、f2、f3参数的集合。当所有的参数被压扁混合在一起时,虽然可以去掉arg1等重复的参数,但是也让哪个参数是给谁用的更加模糊。比如classArticleAdmin(admin.ModelAdmin):prepopulated_fields={"slug":("title",)}当我们读到上面的定义时,prepopulated_fields会影响在哪个接口上对哪些字段进行增删改查,以及如何影响?有的同学可能会说Antd有几十个组件参数。但是当我们看到这样一个React组件的调用代码return时,我们可以点进EditorForm的代码,看看EditorForm的实现。可以查到prepopulatedFields传进来的参数用在什么地方。但是当代码写成像DjangoAdmin这样的声明式风格时,你就不能很容易的找到去哪里读取prepopulated_fields。最大的可能就是开始做全局文本搜索框架的代码。DSL作者不认为这是一个问题。每个参数都是他们自己亲手添加的,在DSL发明后的三个月内,他们不需要查看文档(如果有文档的话)。但他们的同事就没那么幸运了。误区三:在意想不到的地方修改语言的默认行为,尤其是赋值和取值这两个操作。例如,下面的代码functiondoSomething(a){a.b='hello';console.log(a.b);}控制台输出什么?应该是你好吧?那么,如果传入的a是这样的呢?functiondoSomething(a){a.b='hello';console.log(a.b);}consta={};Object.defineProperty(a,'b',{value:'world'});doSomething(a);这次控制台输出的是world而不是hello。C++拷贝构造函数,隐式转换构造函数。还有一个类似的问题。将行为潜入赋值语法。也就是框架代码,DSL的实现,他们可以通过魔法修改赋值操作和值操作。大多数程序员很难意识到一行平凡的代码可以做非凡的事情。这样就会导致出现问题的时候,真正导致问题的地方被跳过了,因为那个地方可能无非就是一行值操作,或者一行赋值操作。把代码写好,而不是耍花招。再使用前,先与产品经理或客户达成共识。别想着省那么多代码。当需要一个接口时,定义一个接口。当需要后端API时,您可以定义一个API。哪怕只是一行代码。让用户调用库而不是定义一个框架并要求用户提供一堆配置。不要修改赋值和检索的行为,不要制造意外。不要以DRY的名义做任何事情。可能出于其他动机,结果是DRY。但是不要以DRY为起点做任何事情。谨借这篇文章,对自己过去所犯的一些错误提出批评,以示警示。