Dependency Injection for ABAP
Dependency Injection for ABAP:
Loose Coupling and Ease of Testing
What Is Dependency Injection?
Unit testing is often made more diﬃcult by the heavy use of classes as namespaces and the proliferation of static methods to encapsulate conﬁguration 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 oﬀers a partial solution to our problem, by oﬀering 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.
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.)
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.
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.
test_mode. ” indicates we want a class tagged with test tag
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.
pure_mode. ” indicates we want a class tagged with default tag
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.
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.
mif_app type ref to zbigapp_if_cntrl.
test_printing for testing.
class bigapp_app_unit_test implementation.
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.
* instantiate the application model. The injection is performed
* inside of this call.
lif_mod = zbigapp_cl_model_fact=>load(
i_key = ‘0042/DMO/000/00’ ).
* instantiate the application controller.
mif_app = zbigapp_cl_cntrl_fact=>create(
i_model = lif_mod ).
* 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.
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.
act = zbigapp_test_device=>m_printdata_ok( )
exp = ‘X’
msg = ‘Test device reports print data was not ok’).
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.
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.
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).
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.
append lvc_ifcl_item to li_ifcl_items.
get_imp_classes( ” classes that implement the passed in interface
i_interface = lvc_ifcl_item
e_classes = li_ifcl_items
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.
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.
i_classlist = li_ifcl_items
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.
* if we’re in test mode, read first test class if exists.
* if we’re in pure mode, read first default class if exists.
* see if an alternative class exists.
* see if we have a class marked as standard.
* fall through, when no keywords found just return first class.
read table li_ifcl_items into lvc_ifcl_item
if lvc_ifcl_item is initial.
Next, we instantiate the class we want to use, and put the reference into c_ref.
create object c_ref type (lvc_ifcl_item).
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.
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
lvs_msg type string.
*> &1 ref to interface or class
changing c_ref = &1 ).
assert &1 is not initial.
*> &1 obj to test
lvs_msg = &1->test( ).
* we have the business logic interface:
* we have business logic classes implementing ZDI_IF:
* default (standard, from us) ZDI_DEFAULT
* alternative (customer) ZDI_ALTERNATIVE
* test (for units) ZDI_TEST
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.
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
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
lif_di5. ” instantiates default class ZDI_DEFAULT
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
Thank you for such an elaborate post. It's a bit too much for me to digest all at once (bookmarked for later reading), but I can certainly appreciate the effort that went into the blog. Nice formatting on the notes, I shall steal it. 🙂
Couldn't see the UML diagram though, it just shows a placeholder, hm...
Hi Jack, this looks really interesting. However, I can't get the test report to run. It always dumps while trying to create an instance (exception "NOT_A_REFERENCE").
Could you please give me an update on this?
That was a really good article. It has taken me a long time to get my head round what dependency injection is, I though it was all to do with unit testing, but it is far more.
I downloaded your code, and because I am what I am, I had to totally re-write it, but i could not have done it if I was not shown the example first. Now I know what this is for, it's creating objects with a lot less effort i.e. lines of code.
I can move from:-
... to doing the same thing without the data declartions and two of the three CREATE statements....
In this example I am still using this for unit testing, but I could just as easily have instructed the program to give me a subclass. If given no instructions at all my "injector" will just create a class of the type in the constructor.
I don't know if this is better or worse than the approach you outlined, but it fits my particular needs.