Application Development Blog Posts
Learn and share on deeper, cross technology development topics such as integration and connectivity, automation, cloud extensibility, developing at scale, and security.
cancel
Showing results for 
Search instead for 
Did you mean: 
atsybulsky
Active Participant

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.

Callback case


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!

Interfaces ?


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!

Performance


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.
The code examples for this post and the benchmark can be found at https://github.com/sbcgua/abap_callbacks_post.


I hope you find this useful or at least interesting. ?


P.S. I'm using naming "lambda" in the example. These are not exactly lambda function which are usually unnamed and defined in-code - abap does not support it