Dependency Injection with ABAP Objects
Back to list < Payment Notes Parser using Regular Expressions FS-CD Business Domain Filter >
, section>
In order to keep the effort for the development of unit tests manageable, aspects such as the single response principle or inversion of control are required in the OO design. For the latter in particular, a dependency injection container is an obvious choice in other environments such as Java or C#. In this blog post I would like to introduce a DI container for ABAP Objects that can be used for many simple scenarios.
For unit tests, it is important to design classes that are as independent of other classes as possible so that they can be tested in isolation. The use of interfaces is already a first step towards decoupling. But at the latest when an object of the dependent class is created, there is a dependency that leads to the problem in unit tests, that the dependent class cannot be replaced by a Mock. One solution is Dependency-Injection. The class to be tested may not create its dependent classes itself, but gets them injected, e. g. by passing parameters in the constructor.
The DI container presented in this blog post has the following features:
-
Configuration by code, not by Customizing
- Constructor Injection
- Singleton support
- no multiple bindings
The DI container has two public methods:
- The method BIND to register a class or interface in the DI container
- The method GET to get an object for the class or interface
An object for an interface? Let us assume that we have an interface for reading data from the SAP Business Partner. An implementing class is then registered for this interface, and when an object is requested for this interface, an object of the implementing class is created and returned. The user of the interface does not know the concrete class, and therefore has no dependency on this concrete class. For unit tests, a mock is now simply registered for this interface, so that the users of the interface can be tested without any data having to exist at all in the SAP Business Partner.
It becomes really interesting when the whole thing is done recursively. When the implementing class is created, the system checks whether it also has dependencies. The system checks whether the class has a constructor and, if so, whether it has object references as parameters. In this case, the system also attempts to create objects for the object references. The same check is also performed for these objects, and so on.
Source code
Here the complete source code of the class ZCL_DI_CONTAINER:
class ZCL_DI_CONTAINER definition public final create public . public section. *"* public components of class ZCL_DI_CONTAINER *"* do not include other source files here!!! types T_SCOPE type CHAR1 . constants SCOPE_TRANSIENT type T_SCOPE value 'T'. "#EC NOTEXT constants SCOPE_SINGLETON type T_SCOPE value 'S'. "#EC NOTEXT methods BIND importing !SERVICE type CLIKE !TARGET type CLIKE optional !SCOPE type T_SCOPE default 'T' . methods GET importing !SERVICE type CLIKE returning value(RESULT) type ref to OBJECT . protected section. *"* protected components of class ZCL_DI_CONTAINER *"* do not include other source files here!!! private section. *"* private components of class ZCL_DI_CONTAINER *"* do not include other source files here!!! types: BEGIN OF t_binding, service TYPE string, service_descr TYPE REF TO cl_abap_objectdescr, target_descr TYPE REF TO cl_abap_classdescr, scope TYPE t_scope, singleton TYPE REF TO object, END OF t_binding . types: t_binding_tab TYPE HASHED TABLE OF t_binding WITH UNIQUE KEY service . data BINDINGS type T_BINDING_TAB . methods CREATE_BINDING importing !SERVICE_DESCR type ref to CL_ABAP_OBJECTDESCR !TARGET_DESCR type ref to CL_ABAP_CLASSDESCR optional !SCOPE type T_SCOPE default 'T' returning value(RESULT) type ref to T_BINDING . methods GET_CONSTRUCTOR_PARMBIND importing !TARGET_DESCR type ref to CL_ABAP_CLASSDESCR returning value(RESULT) type ref to ABAP_PARMBIND_TAB . ENDCLASS. CLASS ZCL_DI_CONTAINER IMPLEMENTATION. * <SIGNATURE>---------------------------------------------------------------------------------------+ * | Instance Public Method ZCL_DI_CONTAINER->BIND * +-------------------------------------------------------------------------------------------------+ * | [--->] SERVICE TYPE CLIKE * | [--->] TARGET TYPE CLIKE (optional) * | [--->] SCOPE TYPE T_SCOPE (default ='T') * +--------------------------------------------------------------------------------------</SIGNATURE> METHOD bind. DATA: service_descr TYPE REF TO cl_abap_objectdescr, target_descr TYPE REF TO cl_abap_classdescr. service_descr ?= cl_abap_objectdescr=>describe_by_name( service ). IF NOT target IS INITIAL. target_descr ?= cl_abap_classdescr=>describe_by_name( target ). ENDIF. create_binding( service_descr = service_descr target_descr = target_descr scope = scope ). ENDMETHOD. * <SIGNATURE>---------------------------------------------------------------------------------------+ * | Instance Private Method ZCL_DI_CONTAINER->CREATE_BINDING * +-------------------------------------------------------------------------------------------------+ * | [--->] SERVICE_DESCR TYPE REF TO CL_ABAP_OBJECTDESCR * | [--->] TARGET_DESCR TYPE REF TO CL_ABAP_CLASSDESCR (optional) * | [--->] SCOPE TYPE T_SCOPE (default ='T') * | [<-()] RESULT TYPE REF TO T_BINDING * +--------------------------------------------------------------------------------------</SIGNATURE> METHOD create_binding. DATA binding TYPE t_binding. IF NOT target_descr IS INITIAL. binding-target_descr = target_descr. ELSE. binding-target_descr ?= service_descr. ENDIF. ASSERT binding-target_descr->is_instantiatable( ) EQ abap_true. binding-service = service_descr->absolute_name. binding-service_descr = service_descr. binding-scope = scope. INSERT binding INTO TABLE bindings REFERENCE INTO result. ENDMETHOD. * <SIGNATURE>---------------------------------------------------------------------------------------+ * | Instance Public Method ZCL_DI_CONTAINER->GET * +-------------------------------------------------------------------------------------------------+ * | [--->] SERVICE TYPE CLIKE * | [<-()] RESULT TYPE REF TO OBJECT * +--------------------------------------------------------------------------------------</SIGNATURE> METHOD get. DATA: binding TYPE REF TO t_binding, parmbindtab TYPE REF TO abap_parmbind_tab, service_descr TYPE REF TO cl_abap_objectdescr. service_descr ?= cl_abap_objectdescr=>describe_by_name( service ). READ TABLE bindings REFERENCE INTO binding WITH TABLE KEY service = service_descr->absolute_name. IF sy-subrc NE 0. binding = create_binding( service_descr ). ENDIF. IF binding->scope EQ scope_singleton AND binding->singleton IS BOUND. result = binding->singleton. RETURN. ENDIF. parmbindtab = get_constructor_parmbind( binding->target_descr ). IF parmbindtab->* IS INITIAL. CREATE OBJECT result TYPE (binding->target_descr->absolute_name). ELSE. CREATE OBJECT result TYPE (binding->target_descr->absolute_name) PARAMETER-TABLE parmbindtab->*. ENDIF. IF binding->scope EQ scope_singleton AND NOT binding->singleton IS BOUND. binding->singleton = result. ENDIF. ENDMETHOD. * <SIGNATURE>---------------------------------------------------------------------------------------+ * | Instance Private Method ZCL_DI_CONTAINER->GET_CONSTRUCTOR_PARMBIND * +-------------------------------------------------------------------------------------------------+ * | [--->] TARGET_DESCR TYPE REF TO CL_ABAP_CLASSDESCR * | [<-()] RESULT TYPE REF TO ABAP_PARMBIND_TAB * +--------------------------------------------------------------------------------------</SIGNATURE> METHOD get_constructor_parmbind. DATA: constructor TYPE REF TO abap_methdescr, parmdescr TYPE REF TO abap_parmdescr, refdescr TYPE REF TO cl_abap_refdescr, dependency TYPE REF TO cl_abap_objectdescr, parmbind TYPE abap_parmbind. FIELD-SYMBOLS <fs> TYPE any. CREATE DATA result. READ TABLE target_descr->methods REFERENCE INTO constructor WITH KEY name = 'CONSTRUCTOR'. IF sy-subrc EQ 0. LOOP AT constructor->parameters REFERENCE INTO parmdescr. ASSERT parmdescr->type_kind EQ cl_abap_objectdescr=>typekind_oref. ASSERT parmdescr->parm_kind EQ cl_abap_objectdescr=>importing. refdescr ?= target_descr->get_method_parameter_type( p_method_name = constructor->name p_parameter_name = parmdescr->name ). CREATE DATA parmbind-value TYPE HANDLE refdescr. ASSIGN parmbind-value->* TO <fs>. dependency ?= refdescr->get_referenced_type( ). <fs> ?= get( dependency->absolute_name ). parmbind-name = parmdescr->name. parmbind-kind = cl_abap_objectdescr=>exporting. INSERT parmbind INTO TABLE result->*. ENDLOOP. ENDIF. ENDMETHOD. ENDCLASS.
The GET_CONSTRUCTOR_PARMBIND method is very important. The transfer parameters for the constructor are determined here, and recursion to resolve these parameters takes place here.
When registering classes, the TARGET type can be omitted. This allows classes to be registered on themselves. This can be used to register a class as Singleton. This means that this design pattern no longer has to be implemented in the class itself.
Usage
Classes are usually registered when a program is started, for example in the events LOAD-OF-PROGRAM of a function group or INITIALIZATION of a report, or in a SETUP method of a unit test class.
Example: Let us assume that there is an interface ZIF_BP_SERVICE with service methods for reading business partner data. There is also a class ZCL_BP_SERVICE, which implements this interface. There is also the interface ZIF_ACCOUNT and the class ZCL_ACCOUNT. In our program we register them like this:
LOAD-OF-PROGRAM. DATA di_container TYPE REF TO zcl_di_container. CREATE OBJECT di_container. di_container->bind( service = 'ZIF_BP_SERVICE' target = 'ZCL_BP_SERVICE' ). di_container->bind( service = 'ZIF_ACCOUNT' target = 'ZCL_ACCOUNT' ).
Class ZCL_ACCOUNT requires the BP service and therefore gets a corresponding parameter in its constructor:
CLASS zcl_account DEFINITION PUBLIC. PUBLIC SECTION. INTERFACES zif_account. METHODS constructor IMPORTING bp_service TYPE REF TO zif_bp_service. * ... ENDCLASS.
An object of the class ZCL_ACCOUNT is obtained in the following way:
DATA account TYPE REF TO zif_account. account ?= di_container->get( service = 'ZIF_ACCOUNT' ).
Summary and Outlook
The presented DI container will certainly be sufficient for many simple scenarios. However, there are also scenarios in which more extensive requirements exist. If several implementations exist for an interface and the concrete implementation is to be selected using rules or filter values, multiple bindings are required. With complex class hierarchies, the limits of constructor injection are quickly reached. Setter injection, in which the dependencies are injected into the object using special methods, is a good solution here.
Translated with DeepL
Back to list < Payment Notes Parser using Regular Expressions FS-CD Business Domain Filter >