This blog post supplements my main blog post series, “Functional Programming in ABAP”. It was inspired by a question from one of our community members, leading me to assume that ABAP developers might not be too familiar with dependency injection—or perhaps they’ve used it without recognizing the term. On this occasion, I aim to shed light on the similarities between higher-order functions and dependency injection.
Formally speaking, a function that takes a function as an argument or returns a function as a result is called a higher-order function. In practice, however, because the term curried already exists for returning functions as results, the term higher-order is often just used for taking functions as arguments.
Why is it useful?
module NoHOF ( mapIncrmt
, mapDouble ) where
incrmt :: Num a => a -> a
incrmt = (+1)
mapIncrmt :: Num a => [a] -> [a]
mapIncrmt xs = [ incrmt x | x <- xs ]
double :: Num a => a -> a
double = (*2)
mapDouble :: Num a => [a] -> [a]
mapDouble [] = []
mapDouble (x:xs) = double x : mapDouble xs
ghci> mapIncrmt [1..10]
[2,3,4,5,6,7,8,9,10,11]
ghci> mapDouble [1..10]
[2,4,6,8,10,12,14,16,18,20]
Higher-order functions allow you to be more adaptable, you can use one function to handle different operations. Without them, your code might become a bit repetitive and harder to understand, as you have to write more specific functions for each job.
module ExercisesHOF ( mFilterMap
, mFilterMap'
, mAll
, mAny
, mTakeWhile
, mTakeWhile'
, mDropWhile
, mFilter
, twice
, map
) where
import Control.Monad (when)
mFilterMap :: (Ord a, Eq a) => (a -> Bool) -> (a -> b) -> [a] -> [b]
mFilterMap p f xs = [ f x | x <- xs, p x ]
mFilterMap' :: (Ord a, Eq a) => (a -> Bool) -> (a -> b) -> [a] -> [b]
mFilterMap' p f = map f . mFilter p
mAll :: (a -> Bool) -> [a] -> Bool
mAll p = foldr ((&&) . p) True
mAny :: (a -> Bool) -> [a] -> Bool
mAny = (or .) . map
mTakeWhile :: (a -> Bool) -> [a] -> [a]
mTakeWhile p xs = go p xs []
where go _ [] zs = zs
go f (y:ys) zs
| f y = go f ys (y:zs)
| otherwise = zs
mTakeWhile' :: (a -> Bool) -> [a] -> [a]
mTakeWhile' _ [] = []
mTakeWhile' p (x:xs)
| p x = x : mTakeWhile' p xs
| otherwise = []
mDropWhile :: (a -> Bool) -> [a] -> [a]
mDropWhile _ [] = []
mDropWhile p all@(x:xs)
| p x = mDropWhile p xs
| otherwise = all
mFilter :: (a -> Bool) -> [a] -> [a]
mFilter _ [] = []
mFilter pred (x:xs)
| pred x = x : mFilter pred xs
| otherwise = mFilter pred xs
twice :: (a -> a) -> a -> a
twice f = f . f
map :: (a -> b) -> [a] -> [b]
map f xs = [ y | x <- xs, let y = f x ]
ghci> mAll even [2,4,6,8,10]
True
ghci> mAny odd [2,4,6,7,8,10]
True
ghci> mAny odd [2,4,6,8,10]
False
ghci> twice (+2) 3
7
ghci> map (+1) [1..10]
[2,3,4,5,6,7,8,9,10,11]
ghci> mFilterMap even (+1) [1..20]
[3,5,7,9,11,13,15,17,19,21]
ghci> mTakeWhile (<5) [1..10]
[4,3,2,1]
ghci> mTakeWhile' (<5) [1..10]
[1,2,3,4]
The advantage of using a higher-order function is that it allows for more concise and expressive code by abstracting common computation patterns. Higher-order functions take other functions as arguments or return functions as results (curried function), providing a flexible and reusable way to manipulate data.
The form of a higher-order function is almost trivial and straightforward, but how about his brother, dependency injection.
The basic idea of the Dependency Injection is to have a separate object, an assembler, that populates a field in the client class with an appropriate implementation for the Service interface, resulting in a dependency diagram along the lines of Figure 1 below.
There are three main styles of dependency injection.
I will keep the example general and straightforward, making it easy to understand the concept without getting lost in unnecessary complex logic, so please let’s be focused on the pattern.
INTERFACE zif_service
PUBLIC .
METHODS generate_data IMPORTING raw_data TYPE any
RETURNING VALUE(result) TYPE REF TO data.
ENDINTERFACE.
The interface zif_service
serves as a contract that will be implemented by some class
CLASS zcl_service_impl01 DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
INTERFACES zif_service .
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_service_impl01 IMPLEMENTATION.
METHOD zif_service~generate_data.
" Logic implementation 01
" ...
ENDMETHOD.
ENDCLASS.
The first implementation of the interface zif_service
CLASS zcl_service_impl02 DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
INTERFACES zif_service .
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_service_impl02 IMPLEMENTATION.
METHOD zif_service~generate_data.
" Logic implementation 02
" ...
ENDMETHOD.
ENDCLASS.
The second class that implements the interface zif_service
I assume that the first and second classes have different implementation logic.
CLASS zcl_client DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
CONSTANTS: BEGIN OF impl_type,
impl01 TYPE numc2 VALUE '01',
impl02 TYPE numc2 VALUE '02',
END OF impl_type.
METHODS constructor IMPORTING serv_impl_type TYPE numc2
RAISING
cx_adt_res_precondition_failed.
METHODS action IMPORTING request TYPE any
RETURNING VALUE(result) TYPE string.
PROTECTED SECTION.
PRIVATE SECTION.
DATA service TYPE REF TO zif_service.
ENDCLASS.
CLASS zcl_client IMPLEMENTATION.
METHOD constructor.
case serv_impl_type.
when '01'.
service = NEW zcl_service_impl01( ).
when '02'.
service = NEW zcl_service_impl02( ).
when OTHERS.
RAISE EXCEPTION NEW cx_adt_res_precondition_failed(
textid =
cx_adt_res_precondition_failed=>cx_adt_res_precondition_failed
).
endcase.
ENDMETHOD.
METHOD action.
FIELD-SYMBOLS <result> TYPE string.
DATA(processed_data) = service->generate_data( request ).
" process
" ...
" assigning <result>
result = <result>.
ENDMETHOD.
ENDCLASS.
The ABAP code defines a class named zcl_client
that determines its implementation dependency (zif_service
) internally based on a provided type during instantiation. The constructor method checks the specified implementation type and creates an instance of either zcl_service_impl01
or zcl_service_impl02
accordingly. The action
method then processes a request using the chosen implementation and assigns the result. This approach, while not using a full-fledged dependency injection framework, demonstrates a simple form of dependency management within the class itself, allowing flexibility in choosing the implementation at runtime.
CLASS zcl_assembler DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun .
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_assembler IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
TRY.
DATA(client) = NEW zcl_client( zcl_client=>impl_type-impl01 ).
out->write( client->action( request = `Greeting` ) ).
CATCH cx_adt_res_precondition_failed INTO DATA(err).
out->write( err->get_text( ) ).
ENDTRY.
ENDMETHOD.
ENDCLASS.
The ABAP code defines a class named zcl_assembler
that serves as a main program showcasing the usage of a client object and its dependency. In the main
method, an instance of the zcl_client
class is created with a specified implementation type (impl01
), and the action
method is called with a sample request. Any precondition failure is caught, and the error message is output. This code demonstrates the orchestration of the client and its dependency without dependency injection technique, illustrating a simple program flow with error handling.
The upcoming examples will continue to utilize the previously defined objects (zif_service, zcl_service_impl01, zcl_service_impl02)
CLASS zcl_client DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
METHODS constructor IMPORTING service TYPE REF TO zif_service
RAISING cx_prc_instance_not_bound.
METHODS action IMPORTING request TYPE any
RETURNING VALUE(result) TYPE string.
PROTECTED SECTION.
PRIVATE SECTION.
DATA service TYPE REF TO zif_service.
ENDCLASS.
CLASS zcl_client IMPLEMENTATION.
METHOD constructor.
IF service IS BOUND.
me->service = service.
ELSE.
RAISE EXCEPTION NEW cx_prc_instance_not_bound( ).
ENDIF.
ENDMETHOD.
METHOD action.
FIELD-SYMBOLS <result> TYPE string.
DATA(processed_data) = service->generate_data( request ).
" process
" ...
" assigning <result>
result = <result>.
ENDMETHOD.
ENDCLASS.
The ABAP code defines a class zcl_client
with a constructor injection. The constructor takes a service
parameter, which is a reference to an object implementing the zif_service
interface. If a valid service instance is provided, it is assigned to the private instance attribute; otherwise, an exception is raised. The action
method then uses the injected service to generate data. This approach demonstrates a form of constructor injection, allowing external components to provide the necessary dependencies during object instantiation.
CLASS zcl_assembler DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun .
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_assembler IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
TRY.
DATA(client01) = NEW zcl_client(
NEW zcl_service_impl01( ) ).
out->write( client01->action( `Greeting` ) ).
CATCH cx_prc_instance_not_bound INTO DATA(err01).
out->write( err01->get_text( ) ).
ENDTRY.
TRY.
DATA(client02) = NEW zcl_client(
NEW zcl_service_impl02( ) ).
out->write( client02->action( `Greeting` ) ).
CATCH cx_prc_instance_not_bound INTO DATA(err02).
out->write( err01->get_text( ) ).
ENDTRY.
ENDMETHOD.
ENDCLASS.
Using constructor injection, every time we need to change the dependency implementation, we must create a new object.
CLASS zcl_client DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
METHODS action IMPORTING request TYPE any
RETURNING VALUE(result) TYPE string.
METHODS: set_service IMPORTING service TYPE REF TO zif_service
RAISING cx_prc_instance_not_bound.
PROTECTED SECTION.
PRIVATE SECTION.
DATA service TYPE REF TO zif_service.
ENDCLASS.
CLASS zcl_client IMPLEMENTATION.
METHOD action.
FIELD-SYMBOLS <result> TYPE string.
IF service IS NOT BOUND.
RETURN.
ENDIF.
DATA(processed_data) = service->generate_data( request ).
" process
" ...
" assigning <result>
result = <result>.
ENDMETHOD.
METHOD set_service.
IF service IS BOUND.
me->service = service.
ELSE.
RAISE EXCEPTION NEW cx_prc_instance_not_bound( ).
ENDIF.
ENDMETHOD.
ENDCLASS.
The ABAP code defines a class zcl_client
that emphasizes setter injection. The class includes a set_service
method allowing the injection of a zif_service
dependency and the action
method uses this injected dependency to generate processed data. The setter method is used to inject the dependency. It ensures that the service parameter is bound before assigning it to the private instance attribute, and it raises an exception if the service parameter is not bound.
CLASS zcl_assembler DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun .
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_assembler IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
DATA(client) = NEW zcl_client( ).
TRY.
client->set_service( NEW zcl_service_impl01( ) ).
out->write( client->action( `Greeting` ) ).
CATCH cx_prc_instance_not_bound INTO DATA(err01).
out->write( err01->get_text( ) ).
ENDTRY.
TRY.
client->set_service( NEW zcl_service_impl02( ) ).
out->write( client->action( `Greeting` ) ).
CATCH cx_prc_instance_not_bound INTO DATA(err02).
out->write( err01->get_text( ) ).
ENDTRY.
ENDMETHOD.
ENDCLASS.
We don’t need to instantiate new objects whenever we want to switch the dependency implementation. This approach demonstrates a form of dependency injection, where the dependency is set externally, enhancing flexibility and testability.
The provided ABAP code exhibits a basic form of interface-based dependency injection. The zcl_client
class implements the zif_service_setter
interface, allowing it to receive different service implementations. The zcl_service_injector
class serves as a dependency manager, providing methods to inject services into the client and switch between different implementations. In the zcl_assembler
class’s main method, instances of the client and service injector are created. The service injector injects a service into the client, enabling the client to perform actions with the injected service.
INTERFACE zif_service_setter
PUBLIC .
METHODS set_service IMPORTING service TYPE REF TO zif_service
RAISING
cx_prc_instance_not_bound.
ENDINTERFACE.
CLASS zcl_service_injector DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
METHODS inject_service IMPORTING client TYPE REF TO zif_service_setter.
METHODS switch_service.
PROTECTED SECTION.
PRIVATE SECTION.
DATA client TYPE REF TO zif_service_setter.
ENDCLASS.
CLASS zcl_service_injector IMPLEMENTATION.
METHOD inject_service.
IF client IS NOT BOUND.
RETURN.
ENDIF.
me->client = client.
me->client->set_service( NEW zcl_service_impl01( ) ).
ENDMETHOD.
METHOD switch_service.
IF me->client IS NOT BOUND.
RETURN.
ENDIF.
me->client->set_service( NEW zcl_service_impl02( ) ).
ENDMETHOD.
ENDCLASS.
CLASS zcl_client DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
INTERFACES zif_service_setter.
METHODS action IMPORTING request TYPE any
RETURNING VALUE(result) TYPE string.
PROTECTED SECTION.
PRIVATE SECTION.
DATA service TYPE REF TO zif_service.
ENDCLASS.
CLASS zcl_client IMPLEMENTATION.
METHOD action.
FIELD-SYMBOLS <result> TYPE string.
IF service IS NOT BOUND.
RETURN.
ENDIF.
DATA(processed_data) = service->generate_data( request ).
" process
" ...
" assigning <result>
result = <result>.
ENDMETHOD.
METHOD zif_service_setter~set_service.
IF service IS NOT BOUND.
RAISE EXCEPTION NEW cx_prc_instance_not_bound( ).
ENDIF.
me->service = service.
ENDMETHOD.
ENDCLASS.
CLASS zcl_assembler DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun .
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_assembler IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
DATA(client) = NEW zcl_client( ).
DATA(injector) = NEW zcl_service_injector( ).
injector->inject_service( client ).
DATA(result01) = client->action( `Greeting` ).
injector->switch_service( ).
DATA(result02) = client->action( `Greeting` ).
out->write( result01 ).
out->write( result02 ).
ENDMETHOD.
ENDCLASS.
I think this style is quite complex. For clarity purposes, I will provide some proof by debugging the code.
Initial value of service, before injection
After injection, the service attribute has the value of zcl_service_impl01 object
After switching, the service attribute has the value of zcl_service_impl02 object
In a parameter injection, the dependency is passed directly into the function/ method that uses the dependency by its argument. I don’t want to waste your time reviewing the example of this style since it has been declared in my main blog post
If you are the slave of simplicity, “keep it simple” is your main motto, and the monolith is your sect, but you still want to separate/ segregate the concerns and your only enemy is doing test double. here is the mono solution.
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" GLOBAL CLASS SECTION "
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
CLASS zcl_client DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
METHODS action IMPORTING request TYPE any
RETURNING VALUE(result) TYPE string.
PROTECTED SECTION.
PRIVATE SECTION.
DATA service TYPE REF TO lif_service.
ENDCLASS.
CLASS zcl_client IMPLEMENTATION.
METHOD action.
FIELD-SYMBOLS <result> TYPE string.
DATA service TYPE REF TO lif_service.
IF me->service IS BOUND.
service = me->service.
ELSE.
service = NEW lcl_service_impl( ).
ENDIF.
DATA(processed_data) = service->generate_data( request ).
ASSIGN processed_data->* TO <result>.
IF <result> IS ASSIGNED.
result = <result>.
ENDIF.
ENDMETHOD.
ENDCLASS.
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Class-relevant Local Types "
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
*"* use this source file for any type of declarations (class
*"* definitions, interfaces or type declarations) you need for
*"* components in the private section
INTERFACE lif_service.
METHODS generate_data IMPORTING raw_data TYPE any
RETURNING VALUE(result) TYPE REF TO data.
ENDINTERFACE.
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Local Types "
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
*"* use this source file for the definition and implementation of
*"* local helper classes, interface definitions and type
*"* declarations
CLASS lcl_service_impl DEFINITION
CREATE PUBLIC .
PUBLIC SECTION.
INTERFACES lif_service .
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS lcl_service_impl IMPLEMENTATION.
METHOD lif_service~generate_data.
FIELD-SYMBOLS <result> TYPE string.
result = NEW string( ).
ASSIGN result->* TO <result>.
IF <result> IS ASSIGNED.
<result> = `Higher-Order Function is Awesome`.
ENDIF.
ENDMETHOD.
ENDCLASS.
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Test Classes "
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
*"* use this source file for your ABAP unit test classes
CLASS ltcl_client DEFINITION DEFERRED.
CLASS zcl_client DEFINITION LOCAL FRIENDS ltcl_client.
CLASS ltd_service DEFINITION CREATE PUBLIC FOR TESTING.
PUBLIC SECTION.
INTERFACES lif_service.
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS ltd_service IMPLEMENTATION.
METHOD lif_service~generate_data.
FIELD-SYMBOLS <result> TYPE string.
result = NEW string( ).
ASSIGN result->* TO <result>.
IF <result> IS ASSIGNED.
<result> = `Dependency Injection is Awesome`.
ENDIF.
ENDMETHOD.
ENDCLASS.
CLASS ltcl_client DEFINITION FINAL FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA cut TYPE REF TO zcl_client.
METHODS:
test_backdoor FOR TESTING RAISING cx_static_check,
test_without_injection FOR TESTING RAISING cx_static_check,
setup.
ENDCLASS.
CLASS ltcl_client IMPLEMENTATION.
METHOD setup.
cut = NEW #( ).
ENDMETHOD.
METHOD test_backdoor.
" backdoor injection
" when
cut->service = NEW ltd_service( ).
" given
DATA(act) = cut->action( `Greeting` ).
" then
cl_abap_unit_assert=>assert_equals( msg = 'msg'
exp = `Dependency Injection is Awesome`
act = act ).
ENDMETHOD.
METHOD test_without_injection.
" given
DATA(act) = cut->action( `Greeting` ).
" then
cl_abap_unit_assert=>assert_equals( msg = 'msg'
exp = `Higher-Order Function is Awesome`
act = act ).
ENDMETHOD.
ENDCLASS.
In summary, both higher-order functions and dependency injection play a crucial role in making code flexible, easy to modify, and straightforward to test. They share a common goal: promoting abstraction. Whether it’s passing functions around or injecting dependencies, the emphasis on abstraction ensures that our code remains adaptable to changes. By embracing these concepts, developers create software that not only meets current needs but also evolves smoothly over time, forming a strong foundation for efficient and resilient development.