1. 先打破一个误解:老语法不等于老代码
前阵子帮一个 ECC 6.0 的 ABAP 项目做代码走查,讨论到 Clean Code 时,负责维护的同事摊手说:这代码全是历史遗留,反正我们版本也用不了新语法,Clean Code 那一套管不着。我走进那个流传多年的增强程序看了一下——三层嵌套 LOOP 打底,中间串了五个 IF 分支,变量叫 WA_IT1、FIELD2 这种名字。那一刻我意识到,很多团队并不是不想整洁,而是被"老版本做不到"这个念头给困住了。
我特别反感把"老版本"当成"烂代码"的免罪金牌。老 ABAP 缺的只是糖衣语法,它限制的是你必须怎么声明变量、怎么取数据、怎么拼接字符串,它从来没有限制你怎么命名、怎么拆结构、怎么控制每个方法的复杂度。换句话说:语法层面的现代化做不到,结构层面的整洁完全可以做到。
所以这篇想聊的,不是在老版本里假装 Clean Code,而是把那些真正能在 7.4 之前 ABAP 环境(常见的就是 ECC 6.0 这类老 NetWeaver 系统)里落地的规矩讲清楚,再讲讲怎么让这套规矩在一堆紧急修复、人员流动、传输审批里活下来,也就是做到"可持续"。文章里的代码我全部用老语法写,拿过来就能在 ECC 6.0 编译运行,适合正在维护老系统的开发、刚被派到老项目的 ABAP 新人,以及想给团队立规矩的技术负责人参考。
2. 认清老版本的语法边界,才知道底线画在哪
2.1 等不来的新特性:7.4 之前缺了什么
想把老版本的项目代码写整洁,第一步不是去学新规范,而是接受一个现实:有一批在现代 ABAP 里最常用的写法,在老环境里根本不存在。
最典型的就是内联声明DATA(...)、表表达式lt_items[ 1 ]、字符串模板|...|、构造操作符VALUE、NEW、CORRESPONDING #。这些是 7.40 才大规模出现的。很多老项目的系统还在这个版本之前,能用的语法组合自然更受限。
这意味着什么?意味着你在做代码设计时,不能把"反正最后可以一行写得很简洁"当成偷懒的借口。你写的每一行都必须以老语法能编译为底线,同时还要在这个底线之上追求可读性和可维护性。这个约束听起来是枷锁,但换个角度看,它反而逼着人去做更扎实的结构设计。
打一个比方:新语法像是给你一个带电动螺丝刀、激光测距仪的工具箱;老版本只有一把螺丝刀和一把卷尺。工具少,不代表你装不出一件像样的家具,只是要求你在每一步更在意顺序和精度。
2.2 没有内联声明时,变量区域就是"第一印象"
老语法下,方法里或 FORM 里的变量得先集中在 DATA 段声明,跑业务逻辑时看不到变量类型,也看不到初值。所以 DATA 区域写得好不好,直接决定读代码的人的第一印象。
我见过最糟的写法,就是把几十个变量堆成一大坨,不分组、不加注释,命名还都是 L_、W_ 加两个字母敷衍了事。而稍微花心思的做法,是给 DATA 区做分区,按照用途和来源把变量归拢,每个小组配一行简短注释。
DATA: lt_sel_text TYPE STANDARD TABLE OF tline, lt_config TYPE STANDARD TABLE OF tvarv, ls_config LIKE LINE OF lt_config, mv_num_text TYPE string, mv_is_numeric TYPE abap_bool. " 工作区集中放中间变量,不混业务表结构 FIELD-SYMBOLS: <ls_item> TYPE ty_item.这里有两个细节值得注意。第一,DEFINE宏能不用就不用,尤其别拿它当"伪局部方法",宏会让调用点失去上下文,排查问题的时候非常痛苦。第二,能用LIKE LINE OF的地方少写一个完整结构类型,将来表结构调整时,改动面会小很多。老版本没有内联声明,我们就用严谨的声明顺序来补偿这部分可读性损失。
2.3 哪些降级可以接受,哪些是披着兼容外衣的偷懒
《Clean ABAP》这类指南里被点名的很多坏味道,在老版本里其实有"历史原因"。比如 SELECT 不能内联定义工作区,只能先DATA: ls_vbak TYPE vbak.再SELECT SINGLE * INTO ls_vbak FROM vbak WHERE ...,这种降级是没办法,不算偷懒。
但有些妥协纯粹是懒。我整理过一张表,评审和自测时可以直接拿来对照:
| 场景 | 可接受的降级 | 不可接受的偷懒 |
|---|---|---|
| 查询取数 | 先声明工作区,再 SELECT INTO | SELECT * 把用不到的字段全搬回来 |
| 字符串处理 | CONCATENATE 加 RESPECTING BLANKS | 裸拼字符串,空格全靠运气 |
| 内表读取 | READ TABLE 后仔细检查 sy-subrc | 循环里反复 READ 同一张表 |
| 参数校验 | 用 CO / CN 判断后抛业务异常 | 不校验,等运行时 dump |
降级方案的核心原则只有一句话:能用老语法实现同等语义的,就该老老实实写;该做的参数校验和异常处理,不因为代码啰嗦就不做。
3. 我在老环境里死磕出来的四条"硬规矩"
3.1 方法拆到一屏看完,命名准到不用猜
老语法最大的问题是"信息密度低":同样一段逻辑,用 7.40 写可能十行,用老语法写要二十行。如果再不拆方法,一个五六十行的方法基本就是阅读灾难。
我的做法是:把方法体控制在 20 行左右,一个方法只说一件事。判断标准很简单——如果这个方法的开头要写一大段注释来解释"它到底想干什么",说明这个方法拆得不够细。方法名就是最好的注释。
举个重构例子。下面这段老代码在许多遗留程序里都很常见:
FORM build_output. LOOP AT gt_items INTO gs_item. IF gs_item-status = 'A'. gs_output-text = gs_item-material. CONCATENATE gs_output-text '(待确认)' INTO gs_output-text. gs_output-flag = 'X'. APPEND gs_output TO gt_output. ENDIF. ENDLOOP. ENDFORM.功能不复杂,但"待确认文本怎么拼"和"什么状态要输出"耦合在一起,将来状态变多,就只能继续往这个循环里堆 IF。拆完是这样:
FORM build_output. LOOP AT gt_items INTO gs_item WHERE status = mc_status_pending. PERFORM build_pending_line USING gs_item CHANGING gs_output. APPEND gs_output TO gt_output. ENDLOOP. ENDFORM. FORM build_pending_line USING ps_item TYPE ty_item CHANGING ps_output TYPE ty_output. ps_output-text = ps_item-material. CONCATENATE ps_output-text mc_suffix_pending INTO ps_output-text RESPECTING BLANKS. ps_output-flag = mc_flag_yes. ENDFORM.这样改完之后,LOOP 的过滤条件、文本拼接各自独立。下次要增加"已取消"的展示逻辑,只需要给循环加一个 WHERE 条件,再写一个独立 FORM,不会误伤原逻辑。命名上我坚持旧式的匈牙利前缀(LT_ 内表、LS_ 工作区、LV_ 普通变量、MT_ 类属性)。可能有人觉得啰嗦,但在老版本没有现代 IDE 智能提示的年代,前缀是唯一能快速判断变量类型的途径,这个传统不该丢。
3.2 内表处理固定套路:SORT 的稳定性陷阱
排序是 ABAP 里天天用的操作,但不少新手不知道一个坑:老版本里SORT的稳定性并不被保证。你按日期排完再按状态排,如果不额外加关键字,相同状态内到底谁在前,不同版本、不同数据量下结果可能不一样。
在比较老的系统里,我的建议是不要赌稳定性,排序键一定要写全。假设你要先按状态分组、再按创建时间倒序、再按凭证号兜底,那就一次性排到位:
SORT gt_alv BY status ASCENDING erdat DESCENDING vbeln ASCENDING.如果你的系统版本支持STABLE追加(这个追加不是所有老版本都有,确认一下你系统里的 ABAP 关键字帮助),在需要保持原有相对顺序的场景,就明确写:
SORT gt_alv STABLE BY status ASCENDING erdat DESCENDING vbeln ASCENDING.顺带说一句,数据进内表之后,能少排序就少排序;如果是从数据库读取,能用ORDER BY解决的问题不要在内表里再排一遍,代码能少写,性能也更稳。
3.3 READ / MODIFY 的读写姿势要统一
老版本没有表表达式,读取内表最常用的就是READ TABLE ... WITH KEY。常见的坏味道是在循环里反复 READ 同一张表,或者在IF sy-subrc <> 0之后不处理就继续往下跑,导致分支越来越深。
我的套路是把读取封装得"像查数据库一样简单",并且只搬需要的字段,不要整行搬:
READ TABLE lt_config INTO ls_config WITH KEY name = 'MAX_COUNT'. IF sy-subrc EQ 0. lv_max_count = ls_config-low. ENDIF.修改内表同样要统一。能用MODIFY FROM直接改的不要先 DELETE 再 INSERT;需要按条件批量改的,先LOOP AT ... ASSIGNING,然后针对具体字段做MODIFY ... TRANSPORTING,减少不必要的整行写操作。
更重要的习惯是:每次 READ 完,必须立刻检查sy-subrc,但不要只做一个空判断。要想清楚业务上"找不到记录"意味着什么——是继续、是跳过、还是抛一个明确异常。很多脏数据 Bug 就出在"READ 不成功但后续代码照样跑"。
3.4 类型判断和字符串处理:老写法也能写干净
再补一条老开发非常常见的需求:判断一个输入是不是数值。老版本里最常用的写法是:
IF lv_input CO '0123456789' AND lv_input IS NOT INITIAL.注意CO表示"仅由右操作数中的字符构成",空字符串也会满足CO,所以必须加上IS NOT INITIAL。如果要支持正负号和小数点,就把字符集扩展成'0123456789+-.',再配合格式校验。这种逻辑如果散落各处,就会变成一处处隐患,我建议把它收口成一个方法:
METHOD is_numeric_text. rv_result = boolc( iv_text CO '0123456789' AND iv_text IS NOT INITIAL ). ENDMETHOD.字符串拼接也一样。老版本没有|...|,但我见过太多不加RESPECTING BLANKS的CONCATENATE,拼出来的文本要么多剁空格要么少了空格。固定姿势是:拼接前先对每个片段做明确的CONDENSE,拼接时带上RESPECTING BLANKS,最后再整体CONDENSE。所有拼接逻辑尽量塞进一个方法或一处函数组,不要在业务代码里裸写一串。
4. 增强开发不"贴膏药":VA03 与 SM30 的实战说明
4.1 增强点里的最小代码原则
老项目里大量"历史遗代码"其实都出自增强:VA03 的销售凭证显示增强、SM30 维护视图的取数增强、各种 USER EXIT 和隐式增强点。写增强最容易犯的毛病,是把一堆逻辑全塞进增强点,导致增强程序和标准程序完全缠在一起,升级一次崩一次。
我在所有增强开发里坚持一条原则:增强点只做"入口翻译",所有实际逻辑交给自定义类或函数组。增强点里只保留三样东西:
- 从调用上下文取参数;
- 调用自定义类的方法;
- 如果有返回值,再做一个最简单的分支处理。
这样标准程序升级时,增强点接口的变化范围被压缩到最小,逻辑调整都在我们自己的代码里,排查也方便,传输也只带走我们自己的对象。
4.2 VA03 销售凭证显示增强:入口薄、出口短
拿 VA03(销售订单显示)来举例。这类增强常见需求是:显示订单时根据订单类型或客户维度,隐藏某些字段、补显示自定义数据、或者做权限范围内的数据脱敏。
老版本里很多同事直接在标准程序的某个 include 里写 USER EXIT,比如在 MV45AFZZ 里找一处 USER EXIT,然后噼里啪啦写几十行。如果项目历史原因必须留在 EXIT 里,我只留一行调用,例如在增强点里放:
DATA: lo_screen_ctrl TYPE REF TO zcl_sd_va03_screen_ctrl. CREATE OBJECT lo_screen_ctrl. lo_screen_ctrl->hide_fields_for_vbeln( iv_vbeln = vbak-vbeln ).这里假设当前增强点能访问到销售订单抬头工作区 vbak。真正的字段控制逻辑全部写进zcl_sd_va03_screen_ctrl,内部用LOOP AT SCREEN控制屏幕字段,判断条件用封装的订单读取方法返回,不在增强点里到处 SELECT。
这么做的收益很明显:VA03 的增强从"在标准代码里加了一大段逻辑"变成"标准代码里多了一行调用,其余全在我们自己的类里"。后续要调整隐藏规则,不需要再动标准 include,也不会因为升级适配把整个增强连根拔起。
4.3 SM30 取描述:把重复查询装进一个带缓存的笼子
SM30 维护视图是 ABAP 开发者每天都在碰的东西。在维护配置表时,经常需要带出描述文本,比如维护一个销售区域,希望旁边显示区域描述。很多人图快,在增强里直接再查一次文本表,结果同一个屏幕里反复 SELECT 同一张文本表,肉眼可见地卡。
正确的收口方式,是做一个带缓存的"描述读取器"。第一次读某条时查库,把结果放进内存表缓存,后续再读同一个键就直接返回缓存:
METHOD get_region_desc. READ TABLE mt_cache INTO ms_cache WITH KEY region = iv_region. IF sy-subrc EQ 0. rv_desc = ms_cache-desc. RETURN. ENDIF. " 缓存未命中才查库 SELECT SINGLE bezei FROM t005t INTO rv_desc WHERE spras = sy-langu AND land1 = iv_country AND bland = iv_region. ... ENDMETHOD.这里有三个细节值得强调:缓存内表要和业务数据分开存放,别跟主流程的数据混在一个内表里;缓存命中判断要放在方法最前面,避免后续代码把它挤到不起眼的位置;查询失败时一定要留一个可读的默认值或抛出明确异常,不要静默返回空串。这样写之后,SM30 或报表里取描述就是统一入口,数据库访问次数从"每次屏幕刷新查 N 次"降成"每个键最多查 1 次",代码里也不会再到处是裸的SELECT SINGLE bezei。
5. 可持续靠的是流程闸门,不是个人洁癖
5.1 老 ABAP 专属的代码评审清单
个人再有洁癖,人一走规矩就散。我在团队里反复强调:可持续的 Clean Code,不能依赖某个人的自觉,必须变成评审时的硬指标。所以我给老 ABAP 环境专门做过一份评审清单,每一条都是可以在代码走查时直接打钩的:
- 方法或 FORM 体是否超过 20~30 行?如果超了,是否有足够充分的拆分理由?
- 变量名是否满足项目前缀约定,并且能看出用途?
- LOOP 里有没有重复 READ 同一张表?
- 每次 READ 之后是否检查并合理处理了
sy-subrc? - SORT 的排序键是否完整,有没有在赌稳定性?
- 增强点里是否只有入口调用,没有堆逻辑?
- 字符串拼接是否都用了
RESPECTING BLANKS,并且收口到方法里? - 有没有新增的裸 SELECT 可以归到已有的方法或类里?
评审不是为了找茬,是为了让每个接手代码的人知道"什么样的代码在这个老环境里算合格"。清单一旦定下来,新人照着改,老人照着审,比单纯说一句"你写得不 Clean"要高效得多。
5.2 ABAP Unit 在老版本里的"最低成本"起步
老版本没有现代 IDE 里那种一键生成测试类的顺滑体验,但 7.0 之后的系统都支持 ABAP Unit。我见过不少老项目连一个测试方法都没有,理由是"跑太慢、没人写"。但实际上,对纯逻辑类的方法做单测,成本远比想象的低。
我的起步方式是:只给"纯计算型"方法写测试,比如状态判断、文本拼装、数值校验。这类方法输入输出明确,不依赖数据库和屏幕,是单测性价比最高的一类。
CLASS ltcl_numeric_check IMPLEMENTATION. METHOD verify_number. cl_abap_unit_assert=>assert_equals( exp = abap_true act = zcl_util=>is_numeric_text( '12345' ) ). ENDMETHOD. ENDCLASS.别想着一步到位给所有功能补测试,在旧项目里那不现实。先给最容易被后续需求改坏的工具方法补上一点保障,哪怕只有几个用例,也能在下次重构时帮你兜住底。
5.3 传输审批、Code Inspector 和质量回路
最后聊"可持续"里最现实的一环:传输。老项目的传输链路通常是固定的,释放请求要过授权对象 F_001 这类传输相关权限。很多团队把传输审批当成纯流程动作,其实这一步完全可以做成质量闸门。
我建议至少做两件事。第一,释放传输前跑一遍 Code Inspector(事务码 SCI),老版本就有这个工具,不需要多先进的 CI/CD 就能把未使用变量、危险语句、性能隐患提前扫出来。第二,传生产之前,由团队里固定一两个人按 5.1 那份清单做人工复核,重点看增强点和新增读取逻辑。
这条路走下去,代码的整洁就不再是靠某个人扛着,而是靠一条固定回路:写代码的人遵守约定,审批的人检查约定,出问题的代码能被单测或评审拦住。系统虽然老,质量回路却能一直转。
我自己经历过最直观的变化是:以前紧急修复救火时,第一件事是花半小时读懂那段历史烂代码;现在哪怕在 ECC 6.0 上,接手一个增强几乎不用问上一个开发,打开类就看懂了结构。老版本会不会被淘汰是产品战略问题,我们手上的代码能不能让下一个人少受点罪,是每天的工程问题。