上个月帮财务模块做代码走查,看一段从老系统迁移过来的计算程序。逻辑本身并不复杂,但我花了将近十五分钟才搞清楚它到底在算什么。原因很俗套:满屏的lv_、ls_、lt_、lw_,每个变量名都在告诉我"这是个变量""这是个结构""这是个内表",却没有一个告诉我"它存的是客户未清项"还是"订单行上的净价值"。那时候我就想,ABAP 开发圈里这套类型前缀的命名习惯,是不是该重新聊一聊了。
这篇东西我想讲的就是命名这件事,具体说是"用意图驱动的命名替换或改造传统类型前缀命名"。它不是一篇空谈命名的理论文章,而是会从 ABAP 语言特性、代码可读性的底层逻辑、真实重构案例、团队落地阻力几个角度展开,适合正在写 ABAP、维护老报表的开发者,也适合需要在团队里推动代码规范的组长或架构师。看完之后你能直接拿一套可操作的规则回去,把你自己的代码、你们模块的公共函数慢慢盘顺。
1. 为什么命名会成为可读性瓶颈
1.1 类型前缀的来龙去脉
ABAP 里的类型前缀,圈内通常叫匈牙利命名法。这名字其实是从微软那边传过来的,但 ABAP 开发者把它本地化得特别彻底。以至于很长一段时间里,lv_开头就是局部变量,ls_开头就是结构,lt_开头就是内表,lo_开头就是对象引用,lr_开头就是引用变量。你要是写一个变量不带头,老同事一眼就能看出来你是半路转行写 ABAP 的。
这套规则的初衷并不蠢。在 ABAP 开发的早期,IDE 没有现在这么智能,代码区域没有变量类型悬浮提示,调试器也没有方便的 Watch 面板。那时候你看到ls_so_item,不需要点进任何窗口,光靠名字就知道它是一个结构体,里面应该装的是 sales order 的某项数据。这种"看一眼就知道技术类别"的效率,在那个年代是真真切切存在的。
另外一个原因和 ABAP 语言本身的形态有关。ABAP 的代码很长,一个函数可能 500 行起步,一个报表从变量声明到WRITE输出隔着几屏代码。变量声明的位置和实际使用的位置距离很远,如果你不在名字里带上类型信息,翻到第 300 行看到一个customer_name,真不确定它到底是字符串还是结构体。所以,类型前缀在那个年代不只是习惯,它是代码可读性的一部分补偿机制。
1.2 前缀为什么开始抢戏
问题出在两条线上。第一条线是 IDE 和语言版本升级了,但很多团队的命名习惯没跟上。现在的 ABAP Development Tools 里鼠标悬停就能看到字段类型,调试器里自动列出当前作用域所有变量,DATA(...)这种内联声明也早就普及了。局部变量的类型对读者来说几乎是在使用位置上唾手可得的信息。这时候前缀再占据名字的前几个字符,属于重复信息,而且还有代价。
第二条线是 ABAP 的字长本来就紧张。ABAP 内部对象名最长 30 个字符,方法名、变量名、组件名共享这个配额。你给名字前面加lv_三个字符,加ls_三个字符,加lt_三个字符,真正留给业务含义的空间就少了 10%。碰到purchase_requisition_item这种二十多个字符的天然业务词,前缀一占,后面就只剩下缩写的余地了。
但我觉得最核心的问题还不是长度,而是"解码路径"变长了。读代码的人看到lt_open_orders这个名字,第一反应是先解码lt这个前缀,告诉自己"这是个内表",然后才去看后面的open_orders去理解业务含义。可是"这是个内表"这个信息,在你读代码的时候本来就不需要专门去记。你关心的是这个内表里装的是哪些数据、循环出来之后要拿它做什么。前缀把注意力引到了技术类别上,而不是业务意义上。当这种干扰大量出现时,看代码就会觉得累——就像一个人跟你说话,每句话前面都要加一句"我下面要说的是名词/动词/形容词",你很容易就被搞烦。
1.3 可读性的真正目标
所以我在团队里经常说一句话:可读性的目标,是让读者在最短时间内恢复你对业务模型的抽象。代码不是拿给机器看的,机器只要语法对就能跑;代码是拿给人看的,人需要在字里行间读出"这里在做什么业务判断"。
把目标定成"恢复业务模型",命名就有一个特别简单直接的判据:一个变量出现在某一行代码里时,读者能不能不查声明、不看上一屏,就说出它在这个场景里承担什么业务职责。lv_cnt不行,因为它没有业务含义;invoice_count是可以的;overdue_invoice_count更好,因为它在具体场景里带了判断条件,读者一看就懂这里统计的是什么东西。
有人听到这可能觉得我在否定匈牙利命名法。其实不是。我反对的不是"在名字里保留必要的信息",而是反对"把类型信息摆在业务信息前面"。真正合理的做法是把两者的优先级换一下:业务意图是第一位的,类型信息如果需要,再根据具体场景补在后面。比如overdue_invoices是个内表这件事,读者从后面的语境基本都能猜出来,根本不需要刻意把"内表"塞进名字里。
2. 意图驱动命名的核心规则
2.1 数据对象:业务概念优先
先说我建议的具体写法,再解释为什么。对于普通局部变量、方法和类的数据声明,我推荐下面的风格:
| 传统前缀写法 | 意图驱动写法 | 说明 |
|---|---|---|
lv_kunnr | sold_to_party | 直接写业务角色,而不是表的字段名 |
ls_so_item | sales_order_item | 单数名词表达一个结构体项 |
lt_so_item | sales_order_items | 复数名词表达一组数据 |
lv_netwr | order_net_value | 把缩写展开成完整业务词 |
lv_flag | is_credit_blocked | 用谓词性名字表达判断结果 |
lw_log_handle | application_log_handle | 去掉无意义技术括号 |
这里面有一个关键想法:结构体强调的是"一个业务对象",内表强调的是"一批业务对象"。英文里单复数本身就具备这层表达能力,完全不需要ls和lt来额外标记。sales_order_item是一个对象,sales_order_items是一堆对象,读者从脑内英语的语感就能自动建立"一个是单条、一个是集合"的直觉,这套映射比"ls 和 lt 分别对应什么"自然得多。
字段符号也同理。ABAP 里 ASSIGNING 的换符号必须带尖括号,这是语法强制,不是命名问题。但尖括号内部的名字可以不用FIELD-SYMBOL(<fs_xxx>)这种前缀式写法,直接用<sales_order_item>就行。语法上尖括号已经提供了"这是字段符号"的识别线索,名字里再重复一遍,就只剩浪费。
2.2 方法与接口:让调用点像一句话
方法命名比变量命名更容易被忽略,但它的可读性收益其实更大。因为方法是跨越类、函数组的边界在传递,读者看调用点的时候,往往没有上下文来辅助理解,只有方法名这一条通道。
我常用的方法是把调用点默认为"一句自然语言的祈使句":动词加业务对象。比如get_customer_open_invoices、post_goods_receipt、release_purchase_order、calculate_tax_amount。如果要表达条件判断,用is_或has_开头:is_order_fully_delivered、has_active_block。ABAP 老代码里常见的Z_GET_OPEN_INV这种缩写式命名,我建议尽量换掉,缩写省不了几个字符,却要读者回去猜"INV 是 invoice 还是 inventory"。
方法参数上也一样。我见过很多命名规范要求 IMPORTING 参数带I_前缀,导出带E_,改换带C_。这在某些大公司 PRG 规范里是硬性要求,我不否认它有历史合理性,但如果你问我个人建议,我会说:接口参数名跟着调用语义走,不要带头。用IMPORTING iv_customer_id不如直接用IMPORTING customer_id。原因很直接,开发工具和方法签名里已经声明了IMPORTING,读者本来就知道它是输入参数,再在名字里写一遍iv_,属于重复信息。
2.3 常量、类型与选择屏幕怎么处理
常量是命名里比较特殊的一类。传统写法喜欢用c_前缀,比如c_active、c_closed。我建议改成业务语义更完整的大写词,比如status_active、status_closed、max_retry_count。ABAP 里常量名的字母大小写不影响语法,但你可以在命名上约定:常量用大写或首字母大写区分于普通变量。更重要的其实是让常量表达出"它是什么状态/阈值",而不是单纯"它是个常量"。
类型定义TYPES的处境不太一样。一个类型往往要跨多个方法传递,有时还要作为方法参数类型引用,这时候在类型名里保留一个技术标记确实有意义。我建议保留一个短的_t后缀,比如sales_order_item_t,用来表达"这是一个表类型";单个结构类型可以直接用业务词,比如sales_order_item,或者加_s后缀看你团队习惯。这里我比较务实地保留了一个后缀,因为类型的"可复用性"比局部变量强得多,名字里透露一点类型类别,对调用方法时理解接口定义的帮助更大。
选择屏幕上的变量是另一类很顽固的地方。很多人给选择屏幕变量命名时直接沿用内部表字段名,比如SO_BUKRS、SO_KUNNR,导致 READ 出来之后还要再找个变量去接。我的习惯是选择屏幕变量也走业务语义,比如sold_to_party、company_codes。然后在代码里sold_to_party = so_kunnr这种转换就没有了,直接拿业务变量用。
2.4 ABAP 特殊符号,哪些该留哪些该去
这里必须承认,ABAP 不是一门能让你完全靠语义行走天下的语言。它有一些语法层面的强制符号,不属于命名讨论范畴,比如字段符号的<>、引用变量的->、内表的[]。真正值得讨论的只有"可选的部分"。
一是对象引用的命名。类属性的对象引用,老规范通常要求mo_前缀,比如mo_controller、mo_model。如果你团队的代码里到处都是mo_、ro_这种约定,我建议至少把它们限制在真正的对象引用类型上,同时把业务部分往后挪。比如mo_application_logger改成logger或者application_logger。理由还是那句:类属性的上下文已经说明它是当前类的存储器,类型信息在声明处有,使用处 hover 也能看到,不需要处处标记。
二是内联声明。ABAP 740 以后DATA(...)很普及,内联变量往往只在一小段代码块里使用,生命周期短,作用域小。这种变量过去被冠以lv_前缀显得尤其别扭,因为读者只在很近的位置看到它,业务含义几乎不需要额外解码。内联声明配合意图驱动的名字,才会让代码真的向自然语言靠拢。
三是从数据字典里带出来的工作区。比如SELECT-OPTIONS s_kunnr FOR kunnr,可能你会顺手生成kunnr这个变量。这里我要特别提醒:字典字段名是数据模型层面的命名,它遵循的是 ERP 数据模型的缩写体系,不是说它不好,而是当你把它作为业务对象使用时,最好给它一个业务角色名。kunnr换成sold_to_party,读代码的人会更清楚这个字段在这里指的是"售达方"而不是"收达方"。
3. 从老代码开始的一轮真实重构
3.1 改造前:一眼望去全是 lv_、lt_、ls_
理论说太多容易飘,我拿一段真实场景简化的代码来说。假想一个函数,它的任务是找出所有已经逾期未交货的销售订单,然后把订单号、合同号、逾期天数整理成一张列表输出。传统写法大致长这样:
DATA: lt_vbeln TYPE STANDARD TABLE OF vbeln, ls_vbeln TYPE vbeln, lt_vbak TYPE STANDARD TABLE OF vbak, ls_vbak TYPE vbak, lv_delivery_delay_days TYPE i, lv_netwr TYPE netwr, lv_kunnr TYPE kunnr. SELECT vbeln FROM vbak INTO TABLE lt_vbeln WHERE vbeln IN s_vbeln. LOOP AT lt_vbeln INTO ls_vbeln. SELECT SINGLE vbeln, kunnr, netwr FROM vbak INTO (ls_vbak-vbeln, lv_kunnr, lv_netwr) WHERE vbeln = ls_vbeln-vbeln. " 下面还有 50 多行判断交期的逻辑,全部用 lv_ 系列变量 ENDLOOP.这段代码你让我评,最大的问题不是语法或性能,而是变量名一直在强调"我是局部变量、我是内表、我是结构",却完全没告诉我这份数据代表什么。lt_vbak和ls_vbak用了 5 个字符来表达"内表和结构",然后vbak这 4 个字符只是把表名复制了一遍。读者如果不懂vbak在 SD 模块里是什么表,这个名字基本等于没有信息量。
3.2 改造后:语义浮出水面
用意图驱动的方式重写一遍,核心就变了。首先是变量名全部换成业务词,其次是能内联就内联,最后是中间状态变量尽量少。改造后大致长这样:
SELECT vbeln INTO TABLE @DATA(open_sales_orders) FROM vbak WHERE vbeln IN @s_vbeln. LOOP AT open_sales_orders INTO DATA(open_order). SELECT SINGLE vbeln, kunnr, netwr INTO (DATA(order_number), DATA(sold_to_party), DATA(order_net_value)) FROM vbak WHERE vbeln = @open_order-vbeln. DATA(delivery_delay_days) = calculate_delivery_delay_days( order_number = order_number ). IF delivery_delay_days > 0. APPEND VALUE #( order_number = order_number sold_to_party = sold_to_party order_net_value = order_net_value delay_days = delivery_delay_days ) TO overdue_order_list. ENDIF. ENDLOOP.看到区别没有?改造后变量名在使用位置上直接表达业务含义,open_sales_orders、sold_to_party、order_net_value这些名字读下来,代码本身就像一段带有描述性的文字。读者不再需要先翻译"lv_netwr是净价值",因为order_net_value已经告诉你了。语法层面还用了@转义和内联DATA(...),把变量声明和使用放在同一视野内,减少了上翻下翻的次数。
有人会担心SELECT SINGLE里直接DATA(order_number)这种写法影响性能,我实测过,内联声明在运行时没有任何性能差异,纯粹是编译期的语法糖。真正要关注性能的,是别在一个LOOP里发大量SELECT SINGLE,那是另一个维度的问题,跟命名无关,但很多人容易混在一起讨论。
3.3 重构的落地顺序与操作细节
这轮重构看起来简单,真动起手来需要有顺序,不然中途会改乱。我自己操作时一般分四步。
第一步,先做业务梳理。把你正在处理的这一小段业务逻辑写成一句自然语言。上面那个例子就是"找逾期的销售订单,记录它逾期多少天"。然后从这句话里圈出名词:订单、客户、净价值、逾期天数。这些名词就是你变量名的候选。
第二步,把变量按"核心业务对象"和"中间临时缓冲"两个层次分开。核心业务对象指的是要输出到列表或参与最终判断的数据,比如overdue_order_list、delivery_delay_days。中间临时缓冲就是那些只用来搬运数据的结构,比如传统写法里通篇都是ls_xxx的结构。能消除的中间缓冲,用内联和字面量 APPEND 消除;不能消除的,也不要给它瞎起名,直接叫它业务对象本身。
第三步,做方法提取。如果一段逻辑超过 15 行,并且职责相对独立,就把它抽成一个私有方法,方法名用"动词+业务对象"的结构。我这里就抽了calculate_delivery_delay_days,把计算逻辑和主流程分开了。这样主流程变成一组清晰的调用序列,读者跟着方法名走就行,不需要看每一行细节。
第四步,把自己代入成新读者,把重构后的代码从头到尾读一遍,遇到任何一个名字要看第二眼才能反应过来的,立刻回头改。这一步是最费时间的,但也是效果最明显的。改的时候要注意,变量改名不要顺手改业务逻辑,两者要分开提交,否则出了 bug 说不清楚是命名问题还是行为变化。
4. 常见问题与排查技巧实录
4.1 "公司规范不让改"怎么办
这是我在交流时被问最多的问题。很多 ABAP 开发并不是想怎么命名就怎么命名,企业里往往有一份格式规范,规定变量必须带lv_前缀,或者方法参数必须带i_、e_前缀。这种规范年限越长,积重越深,想推动改动很难。
我的建议不是正面硬刚,而是从增量入手。新开发的函数、新增的私有方法、新创建的处理类,在这些全新的代码里率先采用意图驱动命名。你不需要把存量 10 万行的代码一下子都改掉,只需要做到"新代码不再制造新的读代码负担"。等团队里积累了一定量"读起来很流畅"的新代码后,在代码走查会上借这些例子谈收益,比拿一份 PPT 讲理论有效得多。
如果规范确实以文档形式锁死了lv_、lt_前缀,还有一些折中办法。比如把前缀保留,但把后面的名词换成完整业务词:lv_kunnr改成lv_sold_to_party,lt_vbak改成lt_open_orders。这样虽然前缀还在,但业务语义已经比原来好了太多。我再补一句:规范不是刻在石头上的,它服务于代码维护。拿一次真实走过的 bug——因为lv_a和lv_a1看混导致数据串号——去跟规范制定者沟通,通常比抽象讲"可读性"更有说服力。
4.2 搜索与 Where-Used 的常见误区
有人担心去掉前缀后,变量名变成customer、orders这种常见英文词,在 ABAP 开发环境里做全文搜索会命中一大堆无关结果,反而不方便定位。这里我要泼一点冷水:这种担心多数是把搜索引擎习惯套在 ABAP 工具上。
ABAP 的代码搜索和 Where-Used 列表是按对象、按名称精确匹配的。你要找一个变量在哪些地方被使用,直接右键跳转到使用列表,配合名称过滤,就算它叫customer,结果里也不会有多少干扰项。反而是lv_customer这种带前缀的名字,你去搜业务含义时还得先在脑子里去掉lv_,再拆出customer,多一步转换不说,搜出来的还全是"局部变量级的 customer",跟类属性、方法参数里的同一概念完全对不上。
真正影响搜索体验的是缩写不一致。同样是"客户",有人写kunnr、有人写customer、有人写cust。从信息检索的角度讲,统一的业务术语表比统一前缀重要得多。如果团队有能力,建议维护一份 ABAP 命名术语表,规定哪些业务概念用哪些标准词。这比纠结一个lv_前缀值得多。
4.3 30 字符长度限制下如何缩写
ABAP 对象名最长 30 个字符,这个限制在意图驱动命名下反而容易被触到。像purchase_requisition_approval_documents已经 36 个字符了,超了。那我的建议是:先保主干语义,再考虑可识别性,最后才是词典完整拼写。
缩写也不是随意乱缩,要从业务领域内已有的习惯里去取。比如采购领域purchase requisition大家默认写pr或purch_req,销售订单sales order默认写so。用这些已经被业务认同的缩写,不会给读者增加解码负担。反过来,你自己发明pch_req_apprv_doc这种缩写,就是给读者添堵。
再看一个例子:purchase_order_confirmation_number是 33 个字符,超了。根据领域习惯缩写成po_confirmation_no,依然保留完整语义。如果你连confirmation都嫌长,po_conf_no在业务语言里也基本没歧义。关键在于"缩写后的词在领域内部有公共理解",而不是"能省则省"。
4.4 命名意识比命名规范更值钱
最后想提醒大家一个容易掉进去的坑:把意图驱动命名当成一套静态检查规则去做,结果会流于形式。
静态检查器可以检查"变量名是不是以lv_开头",但它检查不出"lv_a这个变量有没有业务含义"。可读性本质上是关于语义的,不是关于字符模式的。我见过有的项目把lv_前缀统一改掉之后,新名字变成var1、var2、var3,这比原来还难读。意图驱动命名的核心,是每次起名时都回到那个问题:这个名字在告诉读者业务上的什么事实。
所以落地时,比起在编码规范文档里加一条"禁止使用类型前缀",更有效的是在代码走查时对具体名字提问:这个变量是干嘛的?它为什么叫这个名字?它在这个方法里的职责边界是什么?这些问题会让写代码的人建立起命名意识,而有命名意识的人,无论拿到一套什么样的规范,都会倾向于把业务语义放到名字的首要位置。规范是下限,意识才是上限。
我自己改完几个程序之后有个很直接的感受:当变量名开始表达业务,代码走查的讨论重心会从"这个变量类型对不对"自动转移到"这个业务逻辑是否合理"上。这个转变,可能比命名本身带来的收益还要大。