Report Modularization — FORM, Function Modules, INCLUDE, Local Classes

Report Modularization: FORM, Function Modules, INCLUDE, Local Classes
ABAP Reports Series · Blog 11 of 15

Report Modularization: FORM, Function Modules, INCLUDE, Local Classes

Intermediate · 20 min read · Needs Blogs 6 and 10

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.
AdditionMeaning
USINGInput. 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.
CHANGINGInput and output. The result goes back to the caller.
TABLESOld way to pass internal tables. Avoid. Use USING or CHANGING with a table type.
Forms have no local memory of their own

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

  1. In SE11, create a table type ZTT_VBAK whose line type is the structure ZST_VBAK from Blog 5. Function parameters need dictionary types.
  2. In SE80, create function group ZSD_ORDERS.
  3. In SE37, create function module Z_GET_SALES_ORDERS in that group.
  4. On the Import tab: IV_DATE_FROM and IV_DATE_TO of type ERDAT, with Pass by value ticked.
  5. On the Export tab: ET_VBAK of type ZTT_VBAK, pass by value.
  6. 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 partDirection
IMPORTINGData coming into the function
EXPORTINGData going out of the function
CHANGINGBoth directions
TABLESOld way for tables. Avoid in new modules.
EXCEPTIONSNamed 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 FORM or 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( ).
TermMeaning
DEFINITIONThe outline: what the class contains
IMPLEMENTATIONThe code of each method
PUBLIC SECTIONWhat other code may use
PRIVATE SECTIONInternal 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
Naming

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 SE38 with 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.
Includes do not make code better

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?

ToolScopeBest forAvoid when
FORMOne programQuick organization, maintaining old codeWriting new, larger reports
Function moduleSystem-wideShared logic, RFC callsLogic used by one report only
Local classOne programNew reports, testable codeThe team cannot read OO code yet
Global class (SE24)System-wideShared, tested business logicThrowaway reports
INCLUDEOne program (file split)Module pools, legacy structureNew reports

7. Structuring a report

A well-organized report reads top to bottom like a table of contents:

  1. Types and data: TYPES, global DATA, CONSTANTS.
  2. Selection screen: PARAMETERS and SELECT-OPTIONS.
  3. Local class definitions and implementations.
  4. Selection screen events: INITIALIZATION, AT SELECTION-SCREEN.
  5. START-OF-SELECTION: create the object and call its methods. Nothing else.

A typical report class has these methods:

MethodJob
validate_inputChecks the selection screen values
get_dataThe SELECT logic
process_dataCalculations, calculated columns, internal table work
build_fieldcatalogALV column setup
display_alvThe ALV call
The golden rule

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)
DataGlobal tables, visible everywherePrivate attributes of the class
LogicFORM routinesMethods with typed parameters
START-OF-SELECTIONCalls forms, reads globalsCreates an object and calls 3 methods
InputReads s_erdat and p_max directlyReceives 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.
Why is there still a FORM?

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_ORDERS and call it from the report instead of the inline SELECT.
  • Add a validate_input method that rejects iv_max below 1.
  • Move the class into its own include file and INCLUDE it.
  • Compare the readability of the old and new START-OF-SELECTION.

10. Common mistakes

  • Using TABLES in a FORM. 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 EXCEPTIONS and check sy-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.

ABAP Reports: Zero to Hero · Blog 11 of 15
WhatsApp WhatsApp us
Call Now Button