模块化基础:子程序与Include程序(5篇)
第5篇:实战落地:用子程序+Include搭建一个可维护的小型项目框架
前面的四篇文章,我们分别学习了子程序的基础、参数传递、Include的用法以及常见避坑指南。理论知识已经齐备,但如何在实际项目中真正运用这些模块化技巧?本文将通过一个完整的实战案例——员工月度工资报表,从0到1演示如何合理拆分子程序、规划Include文件结构,最终搭建一个可维护、易扩展的小型项目框架。读完本文,你将能够把模块化思维落地到日常开发中。
一、项目需求概述
我们要开发一个员工月度工资报表,功能如下:
为了展示模块化,我们会将程序拆分为:主控制流程、数据获取、业务计算、ALV输出、日志记录等模块。
二、整体文件结构规划
我们将创建一个主程序Z_HR_SALARY_RPT,并配套4个Include文件:
| Z_HR_SALARY_RPT(主程序) | 主控流程,调用各Include中的子程序 | START-OF-SELECTION,全局变量声明(少量) |
| ZINCL_HR_SALARY_DATA | 数据获取 | 从数据库读取员工工资、员工主数据 |
| ZINCL_HR_SALARY_CALC | 业务计算 | 计算应发工资、部门汇总 |
| ZINCL_HR_SALARY_ALV | ALV输出 | 生成字段目录、显示报表 |
| ZINCL_HR_SALARY_LOG | 日志记录 | 将计算结果写入日志表 |
命名规范:主程序以Z_开头,Include以ZINCL_开头,清晰表达用途。
三、主程序框架:控制流程
主程序只做三件事:声明必要的全局变量(尽量少)、包含Include、定义执行顺序。
主程序 Z_HR_SALARY_RPT:
REPORT z_hr_salary_rpt.
*———————————————————————-*
* 全局变量声明(仅跨Include共享的数据)
*———————————————————————-*
DATA: gv_bukrs TYPE bukrs, " 公司代码
gv_gjahr TYPE gjahr, " 年份
gv_monat TYPE monat. " 月份
" 主要数据结构(内表)
DATA: gt_salary TYPE TABLE OF zhr_salary, " 原始工资记录
gt_employee TYPE TABLE OF zhr_employee, " 员工主数据
gt_result TYPE TABLE OF ty_result, " 计算结果(见类型定义)
gt_summary TYPE TABLE OF ty_summary. " 部门汇总
*———————————————————————-*
* 包含模块化组件
*———————————————————————-*
INCLUDE zincl_hr_salary_data. " 数据获取子程序
INCLUDE zincl_hr_salary_calc. " 计算子程序
INCLUDE zincl_hr_salary_alv. " ALV输出子程序
INCLUDE zincl_hr_salary_log. " 日志子程序
*———————————————————————-*
* 主控流程
*———————————————————————-*
START-OF-SELECTION.
PERFORM f_get_parameters. " 获取输入参数
PERFORM f_load_data. " 加载数据
PERFORM f_calculate. " 计算
PERFORM f_display_alv. " 输出ALV
PERFORM f_write_log. " 写日志
注意:类型ty_result和ty_summary我们将在计算Include中定义,这样其他Include也能使用。
四、Include设计详解
4.1 数据获取模块(ZINCL_HR_SALARY_DATA)
负责从数据库读取原始数据,并填充主程序中的全局内表。
*&———————————————————————*
*& Include ZINCL_HR_SALARY_DATA
*& 数据获取子程序
*&———————————————————————*
FORM f_get_parameters.
" 从选择屏幕获取参数(假设已定义选择屏幕)
gv_bukrs = p_bukrs.
gv_gjahr = p_gjahr.
gv_monat = p_monat.
ENDFORM.
FORM f_load_data.
" 读取工资记录
SELECT * FROM zhr_salary
INTO TABLE gt_salary
WHERE bukrs = gv_bukrs
AND gjahr = gv_gjahr
AND monat = gv_monat.
IF sy-subrc <> 0.
MESSAGE '没有找到工资数据' TYPE 'W'.
RETURN.
ENDIF.
" 读取员工主数据(使用FOR ALL ENTRIES)
IF gt_salary IS NOT INITIAL.
SELECT * FROM zhr_employee
INTO TABLE gt_employee
FOR ALL ENTRIES IN gt_salary
WHERE pernr = gt_salary-pernr.
ENDIF.
ENDFORM.
4.2 业务计算模块(ZINCL_HR_SALARY_CALC)
定义结果结构、计算每位员工的应发工资,并汇总部门数据。
*&———————————————————————*
*& Include ZINCL_HR_SALARY_CALC
*& 业务计算子程序
*&———————————————————————*
" 定义结果行结构
TYPES: BEGIN OF ty_result,
pernr TYPE pernr_d, " 员工号
ename TYPE emnam, " 姓名
dept_id TYPE dept_id, " 部门ID
dept_name TYPE dept_name, " 部门名称
basic TYPE p DECIMALS 2, " 基本工资
bonus TYPE p DECIMALS 2, " 奖金
deduct TYPE p DECIMALS 2, " 扣款
net TYPE p DECIMALS 2, " 实发工资
END OF ty_result.
TYPES: BEGIN OF ty_summary,
dept_id TYPE dept_id,
dept_name TYPE dept_name,
emp_count TYPE i,
total_net TYPE p DECIMALS 2,
END OF ty_summary.
FORM f_calculate.
DATA: ls_result TYPE ty_result,
ls_emp TYPE zhr_employee,
lt_summary TYPE TABLE OF ty_summary,
ls_summary LIKE LINE OF lt_summary.
" 清空结果表
REFRESH: gt_result, gt_summary.
LOOP AT gt_salary INTO DATA(ls_sal).
" 获取员工信息
READ TABLE gt_employee INTO ls_emp WITH KEY pernr = ls_sal-pernr.
IF sy-subrc <> 0.
CONTINUE.
ENDIF.
" 计算实发
ls_result-pernr = ls_sal-pernr.
ls_result-ename = ls_emp-ename.
ls_result-dept_id = ls_emp-dept_id.
ls_result-dept_name = ls_emp-dept_name.
ls_result-basic = ls_sal-basic.
ls_result-bonus = ls_sal-bonus.
ls_result-deduct = ls_sal-deduct.
ls_result-net = ls_sal-basic + ls_sal-bonus – ls_sal-deduct.
APPEND ls_result TO gt_result.
" 部门汇总累加
READ TABLE lt_summary INTO ls_summary WITH KEY dept_id = ls_emp-dept_id.
IF sy-subrc = 0.
ls_summary-emp_count = ls_summary-emp_count + 1.
ls_summary-total_net = ls_summary-total_net + ls_result-net.
MODIFY lt_summary FROM ls_summary INDEX sy-tabix.
ELSE.
ls_summary-dept_id = ls_emp-dept_id.
ls_summary-dept_name = ls_emp-dept_name.
ls_summary-emp_count = 1.
ls_summary-total_net = ls_result-net.
APPEND ls_summary TO lt_summary.
ENDIF.
ENDLOOP.
" 将汇总结果赋值给全局内表(供ALV显示)
gt_summary = lt_summary.
ENDFORM.
4.3 ALV输出模块(ZINCL_HR_SALARY_ALV)
负责生成字段目录并调用ALV函数。
*&———————————————————————*
*& Include ZINCL_HR_SALARY_ALV
*& ALV输出子程序
*&———————————————————————*
FORM f_display_alv.
" 如果无数据,不显示
IF gt_result IS INITIAL.
RETURN.
ENDIF.
" 生成字段目录(仅示意,实际可调用函数批量生成)
DATA: lt_fieldcat TYPE slis_t_fieldcat_alv,
ls_fieldcat LIKE LINE OF lt_fieldcat.
" 员工号
ls_fieldcat-fieldname = 'PERNR'.
ls_fieldcat-seltext_l = '员工号'.
APPEND ls_fieldcat TO lt_fieldcat.
" 姓名
ls_fieldcat-fieldname = 'ENAME'.
ls_fieldcat-seltext_l = '姓名'.
APPEND ls_fieldcat TO lt_fieldcat.
" 基本工资
ls_fieldcat-fieldname = 'BASIC'.
ls_fieldcat-seltext_l = '基本工资'.
APPEND ls_fieldcat TO lt_fieldcat.
" 实发工资
ls_fieldcat-fieldname = 'NET'.
ls_fieldcat-seltext_l = '实发工资'.
APPEND ls_fieldcat TO lt_fieldcat.
" 可以继续添加其他字段…
" 调用ALV显示函数
CALL FUNCTION 'REUSE_ALV_GRID_DISPLAY'
EXPORTING
i_callback_program = sy-repid
it_fieldcat = lt_fieldcat
i_save = 'A'
TABLES
t_outtab = gt_result
EXCEPTIONS
OTHERS = 1.
ENDFORM.
扩展:如果需要同时显示部门汇总表,可以弹出第二个ALV,或者在主ALV中通过双击事件触发。这里为了简洁,只展示明细。
4.4 日志记录模块(ZINCL_HR_SALARY_LOG)
将计算结果写入日志表,便于追溯。
*&———————————————————————*
*& Include ZINCL_HR_SALARY_LOG
*& 日志记录子程序
*&———————————————————————*
FORM f_write_log.
DATA: ls_log TYPE zhr_salary_log,
lv_count TYPE i.
LOOP AT gt_result INTO DATA(ls_res).
ls_log-pernr = ls_res-pernr.
ls_log-dept_id = ls_res-dept_id.
ls_log-net = ls_res-net.
ls_log-rundate = sy-datum.
ls_log-username = sy-uname.
INSERT zhr_salary_log FROM ls_log.
IF sy-subrc = 0.
lv_count = lv_count + 1.
ENDIF.
ENDLOOP.
IF lv_count > 0.
WRITE: / '日志已写入', lv_count, '条记录'.
ENDIF.
ENDFORM.
五、模块化带来的好处
5.1 可读性
主程序只有20行,清晰地展示了整个报表的执行流程:参数获取 → 读数据 → 计算 → 显示 → 记日志。新接手项目的同事可以在1分钟内了解程序的全貌。
5.2 可维护性
- 如果业务逻辑变了(比如奖金计算规则调整),只需修改ZINCL_HR_SALARY_CALC中的f_calculate子程序,不影响其他模块。
- 如果ALV显示需要增加分组、排序功能,只需改动ZINCL_HR_SALARY_ALV,不会意外破坏数据获取逻辑。
- 如果数据库表结构变更,仅修改对应的Include即可。
5.3 可测试性
可以编写单独的测试程序,仅PERFORM f_calculate,传入模拟数据,验证计算结果是否正确,无需运行整个报表。
5.4 团队协作
可以让三个开发者同时开发不同的Include:一个负责数据读取,一个负责计算,一个负责ALV输出,互不干扰。
六、扩展与优化建议
6.1 将类型定义提取到独立的Include
如果多个Include都需要使用ty_result、ty_summary,可以将它们放到一个单独的ZINCL_HR_SALARY_TYPES中,然后在所有其他Include最开头INCLUDE ZINCL_HR_SALARY_TYPES。
6.2 使用函数模块或类替代Include
对于更复杂的项目,可以将上述子程序迁移到函数模块或类方法中,以获得更好的封装和异常处理。但Include+子程序的模式轻量、简单,非常适合中小型项目。
6.3 添加异常处理
在每个子程序内部增加错误捕获和返回标志位,主程序根据标志位决定是否继续。例如:
FORM f_load_data CHANGING cv_error TYPE abap_bool.
" 如果出错,设置 cv_error = abap_true
ENDFORM.
6.4 使用选择屏幕
可以为报表添加选择屏幕,让用户输入年月和公司代码。选择屏幕的逻辑可以在主程序中独立编写,也可放入f_get_parameters中。
七、总结
通过这个完整的实战案例,我们演示了如何从零开始,使用子程序和Include程序构建一个可维护的小型项目框架。核心要点回顾:
| 主程序 | 控制流程、少量全局变量 | START-OF-SELECTION、PERFORM |
| 数据Include | 数据获取 | SELECT、FOR ALL ENTRIES |
| 计算Include | 业务计算、汇总 | LOOP、内表处理 |
| ALV Include | 报表输出 | REUSE_ALV_GRID_DISPLAY |
| 日志Include | 记录执行结果 | INSERT、LOOP |
模块化不是银弹,但合理的模块化可以让你的代码在三个月后、三年后仍然易于理解和修改。从今天开始,在你编写每一个新程序时,都先问自己三个问题:
然后,按照本文的示例,构建你的Include+子程序框架。祝你在模块化之路上越走越顺!
📌 本系列回顾:
- 第1篇:《从冗余到精简:子程序是模块化开发的第一块基石》
- 第2篇:《子程序核心用法指南:参数传递、返回值与边界场景处理》
- 第3篇:《代码复用的另一条路径:Include程序的底层逻辑与基础用法》
- 第4篇:《避坑指南:子程序与Include程序的常见误用场景解析》
- 第5篇:《实战落地:用子程序+Include搭建一个可维护的小型项目框架》(本文)
作者:你的ABAP学习伙伴
版本记录:2026年5月
💬 你在实际项目中是如何划分Include的?有没有更好的模块化实践?欢迎留言交流。



