Higher-Order Function is Dependency Injection in SAP ABAP
2023-12-7 17:52:41 Author: blogs.sap.com(查看原文) 阅读量:7 收藏


Background

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.

Higher-Order Function

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?

  • Common programming idioms/patterns can be encoded as functions within the language itself by using higher-order function
  • Domain-specific languages can be defined as collections of higher-order functions
  • Algebraic properties of higher-order functions can be used to reason about program

Form of Higher-Order Function

Without higher-order function

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.

With higher-order function

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.

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.

Figure 1

Forms of Dependency Injection

There are three main styles of dependency injection.

  • Constructor Injection, where dependencies are provided through a client’s class constructor
  • Setter Injection, where the client exposes a setter method that accepts the dependency
  • Interface Injection, where the dependency’s interface provides an injector method that will inject the dependency into any client passed to it.

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.

Without dependency injection

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)

Constructor Injection

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.

Setter Injection

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.

Interface Injection

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

Other injection styles

Parameter Injection

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

Backdoor Injection

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.

Conclusion

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.


文章来源: https://blogs.sap.com/2023/12/07/higher-order-function-is-dependency-injection-in-sap-abap/
如有侵权请联系:admin#unsafe.sh