Bicycles. #3 – High level functions and callbacks in abap
Preamble and disclaimer
Creating own implementation for some problem, which was already solved is before, often referred as “inventing a bicycle”. I did invent a couple too ? This post, and maybe a couple of following posts, describe several of my open-source “bicycles”. I “invented” them because in the time I was searching for such functionalities that I didn’t find them. I’m suspect these functionalities do exist already and you can point that out in comments, if you know a better and more standard solution. Yet my “bicycles” were “invented” with the convenience in mind and with an attempt to make clean and universal tools. So I sincerely hope someone will find them useful. If I’m missing an obvious solution – welcome to comment ? KR. Alexander.
You, most probably, stumbled upon situations where you have a common code block with some specific call in the middle that should vary depending on a use case. Such constructs cannot be unified well and you have to reimplement and duplicate most of the code several times. Something like this.
" data declarations and some syntax details ommited " for readability and code is pseudified method send_my_document. api = create_api_client_instance( ). context = prepare_document_context( ). doc = create_my_document( i_params = doc_specific_params i_context = context ). " The specific code, " the document class can be different " params can be different " but note the conext which is created inside result = api->send( doc ). post_process_result( result ). endmethod.
Would it be nice to delegate the middle part of this code to some other external callback method, and stay with one unified high level function to wrap it dynamically? This is possible in apparently all modern languages!
One obvious solution is to use interfaces and implement the code differently depending on the case.
interface lif_my_doc. method create_doc importing i_context type ... returning value(r_doc) type ... endinterface.
And it is a proper approach, unless you have a lot of places and shapes of the methods (for whatever reason). In which case you will end up with many many interfaces and could be lost in their naming probably as well. Are there better options? Below are some thoughts and experiments on that.
Approaching 1st time …
First of all, we still need one interface – it must be unified enough to accept any parameter which will then be casted internally and return any too.
interface lif_lambda. methods run importing i_workset type any changing c_result type any. endinterface.
Now we can pass anything to this method and receive anything.
method run. field-symbol <context> type ty_context. field-symbol <result> type ref to zcl_my_document. assign i_workset to <context>. assign c_result to <result>. <result> = create_my_document( i_params = doc_specific_params i_context = <context> ). endmethod.
However we can make it more elegant. The “changing” adds some unnecessary verbosity here. Why not to use “returning”. Ah, yes, methods cannot return “any” in abap. Well, let’s solve it too.
Approaching 2nd time …
First we need to return a reference. However! Unpacking the reference would be inconvenient for the calling code. So let’s add a helper class to return …
interface lif_lambda. methods run importing i_workset type any optional returning value(ri_result) type ref to lif_lambda_result. endinterface.
where lif_lambda_result is a convenience layer
interface lif_lambda_result. methods str returning value(r_val) type string. methods int returning value(r_val) type i. methods obj returning value(r_val) type ref to object. methods struc changing cs_struc type any. methods tab changing ct_tab type any table. ... endinterface.
and the implementation
class lcl_lambda_result definition final. public section. interfaces lif_lambda_result. data mr_data type ref to data. endclass. class lcl_lambda_result implementation. method lif_lambda_result~obj. field-symbols <val> type any. data l_type type c. assign mr_data->* to <val>. describe field <val> type l_type. if l_type ca 'r'. " object r_val = <val>. endif. endmethod. " ... other unwrappers here endclass.
This allows us to easily convert returning value to the type we expect
li_lambda->run( some_params )->str( ). li_lambda->run( some_params )->obj( ). ...
Approaching 3rd time …
However! Packing of the reference is also inconvenient, because you cannot output reference to a local variable, you have a make a copy. So let’s improve further.
class lcl_lambda_result definition final. public section. interfaces lif_lambda_result. class-methods wrap " instantiation method importing i_result type any returning value(ri_result) type ref to lif_lambda_result. private section. data mr_data type ref to data. " Hide the ref endclass. class lcl_lambda_result implementation. method wrap. data lo_result type ref to lcl_lambda_result. create object lo_result. data l_type type c. describe field i_result type l_type. if l_type = 'r'. " object create data lo_result->mr_data type ref to object. else. create data lo_result->mr_data like i_result. endif. field-symbols <val> type any. assign lo_result->mr_data->* to <val>. <val> = i_result. " Just copy the incoming value into a data ref ri_result = lo_result. endmethod. " all the rest endclass.
Now we can do as follows:
" callback for document creation method run. field-symbol <context> type ty_context. assign i_workset to <context>. " wrap any result with one line ! ri_result = lcl_lambda_result=>wrap( create_my_document( doc_specific_params, <context> ) ). endmethod. " and the calling code would be method send_my_document. api = create_api_client_instance( ). context = prepare_document_context( ). " receive the result converting to the expected type doc = ii_doc_callback->run( context )->obj( ). result = api->send( doc ). post_process_result( result ). endmethod.
Clean and convenient!
All comes with a price. And a couple of cents to pay is to the performance. Packing and unpacking is an overhead. I did some tests here and found that wrappers are 2-4 time slower in case of simplest possible processing. The results are in seconds.
CHAR_W_WRAPPER 0,034402 CHAR_WO_WRAPPER 0,009170 STRING_W_WRAPPER 0,036532 STRING_WO_WRAPPER 0,009457 INT_W_WRAPPER 0,031680 INT_WO_WRAPPER 0,008231 OBJ_W_WRAPPER 0,073308 OBJ_WO_WRAPPER 0,039072 STRUC_W_WRAPPER 0,044464 STRUC_WO_WRAPPER 0,012494 TAB_W_WRAPPER 0,054196 TAB_WO_WRAPPER 0,021800
Having said this, the test is completely artificial and focused on emphasizing the wrapping extra time as much as possible. This is a test for 10000 repetitions, with any (any!) real processing this overhead will be negligible.
Multiple callbacks in one class
Finally, one practical aspect. What if you have several callbacks within one processing class? You can implement the interface only once, right ? One of solutions to mention (among many) are local classes inside global ones (assuming the processing class is global).
class lcl_callback1 definition final. public section. interfaces lif_lambda. class-methods new importing io_this type ref to zcl_parent_global_class returning value(ro_instance) type ref to lcl_callback1. private section. data mo_this type ref to zcl_parent_global_class. endclass. class zcl_parent_global_class definition local friends lcl_callback1. class lcl_callback1 definition final. methods new. create object ro_instance. ro_instance->mo_this = io_this. endmethod. method lif_lambda~run. " do something with mo_this - the parent class endmethod. endclass.
Thus you can have multiple callbacks which will access the state of the parent class almost seamlessly. This is, though, just one of the options.