模块化基础:子程序与Include程序(5篇)
第5篇:实战落地:用子程序+Include搭建一个可维护的小型项目框架
前面的四篇文章,我们分别学习了子程序的基础、参数传递、Include的用法以及常见避坑指南。理论知识已经齐备,但如何在实际项目中真正运用这些模块化技巧?本文将通过一个完整的实战案例——员工月度工资报表,从0到1演示如何合理拆分子程序、规划Include文件结构,最终搭建一个可维护、易扩展的小型项目框架。读完本文,你将能够把模块化思维落地到日常开发中。
一、项目需求概述
我们要开发一个员工月度工资报表,功能如下:
- 接收用户输入的年月(如202605)和公司代码。
- 从数据库读取该月的所有员工工资记录(表
ZHR_SALARY,含员工号、姓名、基本工资、奖金、扣款等)。 - 计算每位员工的应发工资= 基本工资 + 奖金 - 扣款。
- 计算部门汇总(部门从员工主数据表
ZHR_EMPLOYEE获取)。 - 输出ALV报表,展示员工明细和部门汇总。
- 将计算结果写入日志表
ZHR_SALARY_LOG。
为了展示模块化,我们会将程序拆分为:主控制流程、数据获取、业务计算、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的?有没有更好的模块化实践?欢迎留言交流。