Report Modularization: FORM, Function Modules, INCLUDE, Local Classes
1. Why modularization?
Your first reports are 50 lines. Real reports grow to 500, 2,000, even 5,000 lines. If everything sits in one big START-OF-SELECTION block, nobody can read it, test it, or change it safely.
Modularization means splitting the program into small named blocks, each doing one job:
- Easier to read: the main flow shows what happens, the blocks show how.
- Easier to fix: a bug in the SELECT is in one place.
- Reusable: the same block can be called many times, or from other programs.
- Testable: small blocks can be tested on their own (Blog 15).
ABAP gives you four tools for this. This blog covers all four, and tells you when to pick which.
2. FORM / PERFORM: the classical way
A FORM is a named block of code inside your program. You run it with PERFORM. You have already used it in Blogs 6 to 10.
START-OF-SELECTION.
PERFORM get_data.
PERFORM display_alv.
FORM get_data.
SELECT vbeln, erdat FROM vbak
INTO TABLE @gt_vbak UP TO 100 ROWS.
ENDFORM.
Passing data in and out
PERFORM get_order_date USING lv_vbeln CHANGING lv_erdat.
FORM get_order_date USING p_vbeln TYPE vbak-vbeln
CHANGING p_erdat TYPE vbak-erdat.
SELECT SINGLE erdat FROM vbak
WHERE vbeln = @p_vbeln
INTO @p_erdat.
ENDFORM.
| Addition | Meaning |
|---|---|
USING | Input. Passed by reference, so changing it inside the form changes the caller’s variable too. |
USING VALUE(p) | Input, passed by value (a copy). Safe to change. |
CHANGING | Input and output. The result goes back to the caller. |
TABLES | Old way to pass internal tables. Avoid. Use USING or CHANGING with a table type. |
A FORM can read and change every global variable of the program. That is convenient but dangerous: any form may change any global, and bugs are hard to trace. This is the main reason modern code prefers classes.
PERFORM name IN PROGRAM prog calls a form in another program. It works, but it creates a hidden link between two programs. Avoid it in new code.
3. Function modules: the reusable way
A function module lives in the system, not inside one program. Any program can call it. Function modules are grouped in a function group and managed in transactions SE37 (function builder) and SE80.
Calling a function module
CALL FUNCTION 'Z_GET_SALES_ORDERS'
EXPORTING
iv_date_from = s_erdat-low
iv_date_to = s_erdat-high
IMPORTING
et_vbak = DATA(lt_vbak)
EXCEPTIONS
not_found = 1
OTHERS = 2.
IF sy-subrc <> 0.
MESSAGE 'No sales orders found' TYPE 'S' DISPLAY LIKE 'E'.
RETURN.
ENDIF.
Note the direction: EXPORTING is what you send to the function, IMPORTING is what you receive. It is the mirror of the function’s own interface.
Creating one in SE37
- In
SE11, create a table typeZTT_VBAKwhose line type is the structureZST_VBAKfrom Blog 5. Function parameters need dictionary types. - In
SE80, create function groupZSD_ORDERS. - In
SE37, create function moduleZ_GET_SALES_ORDERSin that group. - On the Import tab:
IV_DATE_FROMandIV_DATE_TOof typeERDAT, with Pass by value ticked. - On the Export tab:
ET_VBAKof typeZTT_VBAK, pass by value. - On the Exceptions tab:
NOT_FOUND. Write the source below, then activate.
FUNCTION z_get_sales_orders.
SELECT vbeln, erdat, ernam, vbtyp, netwr, waerk
FROM vbak
WHERE erdat BETWEEN @iv_date_from AND @iv_date_to
INTO CORRESPONDING FIELDS OF TABLE @et_vbak.
IF sy-subrc <> 0.
RAISE not_found.
ENDIF.
ENDFUNCTION.
| Interface part | Direction |
|---|---|
IMPORTING | Data coming into the function |
EXPORTING | Data going out of the function |
CHANGING | Both directions |
TABLES | Old way for tables. Avoid in new modules. |
EXCEPTIONS | Named error cases the caller can test with sy-subrc |
- Use for: logic shared by several programs, or logic that other systems call (RFC-enabled modules).
- Do not use for: code that belongs to one report only. A
FORMor a class method is lighter.
4. Local classes: the modern way
A local class is defined inside your report. It bundles data (attributes) and actions (methods) into one unit, and keeps its data private. Since ABAP 7.40, this is the preferred way to structure a report.
CLASS lcl_report DEFINITION FINAL.
PUBLIC SECTION.
METHODS: get_data,
display.
PRIVATE SECTION.
DATA mt_vbak TYPE STANDARD TABLE OF vbak WITH DEFAULT KEY.
ENDCLASS.
CLASS lcl_report IMPLEMENTATION.
METHOD get_data.
SELECT * FROM vbak INTO TABLE @mt_vbak UP TO 100 ROWS.
ENDMETHOD.
METHOD display.
" show mt_vbak in ALV
ENDMETHOD.
ENDCLASS.
START-OF-SELECTION.
DATA(lo_report) = NEW lcl_report( ).
lo_report->get_data( ).
lo_report->display( ).
| Term | Meaning |
|---|---|
DEFINITION | The outline: what the class contains |
IMPLEMENTATION | The code of each method |
PUBLIC SECTION | What other code may use |
PRIVATE SECTION | Internal data and helper methods, hidden from outside |
NEW lcl_report( ) | Creates an object (an instance) of the class |
lo_report->get_data( ) | Calls a method of the object |
Attributes start with m (mt_vbak, member), method parameters with iv_, it_, ev_, rv_ (importing, exporting, returning). See Blog 13.
5. INCLUDE programs
An include is a piece of source code stored in its own file. The INCLUDE statement copies it into your program when it is compiled. It does not create a separate unit of logic, it only splits the text across files.
REPORT zsales_report.
INCLUDE zsales_report_top. " TYPES, DATA, selection screen
INCLUDE zsales_report_cls. " local classes
INCLUDE zsales_report_f01. " FORM routines
START-OF-SELECTION.
...
- Create it in
SE38with program type Include. - Common suffixes:
_TOP(declarations),_F01(forms),_CLS(classes). - Includes are essential in module pools and function groups, where the system creates them for you.
Moving a messy 3,000-line program into five include files leaves it just as messy. Use includes to organize files, and classes to organize logic.
6. Which one should I use?
| Tool | Scope | Best for | Avoid when |
|---|---|---|---|
FORM | One program | Quick organization, maintaining old code | Writing new, larger reports |
| Function module | System-wide | Shared logic, RFC calls | Logic used by one report only |
| Local class | One program | New reports, testable code | The team cannot read OO code yet |
Global class (SE24) | System-wide | Shared, tested business logic | Throwaway reports |
INCLUDE | One program (file split) | Module pools, legacy structure | New reports |
7. Structuring a report
A well-organized report reads top to bottom like a table of contents:
- Types and data:
TYPES, globalDATA,CONSTANTS. - Selection screen:
PARAMETERSandSELECT-OPTIONS. - Local class definitions and implementations.
- Selection screen events:
INITIALIZATION,AT SELECTION-SCREEN. START-OF-SELECTION: create the object and call its methods. Nothing else.
A typical report class has these methods:
| Method | Job |
|---|---|
validate_input | Checks the selection screen values |
get_data | The SELECT logic |
process_data | Calculations, calculated columns, internal table work |
build_fieldcatalog | ALV column setup |
display_alv | The ALV call |
Keep business logic out of START-OF-SELECTION. That event should only orchestrate: create, get, process, display.
8. Refactoring Program 2
Here is Program 2 from Blog 6 rewritten with a local class. Same output, but each job now has a name and a home.
| Before (Program 2) | After (this blog) | |
|---|---|---|
| Data | Global tables, visible everywhere | Private attributes of the class |
| Logic | FORM routines | Methods with typed parameters |
START-OF-SELECTION | Calls forms, reads globals | Creates an object and calls 3 methods |
| Input | Reads s_erdat and p_max directly | Receives them as parameters |
REPORT zalv_program2_oo.
TYPES: BEGIN OF ty_vbak,
vbeln TYPE vbak-vbeln,
erdat TYPE vbak-erdat,
ernam TYPE vbak-ernam,
vbtyp TYPE vbak-vbtyp,
vbtxt TYPE c LENGTH 15,
netwr TYPE vbak-netwr,
waerk TYPE vbak-waerk,
END OF ty_vbak,
ty_vbak_t TYPE STANDARD TABLE OF ty_vbak WITH DEFAULT KEY,
ty_erdat_range TYPE RANGE OF vbak-erdat.
DATA gv_erdat TYPE vbak-erdat.
SELECT-OPTIONS s_erdat FOR gv_erdat.
PARAMETERS p_max TYPE i DEFAULT 500.
*---------------------------------------------------------------------*
* Class definition
*---------------------------------------------------------------------*
CLASS lcl_report DEFINITION FINAL.
PUBLIC SECTION.
METHODS:
get_data IMPORTING it_erdat TYPE ty_erdat_range
iv_max TYPE i,
has_data RETURNING VALUE(rv_has) TYPE abap_bool,
display,
write_header.
PRIVATE SECTION.
DATA: mt_vbak TYPE ty_vbak_t,
mt_fcat TYPE slis_t_fieldcat_alv.
METHODS:
fill_texts,
build_fieldcat,
add_col IMPORTING iv_field TYPE slis_fieldname
iv_text TYPE scrtext_m
iv_len TYPE i.
ENDCLASS.
*---------------------------------------------------------------------*
* Class implementation
*---------------------------------------------------------------------*
CLASS lcl_report IMPLEMENTATION.
METHOD get_data.
SELECT vbeln, erdat, ernam, vbtyp, netwr, waerk
FROM vbak
WHERE erdat IN @it_erdat
ORDER BY erdat DESCENDING, vbeln
INTO CORRESPONDING FIELDS OF TABLE @mt_vbak
UP TO @iv_max ROWS.
IF sy-subrc <> 0.
MESSAGE 'No sales orders found for your selection'
TYPE 'S' DISPLAY LIKE 'E'.
RETURN.
ENDIF.
fill_texts( ).
ENDMETHOD.
METHOD has_data.
rv_has = xsdbool( mt_vbak IS NOT INITIAL ).
ENDMETHOD.
METHOD fill_texts.
LOOP AT mt_vbak ASSIGNING FIELD-SYMBOL(<ls_vbak>).
<ls_vbak>-vbtxt = SWITCH #( <ls_vbak>-vbtyp
WHEN 'A' THEN 'Inquiry'
WHEN 'B' THEN 'Quotation'
WHEN 'C' THEN 'Order'
ELSE 'Other' ).
ENDLOOP.
ENDMETHOD.
METHOD add_col.
DATA(ls_fcat) = VALUE slis_fieldcat_alv(
fieldname = iv_field
tabname = 'MT_VBAK'
col_pos = lines( mt_fcat ) + 1
seltext_s = iv_text
seltext_m = iv_text
seltext_l = iv_text
outputlen = iv_len ).
APPEND ls_fcat TO mt_fcat.
ENDMETHOD.
METHOD build_fieldcat.
CLEAR mt_fcat.
add_col( iv_field = 'VBELN' iv_text = 'Sales Doc.' iv_len = 12 ).
add_col( iv_field = 'ERDAT' iv_text = 'Created on' iv_len = 12 ).
add_col( iv_field = 'ERNAM' iv_text = 'Created by' iv_len = 12 ).
add_col( iv_field = 'VBTYP' iv_text = 'Cat.' iv_len = 5 ).
add_col( iv_field = 'VBTXT' iv_text = 'Category' iv_len = 15 ).
" Amount column: needs the currency field and a total
APPEND VALUE slis_fieldcat_alv(
fieldname = 'NETWR'
tabname = 'MT_VBAK'
col_pos = lines( mt_fcat ) + 1
seltext_s = 'Net value'
seltext_m = 'Net value'
seltext_l = 'Net value'
outputlen = 16
cfieldname = 'WAERK'
do_sum = 'X' ) TO mt_fcat.
add_col( iv_field = 'WAERK' iv_text = 'Currency' iv_len = 8 ).
ENDMETHOD.
METHOD display.
build_fieldcat( ).
DATA(lv_repid) = sy-repid.
DATA(ls_layout) = VALUE slis_layout_alv( zebra = 'X' colwidth_optimize = 'X' ).
CALL FUNCTION 'REUSE_ALV_GRID_DISPLAY'
EXPORTING
i_callback_program = lv_repid
i_callback_top_of_page = 'ALV_TOP_OF_PAGE'
is_layout = ls_layout
it_fieldcat = mt_fcat
TABLES
t_outtab = mt_vbak
EXCEPTIONS
program_error = 1
OTHERS = 2.
IF sy-subrc <> 0.
MESSAGE 'ALV could not be displayed' TYPE 'S' DISPLAY LIKE 'E'.
ENDIF.
ENDMETHOD.
METHOD write_header.
DATA(lt_header) = VALUE slis_t_listheader(
( typ = 'H' info = 'Sales Orders Report' )
( typ = 'S' key = 'Records:' info = |{ lines( mt_vbak ) }| ) ).
CALL FUNCTION 'REUSE_ALV_COMMENTARY_WRITE'
EXPORTING it_list_commentary = lt_header.
ENDMETHOD.
ENDCLASS.
*---------------------------------------------------------------------*
* Main flow
*---------------------------------------------------------------------*
DATA go_report TYPE REF TO lcl_report.
INITIALIZATION.
s_erdat-sign = 'I'.
s_erdat-option = 'BT'.
s_erdat-low = sy-datum - 30.
s_erdat-high = sy-datum.
APPEND s_erdat.
START-OF-SELECTION.
go_report = NEW #( ).
go_report->get_data( it_erdat = s_erdat[] iv_max = p_max ).
IF go_report->has_data( ) = abap_true.
go_report->display( ).
ENDIF.
*---------------------------------------------------------------------*
* ALV callback: must be a FORM, so it hands over to the object
*---------------------------------------------------------------------*
FORM alv_top_of_page.
go_report->write_header( ).
ENDFORM.
REUSE_ALV_GRID_DISPLAY was written before ABAP Objects, so its callbacks take the name of a FORM, not a method. The form is only a thin bridge. With CL_SALV_TABLE (Blog 15), events are handled by methods directly.
9. Try it yourself
- Convert Program 2 yourself first, then compare with the code above.
- Create the function module
Z_GET_SALES_ORDERSand call it from the report instead of the inlineSELECT. - Add a
validate_inputmethod that rejectsiv_maxbelow 1. - Move the class into its own include file and
INCLUDEit. - Compare the readability of the old and new
START-OF-SELECTION.
10. Common mistakes
- Using
TABLESin aFORM. It is obsolete and creates hidden coupling. - Calling forms of another program with
PERFORM ... IN PROGRAM. Fragile. - Long chains of forms (form calls form calls form). Nobody can follow the flow.
- Ignoring exceptions of a function module. Always add
EXCEPTIONSand checksy-subrc. - A class with 20 or more methods. Split it into several classes with one job each.
- Business logic in
START-OF-SELECTION. It should only orchestrate. - Class used before it is defined. Definitions and implementations must come before the code that creates the object.
- Private data accessed from outside. Add a public method instead of making attributes public.
Up next: Blog 12
Modern ABAP Syntax. Shorter, cleaner code with VALUE, FOR, REDUCE, COND, SWITCH, table expressions, and string templates.
WhatsApp us