Application Development Blog Posts
Learn and share on deeper, cross technology development topics such as integration and connectivity, automation, cloud extensibility, developing at scale, and security.
cancel
Showing results for 
Search instead for 
Did you mean: 
Former Member



Dependency Injection for ABAP:

Loose Coupling and Ease of Testing

Overview

What Is Dependency Injection?

Unit testing is often made more difficult by the heavy use of classes as namespaces and the proliferation of static methods to encapsulate configuration code.

Dependency injection is a design pattern that shifts the responsibility of resolving dependencies to a dedicated dependency injector that knows which dependent objects to inject into application code. Dependency injection offers a partial solution to our problem, by offering an elegant way to plug in either the new objects taking over the responsibilities of static methods, or others required for testing purposes.

[Niko Schwarz, Mircea Lungu, Oscar Nierstrasz, “Seuss: Decoupling responsibilities from static methods for fine-grained configurability”, Journal of Object Technology, Volume 11, no. 1 (April 2012), pp. 3:1-23, doi:10.5381/jot.2012.11.1.a3.]

Why Use Dependency Injection?

Some of the benefits of dependency injection in the ABAP context include:

  • Loose coupling of code
    • An application knows about an interface, and only deals with the interface. The dependency injection system is responsible for hooking the interface reference up to an actual instance of functionality.
  • Ease of testing
    • The dependency injection system can be triggered so that it only instantiates test implementations and mock functionality. This means a developer can test the real system with some functionality switched out, easing testing of parts of a system.

In a real world scenario, dependency injection could be used in parts of an application such as accessing configuration data or handling reading and writing of application data to the database. In a testing scenario, we need to test using a specific configuration for predictable results. We also may not want to save data on the database, and we may depend on specific data being used for tests. Using dependency injection, we can ensure a known and stable configuration is used during the test, and use specific mock data provided instead of data directly from the database, to make sure the data is processed as required.

Dependency Injection in Action

Using RTTI and some Object Oriented Data Dictionary metadata stored within SAP, a straightforward and easy to use approach can be taken to implement this functionality. In this section we have code snippets outlining the approach taken, including UML diagrams. The entire working example can also be downloaded for installation and experimentation.

An Example Report

We will start with an example of dependency injection in action using a basic report. For our simple program, we have an interface containing some business logic, in this case configuration data, called zdi_if_config. We have several implementations of the interface, each of which is intended for a different scenario. For everyday use, we want our application to instantiate the default class, zdi_if_config_def. Another department would like the functionality to behave a little differently, but we don’t want to go straight in and modify our default class, so we create zdi_if_config_cust which descends from the default class and replaces some of the functionality. Finally, when we’re testing our application we want to modify how the default class works, returning test data so we can be sure the environment is not affecting how the application runs – for example, providing a static configuration so we know exactly what behaviour to expect when testing. For this purpose, we have the zdi_if_config_test class.

[UMLRelationshipHierarchy.png]

The application itself is not responsible for instantiating instances of our interface – this is passed off to the dependency injection system, which makes a decision from the available implementations of an interface and instantiates an instance of a class accordingly.



Note: It is worth highlighting the fact that despite discussing the different implementing classes above, the program itself knows nothing of these implementations, and contains no references at all to these types. From the point of view of the program, the interface zdi_if_config is the only known type and the only type dealt with. This is one of the benefits of dependency injection – the program is now very loosely coupled to the implementation of the functionality. All the program needs to know is that there’s a contract for some behaviour it needs, how that contract is fulfilled doesn’t matter to the program.


For this example, we have a data variable which references our interface. The dependency injection system will automatically create an instance for the variable, the underlying functionality depending on the scenario (in this case, a declaratively defined mode.)

 
data:
    lif_config
type ref to zdi_if_config.


We start in standard mode – here we always use the default class unless an alternative implementation has been specified. We will inject an instance into the variable, and the expected outcome is a reference containing an instance of the alternative class.

  inject:
    lif_config.
" instantiates alternative class

In this case, lif_config will contain an instance of zdi_if_config_cust. Next, we will go into test mode, and repeat the same procedure. When asking for a reference to our interface, the system finds the test class and instantiates this. 

  clear lif_config.
  test_mode.
" indicates we want a class tagged with test tag
  inject:
    lif_config.
" instantiates test class

In this case, lif_config will contain an instance of zdi_if_config_test. Finally, we will go into pure mode. Here, we want to ignore any alternative implementations and use only default implementations. Pure mode is useful for when we have developed alternative implementations, but need to use the original default implementation for some reason.

  clear lif_config.
  pure_mode. " indicates we want a class tagged with default tag
  inject:
    lif_config.
" instantiates default class

In this case, lif_config will contain an instance of zdi_if_config_def.



Note: The different modes have been declaratively defined, but they could just as easily be configured, allowing dynamic alteration of how the application instantiates instances without code changes. The actual functionality to use could also be configured, for example specifying that a certain class should be instantiated when a specific user requests an instance for a specific interface.


Unit Testing

In normal operation, the injection would be performed only once per scope. The functionality may be needed in several places throughout the application, and the variable would be injected in all the places required. If a test scenario is needed, the DI test mode would be entered once at the beginning of the unit test (during set-up) and then the application objects would be instantiated. Every time the application requested an instance for the configuration, a test instance would be provided by the DI.

class bigapp_app_unit_test definition for testing.
 
private section.
   
class-data:
      mif_app
type ref to zbigapp_if_cntrl.
   
class-methods:
      class_setup.
   
methods:
      test_printing
for testing.
endclass.

class bigapp_app_unit_test implementation.
 
method class_setup.

   
data:
      lif_mod
type ref to zbigapp_if_model.


* safe in the knowledge that the app will use the test configuration
* and any other test injectables we have, such as database persistence
* overriding classes or a dummy user interface implementation.
    test_mode.

* instantiate the application model. The injection is performed

* inside of this call.

    lif_mod = zbigapp_cl_model_fact=>load(
     
exporting
        i_key =
'0042/DMO/000/00' ).

* instantiate the application controller.
    mif_app = zbigapp_cl_cntrl_fact=>create(
     
exporting
        i_model = lif_mod ).

 
endmethod.

 
method test_printing.

* make sure the test device is configured for the app.
* the real IMG configuration may have an alternative value
* for the current user, but the test configuration class
* overrides this so weuse a specific device during testing.


    cl_aunit_assert=>assert_equals(
      act = mif_app->m_device
     
exp = 'TEST_DEVICE'
      msg =
'Test device was not configured for application').


* tell the application to print some data.
    mif_app->action( zbigapp_if_cntrl=>mcc_print ).


* check our test device to see whether the data was ok.
    cl_aunit_assert=>assert_equals(
      act = zbigapp_test_device=>m_printdata_ok( )
     
exp = 'X'
      msg =
'Test device reports print data was not ok').
 
endmethod.   
endclass

Implementation Approach

Here we will look at the implementation of the injection functionality. The inject command is simply a macro that passes the reference along to a static class method called zdi_cl_injection=>create_inst( ). The single changing parameter is called c_ref, of type any.

method create_inst.

 
data:
    li_ifcl_items   
type mt_classlist_tt,
    lvc_ifcl_item   
type mt_classitem,
    li_tags         
type mt_taglist_tt,
    lr_tag          
type mt_tagitem,
    lr_map          
type mt_classmap,
    lcl_descr_ref   
type ref to cl_abap_refdescr,
    lcl_abap_typdscr
type ref to cl_abap_typedescr.


First, we get some type information using the SAP RTTI system. We need to determine the type of the variable passed in, to make sure it’s a reference and get the dictionary name of the underlying interface or class.


* determine the dictionary type of the reference that was passed in.

  lcl_descr_ref ?= cl_abap_refdescr=>describe_by_data( c_ref ).
  lcl_abap_typdscr = lcl_descr_ref->get_referenced_type( ).

  lvc_ifcl_item = lcl_abap_typdscr->get_relative_name( ).

 
if lvc_ifcl_item is initial.
   
raise nondictionary_type.
 
endif.


Next, we check our cache of previously requested instances. If a class has already been determined for this functionality, then we simply instantiate the class and return the instance.


* check our cache..
 
read table mi_cache into lr_map
   
with key source = lvc_ifcl_item.

 
if sy-subrc eq 0.
   
create object c_ref type (lr_map-target).
   
return.
 
endif.

  lr_map-source = lvc_ifcl_item.


Next, we vary behaviour depending on whether the reference passed in is a reference to a class of interface. We are building up a list of potential instance candidates, so when a class is passed in we simply add it to the list (and we’ll look up descendants in a later step.) If an interface was passed in, we fetch a list of all classes that implement the interface (using table vseoimplem.)


* input can be a class or an interface whose imp'ing classes we want.
 
case lcl_abap_typdscr->type_kind.

   
when cl_abap_typedescr=>typekind_class.

     
append lvc_ifcl_item to li_ifcl_items.

   
when cl_abap_typedescr=>typekind_intf.

      get_imp_classes(
" classes that implement the passed in interface
       
exporting
          i_interface = lvc_ifcl_item
       
importing
          e_classes = li_ifcl_items
      ).

   
when others.
     
raise not_a_reference.
 
endcase.

Next, we explode the dependency tree of the classes we have collected (using table vseoextend.) This is required as the technique we use for selecting implementers of an interface only returns the classes directly implementing an interface, and not the descendants which do not explicitly define the implementation of the interface. Also, if a class reference was passed in then we need to fetch the potential candidates, which would consist of descendants of the class.

explode_descendents( " get entire descendant tree.
   
changing c_classlist = li_ifcl_items ).

 
if li_ifcl_items is initial.
   
raise no_implementing_classes.
 
endif.


Next, we analyse the class list and determine any tags associated with them (using table seotypepls, which contains the forward declaration list which we have hijacked for tagging!) Possible tags include being flagged as a default class, an alternative class or a test class.
 

fetch_class_tags(
   
exporting
      i_classlist = li_ifcl_items
   
importing
      e_taglist = li_tags
  ).


Next, we apply our selection logic – if we are in test mode, we look for a test class; if we are in pure mode we look for a default class; otherwise we look for an alternative class, failing that a default class, and failing that the first class in the list.


 
do.
   
case mvb_mode.
     
when mcc_mode_test.
* if we're in test mode, read first test class if exists.
        get_target_with_tag mcc_tag_tst.
     
when mcc_mode_pure.
* if we're in pure mode, read first default class if exists.
        get_target_with_tag mcc_tag_def.
   
endcase.

* see if an alternative class exists.
    get_target_with_tag mcc_tag_alt.

* see if we have a class marked as standard.
    get_target_with_tag mcc_tag_def.

* fall through, when no keywords found just return first class.
   
read table li_ifcl_items into lvc_ifcl_item
     
index 1.
   
exit.
 
enddo.

 
if lvc_ifcl_item is initial.
   
raise no_implementing_classes.
 
endif.


Next, we instantiate the class we want to use, and put the reference into c_ref.


 
try.
     
create object c_ref type (lvc_ifcl_item).
   
catch cx_sy_create_object_error.
     
raise instantiation_failed.
 
endtry.


Finally, we cache the name of the class and map it to the name of the type of the reference that was passed in.


  lr_map-target = lvc_ifcl_item.
 
append lr_map to mi_cache.

endmethod.

Downloads

Files containing the implementation can be found on the project page: https://bitbucket.org/zmob/di/downloads. This includes two SAPLink nuggets. Install NUGG_ZDI_IF.nugg first - you'll need the interface extension for SAPLink; then install NUGG_ZDI.nugg.

Next Steps (Future Articles)

  • Injecting constructor parameters on the fly
  • Configuration based selection of implementing functionality
  • Containers for managing lifecycle of objects

----

Test Report

report zdi_prog.

data:

  lvs_msg type string.

define inject.

*> &1 ref to interface or class

  zdi=>create_inst(

    changing c_ref = &1 ).

  assert &1 is not initial.

end-of-definition.

define test_mode.

  zdi=>test_mode( ).

end-of-definition.

define pure_mode.

  zdi=>pure_mode( ).

end-of-definition.

define prn.

*> &1 obj to test

  lvs_msg = &1->test( ).

  write lvs_msg.

  new-line.

end-of-definition.

start-of-selection.

* we have the business logic interface:

*   ZDI_IF

* we have business logic classes implementing ZDI_IF:

*   default (standard, from us) ZDI_DEFAULT

*   alternative (customer)      ZDI_ALTERNATIVE

*   test (for units)            ZDI_TEST

  data:

    lcl_di1 type ref to zdi_default,

    lif_di2 type ref to zdi_if,

    lcl_di3 type ref to zdi_default,

    lif_di4 type ref to zdi_if,

    lif_di5 type ref to zdi_if.

* the business logic class that is instantiated varies depending on the

* mode we are in. Within Unit tests, the test mode could be activated

* and any configuration and persistence classes could be switched out.

  inject:

    lcl_di1, " instantiates alternative class ZDI_ALTERNATIVE

    lif_di2. " instantiates alternative class ZDI_ALTERNATIVE

  test_mode. " indicates we want a class tagged with test tag

  inject:

    lcl_di3, " instantiates alternative class ZDI_ALTERNATIVE

    lif_di4. " instantiates test class ZDI_TEST

  pure_mode. " indicates we want a class tagged with default tag

  inject:

    lif_di5. " instantiates default class ZDI_DEFAULT

  prn:

    lcl_di1, " alternative - alternative is a descendant of default

    lif_di2, " alternative - alternative implements interface

    lcl_di3, " alternative - test class is not related to default

    lif_di4, " test - test implements interface

    lif_di5. " default - default implements interface and is tagged def

4 Comments