Skip to Content
Technical Articles

Write Smart ABAP – Not Boring ABAP! – Part 1

 

 

Introduction

I do not see many developers using the new, modern and powerful enhancements to the ABAP language yet as a result most of these modern features are not leveraged in the applications which, I believe, were made with an intention to modernise the language as well as simplify the development and maintenance of the application lifecycle.

The intention of this blog series is to highlight the benefits of functional programming by embracing the new ABAP features (and some old techniques as well which are powerful enough but not very popular for reasons unknown) to write smart, exciting, reusable, sustainable code and avoid writing boring, repetitive (often violating the Don’t Repeat Yourself (DRY) principle or the Separation of Concern (SoC) principle), redundant, old fashioned (and often procedural) code which is a nightmare to maintain and/or to be able to extend these applications.

Back to the Future

We will start with one of the powerful features which is already “old” i.e. it’s been there in the system surely for more than a decade but I do not see many developers using it at all – it is the Resumable Exception. I will demonstrate the handling of a same business scenario exception with and without its Resumable option to understand the difference and also the benefits of a modern development design which leverages these powerful features ultimately saving time & effort which equals money to the customer in the long run.

Business Scenario

The business scenario is quite simple. A list of employees are to be reported with their respective Employee Group (EMPTYP), Salary amount (SALARY) and Phone Numbers (PHONE).

However, for EMPTYP = ‘Contractor’, there will be no salary record found in the system (as it is managed by their recruitment agencies) however other information of these contractors like Employee Type and Phone Numbers are still required to be reported.

Also, if Employee Group (EMPTYP) data is not found in the system (data not maintained), they need to be reported as exceptions for those employees.

Technical Design

In the context of the business scenario, the internal table we will be looping on contains References to employee objects for all Types of employees (Employee Type) i.e. Full-Time, Part-Time, Contractors, Casual and so on.

In each iteration of the loop, we will read all required employee data and move them to build another internal table to display the results.

Things to note in the design phase:

  • The contractor group has no pay details stored in the system as they are managed externally – the business however would still like to see the other available data in the system for them.
    • Enter Exception Objects – we could treat this scenario as an exception and handle them separately however we need to make sure that the exception object is managed in a way to be able to still process the employee attributes for which the data is available.
  • As the attribute Employee Type is quite important to the organisation, any employee with missing Employee Type record needs to reported as exceptions so that their master data can then be maintained.
    • Sounds like a straight forward case of an Exception Object which needs to be handled and then excluded from processing of the report.

Implementation

Exception handling WITH the Resumable addition

Report ztest.

CLASS lcl_employee DEFINITION FINAL CREATE PUBLIC.

  PUBLIC SECTION.

    TYPES: BEGIN OF empl_data,
             empid  TYPE int4,          "Employee ID
             emptyp TYPE string,        "Org Assignment data
             salary TYPE decfloat16,    "Pay data
             phone  TYPE numc10,        "Communication data
           END OF empl_data,
     empl_data_t TYPE SORTED TABLE OF empl_data WITH UNIQUE KEY empid.

    METHODS constructor IMPORTING VALUE(i_empid)   TYPE int4.
    METHODS get_data    RETURNING VALUE(rs_result) TYPE empl_data
                        RAISING RESUMABLE(cx_no_data_found).

  PRIVATE SECTION.

    DATA  emp_id       TYPE int4.

    METHODS get_emptyp RETURNING VALUE(r_result) TYPE string
                       RAISING cx_no_data_found.
    METHODS get_salary RETURNING VALUE(r_result) TYPE decfloat16
                       RAISING RESUMABLE(cx_no_data_found).
    METHODS get_phone  RETURNING VALUE(r_result) TYPE numc10.
    METHODS get_emp_id RETURNING VALUE(r_result) TYPE int4.

ENDCLASS.

CLASS lcl_employee IMPLEMENTATION.

  METHOD constructor.
    me->emp_id = i_empid.
  ENDMETHOD.

  METHOD get_data.
    rs_result = VALUE #( empid  = me->get_emp_id( )
                         emptyp = me->get_emptyp( )
                         salary = me->get_salary( )
                         phone  = me->get_phone( )
                       ).
  ENDMETHOD.

  METHOD get_emptyp.
    r_result = SWITCH #( me->get_emp_id( )
                WHEN 1 THEN |Full-Time|
                WHEN 2 THEN |Part-Time|
                WHEN 3 THEN |Contractor|
                WHEN 4 THEN |Casual|
                ELSE THROW cx_no_data_found(
                            rel_proc_id = CONV #( me->get_emp_id( ) ) )
              ).
  ENDMETHOD.

  METHOD get_phone.
    r_result = SWITCH #( me->get_emptyp( )
                WHEN `Full-Time` THEN |1234567890|
                WHEN `Part-Time` THEN |5678901234|
                WHEN `Casual`    THEN |7890123456|
                ELSE |0399999999|
              ).
  ENDMETHOD.

  METHOD get_salary.
    r_result = SWITCH #( me->get_emptyp( )
                WHEN `Full-Time` THEN 50000
                WHEN `Part-Time` THEN 25000
                WHEN `Casual`    THEN 5000
                ELSE THROW RESUMABLE cx_no_data_found( 
                            rel_proc_id = CONV #( me->get_emp_id( ) ) )
               ).
  ENDMETHOD.

  METHOD get_emp_id.
    r_result = me->emp_id.
  ENDMETHOD.

ENDCLASS.

 DATA extract_t TYPE lcl_employee=>empl_data_t.
 DATA error_t   TYPE string_table.

START-OF-SELECTION.

 DATA(all_employees_t) = VALUE int4_table(  ( 1 ) ( 2 ) ( 3 ) ( 4 ) ( 5 ) ).

  LOOP AT all_employees_t REFERENCE INTO DATA(dref).
    TRY.
      INSERT NEW lcl_employee( dref->* )->get_data( ) INTO TABLE extract_t.

      CATCH BEFORE UNWIND cx_no_data_found INTO DATA(lx_no_data).
        IF lx_no_data->is_resumable = abap_true. 
         "Resumable Exception was raised
          RESUME.
        ELSE.
         "Non-Resumable Exception was raised
          error_t = VALUE #( BASE error_t ( lx_no_data->get_text( ) ) ).
        ENDIF.
    ENDTRY.
  ENDLOOP.

 cl_demo_output=>new( )->write( extract_t )->write( error_t )->display( ).

 

Exception handling WITHOUT the Resumable addition

The implementation of the Local Class (below code snippet) needs to be slightly different in this case as the calling program would want to know for which specific data the exception was raised whereas the above implementation (with the Resumable addition) simply does not care.

CLASS lcl_employee DEFINITION FINAL CREATE PUBLIC.

  PUBLIC SECTION.
    TYPES: BEGIN OF empl_data,
             empid  TYPE int4,          "Employee ID
             emptyp TYPE string,        "Org Assignment data
             salary TYPE decfloat16,    "Pay data
             phone  TYPE numc10,        "Communication data
           END OF empl_data,
     empl_data_t TYPE SORTED TABLE OF empl_data WITH UNIQUE KEY empid.

    METHODS constructor IMPORTING VALUE(i_empid)   TYPE int4.
    METHODS get_emptyp  RETURNING VALUE(r_result) TYPE string
                        RAISING cx_no_data_found.
    METHODS get_salary  RETURNING VALUE(r_result)  TYPE decfloat16
                        RAISING cx_no_data_found.
    METHODS get_phone   RETURNING VALUE(r_result)  TYPE numc10.
    METHODS get_emp_id  RETURNING VALUE(r_result)  TYPE int4.

  PRIVATE SECTION.
    DATA emp_id TYPE int4.

ENDCLASS.

CLASS lcl_employee IMPLEMENTATION.

  METHOD constructor.
    me->emp_id = i_empid.
  ENDMETHOD.

  METHOD get_emptyp.
    r_result = SWITCH #( me->get_emp_id( )
                WHEN 1 THEN |Full-Time|
                WHEN 2 THEN |Part-Time|
                WHEN 3 THEN |Contractor|
                WHEN 4 THEN |Casual|
                ELSE THROW cx_no_data_found(
                            rel_proc_id = CONV #( me->get_emp_id( ) ) )
              ).
  ENDMETHOD.

  METHOD get_phone.
    r_result = SWITCH #( me->get_emptyp( )
                WHEN `Full-Time` THEN |1234567890|
                WHEN `Part-Time` THEN |5678901234|
                WHEN `Casual`    THEN |7890123456|
                ELSE |0399999999|
              ).
  ENDMETHOD.

  METHOD get_salary.    
    r_result = SWITCH #( me->get_emptyp( )
                 WHEN `Full-Time` THEN 50000
                 WHEN `Part-Time` THEN 25000
                 WHEN `Casual`    THEN 5000
                 ELSE THROW cx_no_data_found(
                            rel_proc_id = CONV #( me->get_emp_id( ) ) )
               ).
  ENDMETHOD.

  METHOD get_emp_id.
    r_result = me->emp_id.
  ENDMETHOD.

ENDCLASS.

And the calling program (the consumer) is different as well.

Report ztest.

DATA extract_t TYPE lcl_employee=>empl_data_t.
DATA error_t   TYPE string_table.

START-OF-SELECTION.

 DATA(all_employees_t) = VALUE int4_table(  ( 1 ) ( 2 ) ( 3 ) ( 4 ) ( 5 ) ).

  LOOP AT all_employees_t REFERENCE INTO DATA(dref).
 
   DATA(ref) = NEW lcl_employee( dref->* ).
   
   TRY.
      DATA(extract) = VALUE lcl_employee=>empl_data(
                        empid = ref->get_emp_id( )
                        emptyp = ref->get_emptyp( )
                        phone  = ref->get_phone( )
                      ).
 
      CATCH cx_no_data_found INTO DATA(lx_no_data).
        error_t = VALUE #( BASE error_t ( lx_no_data->get_text( ) ) ).
        CONTINUE.
    ENDTRY.
   
"Note that the call to the method GET_SALARY( ) is separated 
    TRY.
      extract = VALUE #( BASE extract
                   salary = ref->get_salary( )
                 ).
      CATCH cx_no_data_found INTO lx_no_data.
    ENDTRY.

    INSERT extract INTO TABLE extract_t.
  ENDLOOP.

  cl_demo_output=>new( )->write( extract_t )->write( error_t )->display( ).

 

Flexibility of the Resumable Exceptions

Now another similar requirement comes along in the near future but this time the Contractor group is required to be excluded from the report when their salary records are not found in the system and they need to be reported as exceptions as well. All other requirements remain the same.

New Implementation

    – Changes to the Resumable Exception handling

To meet this requirement, the developer who used the Resumable option achieves it very quickly by making minimal changes to the calling program as seen in the code snippet below.

The only change he does is that he handles the Resumable exception (that was raised by the method) as a normal exception object without having to modify the local class; but most importantly, he does not disturb the main processing of the calling program (the consumer).

Report ztest_new.

DATA extract_t TYPE lcl_employee=>empl_data_t.
DATA error_t   TYPE string_table.

START-OF-SELECTION.

 DATA(all_employees_t) = VALUE int4_table(  ( 1 ) ( 2 ) ( 3 ) ( 4 ) ( 5 ) ).

  LOOP AT all_employees_t REFERENCE INTO DATA(dref).
    TRY.
      INSERT NEW lcl_employee( dref->* )->get_data( ) INTO TABLE extract_t.

"This time around it was not handled as a Resumable Exception 
     CATCH cx_no_data_found INTO DATA(lx_no_data). "<--Handled normally
       error_t = VALUE #( BASE error_t ( lx_no_data->get_text( ) ) ).
    ENDTRY.
  ENDLOOP.

cl_demo_output=>new( )->write( extract_t )->write( error_t )->display( ).

 

   – Changes to the Non-Resumable Exception handling

Now the implementation for the calling program (the consumer) needs to change to accommodate the changes to the business requirement.

The developer who did not use the Resumable option has to now modify the processing logic of the consumer to accommodate the changes in the business requirement.

Report ztest_new.

DATA extract_t TYPE lcl_employee=>empl_data_t.
DATA error_t   TYPE string_table.

START-OF-SELECTION.

DATA(all_employees_t) = VALUE int4_table(  ( 1 ) ( 2 ) ( 3 ) ( 4 ) ( 5 ) ).

LOOP AT all_employees_t REFERENCE INTO DATA(dref).

  TRY.
    INSERT VALUE #( 
            LET ref = NEW lcl_employee( dref->* ) IN
             empid  = ref->get_emp_id( )
             emptyp = ref->get_emptyp( )
             phone  = ref->get_phone( )
             salary = ref->get_salary( )
           )
    INTO TABLE extract_t.

   CATCH cx_no_data_found INTO DATA(lx_no_data).
      error_t = VALUE #( BASE error_t ( lx_no_data->get_text( ) ) ).
  ENDTRY.

ENDLOOP.

cl_demo_output=>new( )->write( extract_t )->write( error_t )->display( ).

 

Conclusion

We have demonstrated the difference between raising and handling of an exception object with and without the Resumable addition/option.

We have also demonstrated the benefits of using Resumable exceptions to simplify developments.

Resumable option of the Exception object is a powerful feature and they are extremely flexible yet simple to implement. They can save developers a lot of time and effort especially in regards to reusability of common objects / framework in the system as well as simplifying implementations across the SAP system landscape.

In the next blog of this series we will focus on some of the important principles of OO development and apply those concepts on the modern (smart) ABAP.

25 Comments
You must be Logged on to comment or reply to a post.
  • Thank you for this example. Resumable exceptions are available since ABAP 7.02 ("more than a decade" as you say).

    More information in the ABAP documentation:

    Demo program:

    • DEMO_CATCH_EXCEPTION

    Other blog posts:

  • Hello,

    Thanks for sharing, one quick question here:

          extract = VALUE #( BASE extract
                       salary = ref->get_salary( )
                     ).

    What is the idea? i mean what is the result to get here?

    But if i remember well the command extract is obsolete syntax. is the same command?

    Thanks

    • Hi Javier,

      DATA(extract) = VALUE lcl_employee=>empl_data(
                              empid = ref->get_emp_id( )
                              emptyp = ref->get_emptyp( )
                              phone  = ref->get_phone( )
                            ).

      extract is a workarea of type lcl_employee=>empl_data. I'm also not familiar with any extract command 🙂

      extract = VALUE #( BASE extract
                         salary = ref->get_salary( )
                       ).

      The idea above is to copy the value of the salary field into the same salary field of the workarea extract without touching the other fields of the workarea extract.

      Regards,

      Sougata.

  • That is perfect example. ABAP should be written in a way that it resembles a natural spoken language. And thanks to the new updates, all of these are possible. Less code and more meaning is the key to it and of course with such flexibility as demonstrated.

    Just my 2 cents. These statements which provides a boolean output can be directly added in a IF statement without have to check its result.

    Eg.

    IF lx_no_data->is_resumable IS NOT INITIAL. 

    can be written as

    IF lx_no_data->is_resumable

    Thanks for sharing this and hope to see more content from you.

    Regards
    Gurpreet

  • My bad. It does not work for me either. I guess it only works for methods which returns boolean value like below. Hopefully in upcoming releases this will be taken care of.

    REPORT  ztest_gj.
    
    CLASS lcl_mpc DEFINITION CREATE PUBLIC.
      PUBLIC SECTION.
        METHODS constructor.
        METHODS is_valid
          IMPORTING i_read_this        TYPE char10
          RETURNING VALUE(rv_answer) TYPE abap_bool.
    ENDCLASS.
    
    CLASS lcl_mpc IMPLEMENTATION.
      METHOD constructor.
    
      ENDMETHOD.
      METHOD is_valid.
    * Some validation logic
        IF i_read_this NE space.
          rv_answer = abap_true.
        ENDIF.
      ENDMETHOD.
    
    ENDCLASS.
    
    
    START-OF-SELECTION.
      DATA(lo_model) = NEW lcl_mpc( ).
      IF lo_model->is_valid( 'check this' ).
        WRITE 'All good'.
      ELSE.
        WRITE 'Not valid'.
      ENDIF.

    Regards
    Gurpreet

    • I remember when the IF conditional_method( ) syntax came in (as a result of a suggestion in this forum). Horst Keller explained then why you never get IF ref->conditional on its own. Something to do with backwards compatability.

  • Ha ha, funny.

    The resumable exception. Sounds like the ignorable stop sign(and wrong way sign). This proofs again: Traffic signs have only advisory function.

    Thank god traffic rules are not designed by ABAP OO architects.

  • I appreciate your effort, but IMO a business language should be understandable by business-types.  Sorry, but the above code to me is "forward into the past" gobbly gook, like C/Java/JS and the rest of the C-derivatives.  IMO being professional is to make code readable to those who are not experts in ABAP (newer or older variants) as well as to those who are not privy to the specifications.  To me, that means LOTS of comments no matter how readable you may think that the code is.  Code without comments is not just impolite, but also unprofessional to those who follow (which might just be you a year down the road).  That is smart ABAP to me.

    • Thanks for your comments C Currey . You are not alone, I come across developers every day who are unwilling to change their ways for what they had been doing for decades; the word "Change" scares the living daylight out of them.

      As for your comment on the (LOTS of) comments part, in the source code examples enumerated in this blog the method names are descriptive enough to understand their purpose; the processing logic and the design of the application is discussed under the Technical Design part of this blog which you may have overlooked.

      • Ok, then we disagree.  I always write lots of comments describing the "what" is to be done and not just the technical "how".  No matter how self-descriptive I think that the code may be method/variable names etc. may not be to others.  What is descriptive enough to me when I am in the weeds of the project may fall short later.  Also, I want a functional consultant to be able to skim the comment blocks and have an idea of what is beng done and why.  Thereafter, I use these comments as the base to write the online help (e.g. Ctrl+Shift+F9).

        I have been using the "newer" 7.4+ ABAP syntax for a good five plus years now, OO in ABAP before that along with ADT/Eclipse.  I like most of the newer syntax for brevity and the new functionalities, but I am not a fan when it makes the code less like English.  I have to code so that others in my organization can know what is going on, too. Change is not foreign to me nor I notice to most developers, quite the opposite.  TDD I like to use for making the code less fragile even though it takes longer at first.

        BTW, my first technical manager made us write the user manual and have that presented to the end users and accepted before we were allowed to code even one line.  I hated it, but there was less miscommunication and change requests/fixes later.  KISS (keep it simple, stupid) principle.

        Again, I like your blogs and thanks for doing them.

        • Hello C Currey,

          I understand both aspects, but we belong more to the faction: the code must be self-explanatory.

          Bad comments (superflous?)degrade the readability and comprehensibility of the code
          Comments make the most sense to understand why you do something in the place and not after.
          But you never stop learning.
          Can you maybe give a concrete example how you would document the code of Sougata Chatterjee ?

          For example, we find such comments rather disadvantageous for the code readability and comprehensibility:

          "setting matrial number to the filter
          my_filter_object->set_material_no( other_obj->get_material_no( ) ).
          
          "setting date from to the filter
          my_filter_object->set_date_from( other_obj->get_creation_date( ) ).

           

          • I guess that puts me in a different "faction" then. ;>)

            Your example is to me why "self-explanatory" objects still need comments,
            especially when objects are chained. To my non-OO colleagues, functional
            colleagues, or even "future-me" in 10 months is it immediately clear if I am reading or writing a material number and date somewhere?  I would rather be lazy in the future and read the comments rather than lazy at development time and not write comments.  Maintaining the comments I see as just keeping code from becoming brittle and keeping it up to date.

            *--------------------------------------------------------------------------------------*
            * Store material number w/which we are working in an instance variable.
            *--------------------------------------------------------------------------------------*
            my_filter_object->set_material_no( other_obj->get_material_no( ) ).

            *--------------------------------------------------------------------------------------*
            * Store date of material number w/which we are working in an instance variable.
            *--------------------------------------------------------------------------------------*
            my_filter_object->set_date_from( other_obj->get_creation_date( ) ).

          • :>) Thanks for the insight.
            It's certainly also dependent on the company context.
            We, model software since ~25 years according to the OOA/OOD principle.
            Started with Smalltalk then Java, etc.
            So it is normal that almost all colleagues who have to look at code or change it, understand the OO principles and notation.

            One of our important rules is: a method should be completely readable on the current monitor resolution without scrolling (the monitors are horizontally oriented :)).

            This is not always possible, but most of the time it is possible.