Wednesday, 7 March 2018

Using interfaces to standardize your ABAP OO Development

TLDR:

◈ You can utilize interfaces to move class functionalities to higher abstraction level
◈ You make code cleaner and nicer to look at
◈ Interfaces enable you to create mockup objects and increase code testability
◈ SOLID approach can be achieved
◈ With interfaces you make your class enabled for extension, but closed for modification
◈ Methods can use different object of differrent classes without using RTTS and casting unless they implement the same interface.

As i started to learn ABAP and had former experience with other object oriented languages i struggled with one missing feature here  – unability to overload methods.

What’s a method overloading you might ask?

It’s ability to use the same name of method, but with different arguments passed to the methods and with different implementations.

Without this feature, at some point, classes might become too large and it’s hard to keep track on all methods doing similar activities, but having different names.

Interfaces do not provide overloading abilities, yet they can tidy up and streamline your coding by limiting amount of methods with different names but similar functionalities.

In ABAP we have single class inheritance (so each class can have only one parent) and multiple interface implementation ability.

SAP ABAP Tutorials and Materials, SAP ABAP Certifications, SAP ABAP Learning

For example, presented above LCL_Child_Class inherits all not private variables, methods, types and constants from LCL_Parent_Class and must implement all functionalities from LINF_Utility and LINF_Saver interfaces.

To explain what’s an interface i will use “not that professional” description- it’s a class-like entity, but it carries no implementation of declared methods, yet it might carry over constants and types and variables. Interface cannot be initialized.

By default all methods of interface have to be implemented – this is general rule enforced by object-oriented programming creed. It’s possible to make interface method not obligatory to implement, but this will not be described in the scope of this blog post.

“Real life” use case.

Let’s imagine we have a program that needs to feed table SFLIGHT with data from various sources:

◈ Excel Upload
◈ RFC Upload
◈ Upload of modified and inserted lines during program runtime.

Of course we could also add ADBC sources and consumption of JSON/XML soruces fetched via HTTP client object to the list, but i just want to show the gist, not describe all possible examples in details.

As this is just a presentation of possibiilties – i didn’t create real working program start to finish.

Declaration of interfaces:

We are going to create two interfaces, but just one is really necessary in this example.

First one is the most important. I named it linf_sflight_career as this is local interface which is implemented by local classes for excel, rfc and local table carriers.

interface linf_sflight_carrier.
    types: tt_sflight type standard table of sflight with default key,
           st_sflight type sorted table of sflight with non-unique key mandt carrid connid,
           ht_sflight type hashed table of sflight with unique key mandt carrid connid fldate.
    methods: "! Returns hashed table SFLIGHT contents
             "! @parameter r_sflight |
             get_hashed_records returning value(r_sflight) type ht_sflight,
             "! Returns sorted table SFLIGHT contents
             "! @parameter r_sflight |
             get_sorted_records returning value(r_sflight) type st_sflight,
             "! Returns standard table SFLIGHT contents
             "! @parameter r_sflight |
             get_standard_records returning value(r_sflight) type tt_sflight.       
endinterface.

Interface carries various table types and three methods to be implemented by the classes of excel, rfc and table carrier.

Next interface will be implemented by class responsible for saving data to the database.

interface linf_sflight_saver.
    constants: "! Table lock types
               begin of lock_types,
                exclusive type enqmode value 'E',
               end of lock_types.
    constants: "! Scopes for table lock
               begin of scope_range,
                _2 type char01 value '2',
               end of scope_range.
    constants: _sflight type tablename value 'SFLIGHT'.                      
    methods: "! Save data from carrier object to SFLIGHT table
             "! @parameter i_carrier | Carrier object
             save_data importing i_carrier type ref to linf_sflight_carrier.
endinterface.

At this point you might ask – why do we need so many classes to fulfill such simple task? Why so many interfaces, when we could achieve similar effect utilizing class inheritance approach or even using single class for all the purposes.

Answer is obvious – SOLID. If you’d like to know more about this approach then please let me know in the comments and i will create another blog post purely about SOLID.

Returning to the topic –  now class presentation:

class lcl_excel_carrier definition.
    public section.
        interfaces: linf_sflight_carrier.
        aliases: tt_sflight for linf_sflight_carrier~tt_sflight,
                 st_sflight for linf_sflight_carrier~st_sflight,
                 ht_sflight for linf_sflight_carrier~ht_sflight,
                 get_hashed_records for linf_sflight_carrier~get_hashed_records,
                 get_sorted_records for linf_sflight_carrier~get_sorted_records,
                 get_standard_records for linf_sflight_carrier~get_standard_records.
    protected section.
    private section.
        data: standard_sflight type tt_sflight,
              sorted_sflight type st_sflight,
              hashed_sflight type ht_sflight.
endclass.
class lcl_excel_carrier implementation.
  method get_hashed_records.
    r_sflight = hashed_sflight.
  endmethod.
  method get_sorted_records.
    r_sflight = sorted_sflight.
  endmethod.
  method get_standard_records.
    r_sflight = standard_sflight.
  endmethod.
endclass.

class lcl_rfc_carrier definition.
    public section.
            interfaces: linf_sflight_carrier.
            aliases: tt_sflight for linf_sflight_carrier~tt_sflight,
                     st_sflight for linf_sflight_carrier~st_sflight,
                     ht_sflight for linf_sflight_carrier~ht_sflight,
                     get_hashed_records for linf_sflight_carrier~get_hashed_records,
                     get_sorted_records for linf_sflight_carrier~get_sorted_records,
                     get_standard_records for linf_sflight_carrier~get_standard_records.
    protected section.
    private section.
        data: standard_sflight type tt_sflight,
              sorted_sflight type st_sflight,
              hashed_sflight type ht_sflight.
endclass.
class lcl_rfc_carrier implementation.
  method get_hashed_records.
    r_sflight = hashed_sflight.
  endmethod.
  method get_sorted_records.
    r_sflight = sorted_sflight.
  endmethod.
  method get_standard_records.
    r_sflight = standard_sflight.
  endmethod.
endclass.

class lcl_table_carrier definition.
    public section.
            interfaces: linf_sflight_carrier.
            aliases: tt_sflight for linf_sflight_carrier~tt_sflight,
                     st_sflight for linf_sflight_carrier~st_sflight,
                     ht_sflight for linf_sflight_carrier~ht_sflight,
                     get_hashed_records for linf_sflight_carrier~get_hashed_records,
                     get_sorted_records for linf_sflight_carrier~get_sorted_records,
                     get_standard_records for linf_sflight_carrier~get_standard_records.
    protected section.
    private section.
        data: standard_sflight type tt_sflight,
              sorted_sflight type st_sflight,
              hashed_sflight type ht_sflight.
endclass.
class lcl_table_carrier implementation.
  method get_hashed_records.
    r_sflight = hashed_sflight.
  endmethod.
  method get_sorted_records.
    r_sflight = sorted_sflight.
  endmethod.
  method get_standard_records.
    r_sflight = standard_sflight.
  endmethod.
endclass.

Classes presented above have same functionality, but fully implemented classes would have some specific methods for each of their carrier purposes (like filtering, retrieving sflight data from raw format excel data et cetera.

All carrier classes implement linf_sflight_carrier – therefore we didn’t have to define methods all over again. I did however add aliases for better code readability

Next class we’re going to create is database saver called lcl_database_saver

class lcl_database_saver definition.
    public section.
        interfaces: linf_sflight_saver.
        aliases: lock_types for linf_sflight_saver~lock_types,
                 scope_range for linf_sflight_saver~scope_range,
                 save_data for linf_sflight_saver~save_data,
                 _sflight for linf_sflight_saver~_sflight.
    protected section.
    private section.
        methods: "! Creates table lock key for database lock
                 "! @parameter i_sflight_ref | Reference to SFLIGHT table line
                 "! @parameter r_varkey | Varkey returned
                 create_varkey importing i_sflight_ref type ref to sflight
                               returning value(r_varkey) type vim_enqkey,
                 "! Locks table using passed varkey
                 "! @parameter i_varkey | Table lock key
                 "! @parameter i_tabname | Table name
                 "! @parameter r_subrc | Information on lock creation. 0 = okay
                 lock_table_line importing i_varkey type vim_enqkey
                                           i_tabname type tablename default _sflight
                                 returning value(r_is_locked) type abap_bool,
                 "! Unlocks locked table line
                 "! @parameter i_varkey | Table lock key
                 "! @parameter i_tabname | Table name
                 unlock_table_line importing i_varkey type vim_enqkey
                                             i_tabname type tablename default _sflight.
endclass.
class lcl_database_saver implementation.
  method save_data.
        loop at i_carrier->get_standard_records( ) reference into data(standard_line).
            data(varkey) = create_varkey( standard_line ).
            if lock_table_line( i_varkey = varkey ).
                modify sflight from standard_line->*.
                unlock_table_line( exporting i_varkey  = varkey ).
            endif.
        endloop.
  endmethod.
  method lock_table_line.
    call function 'ENQUEUE_E_TABLEE'
      exporting
        mode_rstable   = lock_types-exclusive    " Lock mode for table RSTABLE
        tabname        = i_tabname    " 01th enqueue argument
        varkey         = i_varkey    " 02th enqueue argument
        _scope         = scope_range-_2
      exceptions
        foreign_lock   = 1
        system_failure = 2
        others         = 3.
     r_is_locked = xsdbool( sy-subrc = 0 ).
  endmethod.
  method unlock_table_line.
    call function 'DEQUEUE_E_TABLEE'
      exporting
        mode_rstable = lock_types-exclusive    " Lock mode for table RSTABLE
        tabname      = i_tabname    " 01th enqueue argument
        varkey       = i_varkey    " 02th enqueue argument
        _scope       = scope_range-_2.
  endmethod.
  method create_varkey.
    r_varkey = |{ i_sflight_ref->mandt }{ i_sflight_ref->carrid }{ i_sflight_ref->connid }{ i_sflight_ref->fldate }|.
  endmethod.
endclass.

And finally – we run the example:

initialization.
data(excel_carrier) = new lcl_excel_carrier( ).
data(rfc_carrier) = new lcl_rfc_carrier( ).
data(database_saver) = new lcl_database_saver( ).

try.
    database_saver->save_data( i_carrier = excel_carrier ).
catch cx_sy_assign_cast_illegal_cast.
catch cx_sy_assign_cast_unknown_type.
catch cx_sy_assign_cast_error.
endtry.

try.
    database_saver->save_data( i_carrier = rfc_carrier ).
catch cx_sy_assign_cast_illegal_cast.
catch cx_sy_assign_cast_unknown_type.
catch cx_sy_assign_cast_error.
endtry.

As you can see by moving abstraction to an interface level we can assure that literally ANY class correctly implementing interface linf_sflight_carrier can be passed to saver method and be processed correctly.

Another advantage of this approach is ability to create mockup objects in fast and easy way for your unit tests. Testable code – better code.

No comments:

Post a Comment