ABAP Dependency Injection – An implementation approach
In this blog I want to show you some concepts of a solution for a “Dependency Injection”(DI)-approach which is used in one of my recent projects. It consists of a DI-Framework written in ABAP and an integration-pattern to get the framework working in a large component-based ABAP-application. The DI-framework is custom-made to fit the needs of the chosen design-approach. I will focus on the parts which helps you to organize your DI-container in a component based software design.
I won’t give you an introduction to DI but rather explain an implementation approach to fit into the abap world. If you’re looking to understand the basics of DI you should read “The issue with having many small classes” and “Dependency Injection for ABAP” or even the wikipedia page for DI would be a good start.
Getting to the requirements
In my recent project I was looking for a flexible solution of managing my OO-instances that build up my applications. While a factory pattern only describes how to design a factory for access by a client, it doesn’t describe how the factory should internally manage instances that it produces. Most of the time you will put a lot of “create object” statements into your factory to set up your application. By this your application-“set up” will be fixed.
Also if you follow the SOLID-Principals in your software-design you will soon face problems described in the blog “The issue with having many small classes”. You have a lot of small classes and you have to write code for instance-creation and management in your factory-classes. This could become annoying.
Another drawback is that it’s hard to reuse your factory since your application-“set up” is fixed and you can’t replace certain classes for special purposes.
So the solution is to combine the factory pattern with a DI-framework. The DI-framework handles the instance management and the factory-pattern provides a way to access certain instances. The benfit is, that you keep the framework-code inside the factory and out of your business code. Someone who uses your factory doesn’t even have to know about the DI-framework.
So in the end the framework fulfills the following reqirements
- Quick/easy to set up
- Debugging possibilities
- Support for all DDIC-Datatypes
- No dependencies in business code (i.e. not forcing to inherit some class)
- Class creation (with “create object” or factory methods)
- Class configuration (constructor-based or with setter-methods)
- Modularization to match a component based architecture
- Support for “where-used list”
- Replacing classes in a given setup
I will first start to give you a quick introduction into the DI-framework and how the basic usage is.
The following class-diagram shows the setup for the examples (“zif_*” for interfaces; “zcl_*” for classes).
“zcl_service_a” needs some other classes to get its job done. “zcl_persistence_a” implements some kind of database access and “zcl_calulate_a” provides some kind of calculation.
If you want to use “zif_service_a” you first have to instantiate “Persistence A” and “Calculate A”. Then you can create “Service A”.
The ABAP code would look like this (In ABAP NW740 syntax)
DATA(lo_persistence) = NEW lcl_persistence_a( ). DATA(lo_calculate) = NEW lcl_calculate_a( ). DATA(lo_service) = NEW lcl_service_a( io_pers = lo_persistence io_calc = lo_calculate ).
This is quite simple. You could put that into your factory and you would be fine. But if your component grows and involves more and more classes the task of putting them together gets annoying. Especially if you tend to create small classes.
On the other hand it gets difficult to reuse your setup (the way you put your classes together). If you want to use your service in a slightly different context and want to replace i.e. the calculate-implementation by your own you have to copy the whole factory and replace the line where you create the calculate-instance. Some nicer approach would be to say: “do everything as is, but instead of your calculate implementation take mine”.
With the DI framework you would do something like this. In the first step you would create the DI-container:
" Create DI-Container DATA(lo_container) = zcl_di_container=>create_instance_default( ).
Then you would register all your classes in the container, so the container has a bulk of classes to work on.
" Register classes into container lo_container->register_classname( iv_classname = 'ZCL_SERVICE_A' ). lo_container->register_classname( iv_classname = 'ZCL_PERSISTENCE_A' ). lo_container->register_classname( iv_classname = 'ZCL_CALCULATE_A' ).
Now you can query the container to get an instance of your “Service A”:
DATA lo_service TYPE REF TO zif_service_a. lo_container->get_instance_value( CHANGING cv_target_value = lo_service ). " Use your instance lo_service->do_something( ... ).
The container analyzes the type of the variable “lo_service”. Since it is “zif_service_a” the container looks for a registered class that matches this interface. This would be “zcl_service_a”. It analyzes the constructor of this class and looks for classes that match the type of the specific parameters. So it will first create instances of “zcl_persistence_a” and “zcl_calculate_a” before it creates an instance of “zcl_service_a”. The instance will be copied into the variable “lo_service”.
The construction and analyzing process is done recursively and it can detect possible inifinite recursions. You can even use other datatypes on your parameters like int, string, structure or tables, which also have to be registered in the container.
The code above would be placed into some kind of factory. While the registration code would be placed into the constructor, the querying code would be placed into the factory methods.
CLASS zcl_factory_a IMPLEMENTATION. METHOD constructor. ao_container = zcl_di_container=>create_instance_default( ). ao_container->register_classname( iv_classname = 'ZCL_SERVICE_A' ). ao_container->register_classname( iv_classname = 'ZCL_PERSISTENCE_A' ). ao_container->register_classname( iv_classname = 'ZCL_CALCULATE_A' ). ENDMETHOD. METHOD get_service_a. " ** RETURNING VALUE(ro_service_a) TYPE REF TO zif_service_a. ao_container->get_instance_value( CHANGING cv_target_value = ro_service_a ). ENDMETHOD. ENDCLASS.
If you want to replace a specific class you can do it by inheriting the factory-class and register your own implementation. I.e. if you want to replace the implementation “ZCL_SERVICE_A” of interface “ZIF_SERVICE_A” by your own implementation “ZCL_SERVICE_A_BETTER” you can do this:
CLASS lcl_factory_a_better IMPLEMENTATION. " INHERITING FROM zcl_factory_a METHOD constructor. super->constructor( ). ao_container->register_classname( i_var_classname = 'ZCL_SERVICE_A_BETTER' ). ENDMETHOD. ENDCLASS.
The container acts like a stack and every new registered datatype or class would be put on top of this stack. If you query for a specific type the container checks for matches from top to bottom. The first match will be returned and used. Since your own class ZCL_SERVICE_A_BETTER is registered last it is on top of the stack. So it will be the first matching type for the interface “zif_service_a” and will be selected. By this way you can replace specific classes.
If your container contains a large amount of classes you may want to check if instances are bound to the correct parameters. The di-framework provides a possibility to trace the querying of the container. The trace is rendered into a graph. The following picture shows the graph from the small example above:
You can see that both parameters of the constructor from “zcl_service_a” are bound to the correct classes.
Another concept realized in this DI-framework are namespaces. In an application that consists of different components each component would need its own container. That’s because you can’t put every class of every component into a single container. This would result in conflicts between different components which implement shared interfaces.
Another option is to use namespaces. Each component uses the same container-data-core but uses a different namespace within this container. The namespaces are isolated from each other so that conflicts are avoided. If you want to use a class of another namespace you can set an alias to the other namespace.
The following example shows the concept of namespaces and aliases.The following class-diagram shows “Service B”:
You can see that “Service B” uses “Service A”. Furthermore it implements “zif_calculate” which is also implemented by “Service A”. The code to set up the DI-container for this scenario would be the following:
DATA(lo_ctx_data) = NEW zcl_di_context_data( ).
The instance of “lo_ctx_data” is the core of each container. It contains the registered classes and datatypes. If you have different container-instances which share this instance of “lo_ctx_data” they will all act on the same container data.
If you create a container you can set up a default namespace. If you register classes they will all be placed into the containers default namespace. “Service A” would be set up in the following way:
DATA(lo_container_a) = /abk/cl_di_container=>create_instance_default( i_var_namespace = 'urn:a' i_obj_context_data = lo_ctx_data ). " Register classes into container lo_container_a->register_classname( iv_classname = 'ZCL_SERVICE_A' ). lo_container_a->register_classname( iv_classname = 'ZCL_PERSISTENCE_A' ). lo_container_a->register_classname( iv_classname = 'ZCL_CALCULATE_A' ).
“Service B” is set up in the following way. It will reuse the previously created instance of “lo_ctx_data”:
DATA(lo_container_b) = /abk/cl_di_container=>create_instance_default( i_var_namespace = 'urn:b' i_obj_context_data = lo_ctx_data ). " Register classes into container lo_container_b->register_classname( iv_classname = 'ZCL_SERVICE_B' ). lo_container_b->register_classname( iv_classname = 'ZCL_PERSISTENCE_B' ). lo_container_b->register_classname( iv_classname = 'ZCL_CALCULATE_B' ). lo_container_b->register_alias( iv_typename = 'ZIF_SERVICE_A' iv_query_namespace = 'urn:a' ).
If you retrieve “Service B” …
DATA lo_service TYPE REF TO zif_service_b. lo_container_b->get_instance_value( CHANGING cv_target_value = lo_service ).
… you will get an instance constructed like in the following picture:
(Click for a larger image)
You can see two namespaces. “urn:a” for “Service A” and “urn:b” for “Service B”. “Service B” has a registered “alias” which connects the two namespaces on the type “zif_service_a”. So just the type “zif_service_a” of namespace “urn:a” is visible in namespace “urn:b”. So you can see that the namespaces are clearly isolated to each other and the querying and construction is restricted to its namespace unless you set an explicit alias.
The last concept I want to show are modules. Modules are a descriptive way to set up a container. You can describe five different aspects with a module:
- Import dependent modules
- Public module items
- Container registration
The following code shows an example for a module for “Service A”:
CLASS zcl_module_a IMPLEMENTATION. METHOD zif_di_factory_module~get_modulename. rv_module_name = 'Service A'. ENDMETHOD. METHOD zif_di_factory_module~get_default_namespace. rv_namespace = 'urn:a'. ENDMETHOD. METHOD zif_di_factory_module~register_imports. " nothing ENDMETHOD. METHOD zif_di_factory_module~register_interface. io_container_mng->register_alias( iv_typename = 'zif_service_a' iv_query_namespace = 'urn:a' ). ENDMETHOD. METHOD zif_di_factory_module~register_classes. io_container_mng->register_classname( iv_classname = 'ZCL_SERVICE_A' ). io_container_mng->register_classname( iv_classname = 'ZCL_PERSISTENCE_A' ). io_container_mng->register_classname( iv_classname = 'ZCL_CALCULATE_A' ). ENDMETHOD. ENDCLASS.
Modules can hold several namespaces. But there is always one namespace that acts as default namespace. Every class or datatype is registered with the default-namespace unless a namespace is explicitly provided on registration.
A module can build a relationship to another module in order to get access to public services (registered classes) provided by the other module. This is done in the “register_import”-Method. I.e. in the previous section “Service B” imports a service from “Service A”. In terms of modules this would be setup by a relationship between these modules. So “Service B” would import module “Service A”. In the module declaration of “Service B” you would find this method implementation:
METHOD zif_di_factory_module~register_imports. io_registry_imports->register_factory_module( NEW zcl_module_a( ) ). ENDMETHOD.
The method “register_interface” describes which registered types of the module should be public and imported into another namespace once the module is imported elsewhere. The method receives the DI-container of the foreign module and can register aliases into the foreign container. The aliases should point to the modules own namespace. By this namespaces of two modules get connected and dedicated registered types are accessible in the foreign container. So this method describes some sort of public interface of the module.
The final method “register_classes” fills up the own default-namespace with classes and datatypes. Every class and datatype has to be registered there, which should be considered on construction of instances by the DI-framework for this module. Into this method you can also place factory classes which are not DI based. So you don’t have to use the feature of importing modules to get access to instances of another components.
If you want to use a module you can do it with this line:
DATA(lo_container) = zcl_di_factory_generic=>get_instance( io_init_module = NEW zcl_module_b( ) )->get_di_container( ).
Now you can retrieve your instance like shown in the previous examples:
DATA lo_service TYPE REF TO zif_service_b. lo_container->get_instance_value( CHANGING cv_target_value = lo_service ).
The benefits of using modules are that …
- you have a managed way how different components are connected to each other.
- you don’t have to care about creating and managing the DI-container.
- every module is only imported once. So if n modules import the same module its content is not registered n times but just once.
- since the module-class is used in a descriptive way, you can dynamicly replace modules.
Finally the DI-implementation provides a code generator which makes module creation a bit easier. So there is a configuration for the SAP “Service Implementation Workbench” (SIW) that can create the module-code and wraps it into a nice factory-class to provide a type-safe access to the DI-container and hiding the DI aspects from users. The SIW generates your factory and module-code and you just have to fill the five module-methods. In the end you have a nice working DI-based factory-class.
This blog could not handle every aspect of this DI-framework. But I hope I could show you the intended design approach to fit in a large component based application.
Please let me know what you think!
Edit (6.1.2013): Some parts in section DI-Modules