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: 
fabianlupa
Contributor
ABAP's reflection (and/or type introspection of you want to make that distinction) capabilities consist mostly of the CL_ABAP_*DESCR-classes and the DESCRIBE statement. To get information on a class for example you might have seen code like this:
DATA(lo_descr) = CAST cl_abap_classdescr( cl_abap_typedescr=>describe_by_name( 'LCL_TEST' ) ).

If you have used reflection in other languages you might also be familiar with this code, same principle just a different API and language:
Class clazz = TestClass.class; // Java
Type type = typeof(TestClass); // C#

One of the differences in comparison to the ABAP code is that you can get a description object of your class without using a literal with the name of the class. This is objectively better because of the following reasons:

  • Compile time safety: If the class does not exist the code will not compile

  • Static usage analysis: Where-Used-List functionality will also find these usages

  • Refactoring: If the class name is changed IDE refactoring support will (because of static usage analysis) find these lines of code and offer to change the name there as well. And if you decide against it, because of compile time safety your code will not compile.


Using the ABAP example you will unfortunately get runtime errors if the class does not exist or is renamed (unless you catch the exception).
Since ABAP in Eclipse is a thing there are way more refactoring capabilities available than in the ABAP workbench. So I think it is more important than ever to avoid these dynamic runtime usages if possible. So let's try getting a CL_ABAP_CLASSDESCR instance where renaming the class using refactoring assistance does not lead to a runtime error.

 

CL_ABAP_CLASSDESCR without classname literal


To avoid the literal the class name needs to be written in code without quotes and still compile. There are only a few options where this is possible that I can think of:

  1. INSTANCE OF logical expression in ABAP 7.50

  2. NEW operator for object instance creation in ABAP 7.40

  3. DATA variable declaration

  4. TYPES type definition

  5. FIELD-SYMBOL declaration

  6. Static member access using classname=>member


Of these 1. does not help because it only results in a logical value, 2. does not help because the class we want to analyze might not be instantiatable (at all or from our position), 6. does not help and 3., 4., and 5. might.

The first approach (which would be just a little bit better) would be to extract the literal to a constant, ideally directly into the class, so that if the class name should change you at least (should) only have one other position to also change it. This would look like this:
CLASS lcl_test DEFINITION.
PUBLIC SECTION.
CONSTANTS:
gc_classname TYPE abap_classname VALUE 'LCL_TEST'.
ENDCLASS.

DATA(lo_descr2) = CAST cl_abap_classdescr(
cl_abap_typedescr=>describe_by_name( lcl_test=>gc_classname )
).

Good luck redefining that constant in subclasses though...

My personal favorite approach is using option 3:
DATA: lo_dummy TYPE REF TO lcl_test.
DATA(lo_descr3) = CAST cl_abap_classdescr(
CAST cl_abap_refdescr(
cl_abap_typedescr=>describe_by_data( lo_dummy )
)->get_referenced_type( )
).

With a nullpointer dummy variable of the correct type you can use the describe_by_data method of the RTTI API and navigate and cast your way towards the class descriptor. It must be pointed out that describe_by_object_ref does not work because you would need an instance of your class for that (REFERENCE_IS_INITIAL-exception), so you need to take the detour using cl_abap_refdescr.

4 and 5 work in a similar way, types is quite interesting I found, because you define a reference type that can the be used by the VALUE inline statement.
TYPES: lty_ref TYPE REF TO lcl_test.
DATA(lo_descr4) = CAST cl_abap_classdescr(
CAST cl_abap_refdescr(
cl_abap_typedescr=>describe_by_data( VALUE lty_ref( ) )
)->get_referenced_type( )
).

I can not think of a way without using some kind of helper declaration using 7.40 inline magic unfortunately. Here are all of the approaches in action with compatible refactorings highlighted:



These do add quite a bit of complexity to your code so they are ideal candidates for a Z-reflection helper class.
DATA(lo_descr) = lcl_reflection_helper=>get_objectdescr_from_data( lo_dummy ).

 

Getting identifiers of attributes


Another task you may or may not stumble upon is dynamically getting instance or class members' values at runtime. The usual approach is this:
CLASS lcl_test DEFINITION.
PUBLIC SECTION.
DATA:
mv_member TYPE i.
ENDCLASS.

ASSIGN lcl_test=>('MV_MEMBER') TO FIELD-SYMBOL(<lg_pointer>).
ASSERT <lg_pointer> IS ASSIGNED.
WRITE <lg_pointer>.

Going a bit further you might think of developing a library that does things on member variables of class instances (like validation or serialization ...). One approach could look like this:
INTERFACE lif_member_publisher.
TYPES:
gty_attribute_name_tab TYPE SORTED TABLE OF abap_attrname WITH UNIQUE KEY table_line.
METHODS:
get_published_members RETURNING VALUE(rt_members) TYPE gty_attribute_name_tab.
ENDINTERFACE.

CLASS lcl_test DEFINITION.
PUBLIC SECTION.
INTERFACES:
lif_member_publisher.
ALIASES:
get_published_members FOR lif_member_publisher~get_published_members.
DATA:
mv_member TYPE i.
ENDCLASS.

CLASS lcl_test IMPLEMENTATION.
METHOD get_published_members.
rt_members = VALUE #( ( CONV #( 'MV_MEMBER' ) ) ).
ENDMETHOD.
ENDCLASS.


DATA(lo_test) = NEW lcl_test( ).
LOOP AT lo_test->get_published_members( ) ASSIGNING FIELD-SYMBOL(<lv_member_name>).
ASSIGN lo_test->(<lv_member_name>) TO FIELD-SYMBOL(<lg_pointer>).
ASSERT <lg_pointer> IS ASSIGNED.
WRITE: / |{ <lv_member_name> }: { <lg_pointer> }|.
ENDLOOP.

While this already works quite well we again have a literal for an identifier in our code. This will lead to an assertion error at ASSERT <lg_pointer> IS ASSIGNED if the member identifier is changed while the literal is not and again refactoring utilities cannot statically find this usage.

In C# there is a statement at language level to avoid this exact problem called nameof (documentation). C# especially needs it because of data bindings in XAML / using the MVVM pattern.

In ABAP there is no such statement that I am aware of. But there is a loophole you can use:
When comparing data reference variables (TYPE REF TO data) that are type compatible the logical expression will be true if they point to the same data object (source). So one can just compare all member variables of a class with a given one and where the expression is true that is the searched member.
Translated to the example above:
CLASS lcl_test IMPLEMENTATION.
METHOD get_published_members.
DATA(lr_ref) = REF #( mv_member ).
DATA(lo_descr) = CAST cl_abap_classdescr( cl_abap_typedescr=>describe_by_object_ref( me ) ).

LOOP AT lo_descr->attributes ASSIGNING FIELD-SYMBOL(<ls_attr>).
ASSIGN me->(<ls_attr>-name) TO FIELD-SYMBOL(<lg_pointer>).
ASSERT <lg_pointer> IS ASSIGNED.
IF lr_ref = REF #( <lg_pointer> ).
rt_members = VALUE #( ( <ls_attr>-name ) ).
EXIT.
ENDIF.
UNASSIGN <lg_pointer>.
ENDLOOP.
ENDMETHOD.
ENDCLASS.

And if you extract that functionality to a helper method it does not even look all too bad:
CLASS lcl_test IMPLEMENTATION.
METHOD get_published_members.
rt_members = VALUE #(
( lcl_reflection_helper=>name_of( io_container = me ir_ref = REF #( mv_member ) ) )
).
ENDMETHOD.
ENDCLASS.

You do however lose quite a bit of performance if there are a lot of member variables to go through, so if you decide on using a strategy like this you might also want to cache the results for reuse.

 

Helper class


And here's a little helper class for both cases discussed. It might not factor in all edge cases so adjust as needed if you want to use it.
"! Reflection tools
CLASS lcl_reflection_helper DEFINITION.
PUBLIC SECTION.
CLASS-METHODS:
"! Get objectdescriptor from data variable
"! @parameter ig_data | Variable typed as REF TO interface/class
"! @parameter ro_descriptor | Object descriptor
"! @raising lcx_illegal_argument | ig_data does not refer to a class / interface
get_objectdescr_from_data IMPORTING ig_data TYPE any
RETURNING VALUE(ro_descriptor) TYPE REF TO cl_abap_objectdescr
RAISING lcx_illegal_argument,
"! Get the identifier of a public variable
"! @parameter ir_ref | Reference to the variable
"! @parameter io_container | Containing object instance
"! @parameter rv_name | Identifier of the data object
"! @raising lcx_illegal_argument | Argument cannot be null
name_of IMPORTING ir_ref TYPE REF TO data
io_container TYPE REF TO object
RETURNING VALUE(rv_name) TYPE abap_attrname
RAISING lcx_illegal_argument.
ENDCLASS.

CLASS lcl_reflection_helper IMPLEMENTATION.
METHOD get_objectdescr_from_data.
DATA(lo_descr) = cl_abap_typedescr=>describe_by_data( ig_data ).

IF lo_descr->type_kind <> cl_abap_typedescr=>typekind_oref.
RAISE EXCEPTION TYPE lcx_illegal_argument.
ENDIF.

DATA(lo_referenced_descr) = CAST cl_abap_refdescr( lo_descr )->get_referenced_type( ).

IF lo_referenced_descr->type_kind <> cl_abap_typedescr=>typekind_class
AND lo_referenced_descr->type_kind <> cl_abap_typedescr=>typekind_intf.
RAISE EXCEPTION TYPE lcx_illegal_argument.
ENDIF.

ro_descriptor = CAST cl_abap_objectdescr( lo_referenced_descr ).
ENDMETHOD.

METHOD name_of.
IF io_container IS NOT BOUND OR ir_ref IS NOT BOUND.
RAISE EXCEPTION TYPE lcx_illegal_argument.
ENDIF.

DATA(lo_descr) = CAST cl_abap_objectdescr(
cl_abap_typedescr=>describe_by_object_ref( io_container )
).

LOOP AT lo_descr->attributes ASSIGNING FIELD-SYMBOL(<ls_attr>)
WHERE visibility = cl_abap_objectdescr=>public.

ASSIGN io_container->(<ls_attr>-name) TO FIELD-SYMBOL(<lg_pointer>).
ASSERT <lg_pointer> IS ASSIGNED.
IF ir_ref = REF #( <lg_pointer> ).
rv_name = <ls_attr>-name.
EXIT.
ENDIF.
UNASSIGN <lg_pointer>.
ENDLOOP.
ENDMETHOD.
ENDCLASS.

 
5 Comments