To represent a fixed value set in ABAP you can use several different technologies. The newest one is enumerations which are provided at language level and can be used as of AS ABAP 7.51. With this blog post I want to show which possibilities there are for ABAP developers to define enumerations and use them in signature elements in the safest way possible.
Enumerations enable you to define a fixed amount of named values for a specific context within the type system of a programming language. These behave like constants but have the additional feature of being limited to the defined values (and not to the type of the constant). This is especially useful when defining formal parameters using enumerations, because then the user of your API is less likely to use your method wrong (because the type system does not allow it, it would be a compile time error).
Think of the following example:
CLASS lcl_light_switcher DEFINITION.
PUBLIC SECTION.
CLASS-METHODS:
"! Switch the lights on / off
"! @parameter iv_on | Should they be on?
switch IMPORTING iv_on TYPE abap_bool.
ENDCLASS.
For most of us it is clear that iv_on should either be ' ' or 'X' because the type of the parameter is abap_bool. So typical usage would be something like this (preferred using the abap_* constants):
lcl_light_switcher=>switch( abap_true ).
lcl_light_switcher=>switch( abap_false ).
lcl_light_switcher=>switch( ' ' ).
lcl_light_switcher=>switch( space ).
lcl_light_switcher=>switch( 'X' ).
However, the type system does not force you to use ' ' or 'X' because abap_bool is just a type definition in the global type-pool abap and its actual type is C with a length of 1.
Goals of enumerations
Enumerations enable you to define a fixed amount of named values for a specific context within the type system of a programming language. These behave like constants but have the additional feature of being limited to the defined values (and not to the type of the constant). This is especially useful when defining formal parameters using enumerations, because then the user of your API is less likely to use your method wrong (because the type system does not allow it, it would be a compile time error).
Example of API miss-usage
Think of the following example:
CLASS lcl_light_switcher DEFINITION.
PUBLIC SECTION.
CLASS-METHODS:
"! Switch the lights on / off
"! @parameter iv_on | Should they be on?
switch IMPORTING iv_on TYPE abap_bool.
ENDCLASS.
For most of us it is clear that iv_on should either be ' ' or 'X' because the type of the parameter is abap_bool. So typical usage would be something like this (preferred using the abap_* constants):
lcl_light_switcher=>switch( abap_true ).
lcl_light_switcher=>switch( abap_false ).
lcl_light_switcher=>switch( ' ' ).
lcl_light_switcher=>switch( space ).
lcl_light_switcher=>switch( 'X' ).
However, the type system does not force you to use ' ' or 'X' because abap_bool is just a type definition in the global type-pool abap and its actual type is C with a length of 1.
This means the following does not lead to a compiler error:
lcl_light_switcher=>switch( 'A' ).
It will however probably lead to an error in the implementation of the method because the programmer who implemented it does not expect someone to use ‘A’ as the actual parameter because by convention only ' ' and 'X' should be used. For abap_bool this is probably fine because most developers know about abap’s missing in-build boolean type and the ways around it. But what about other types that are module / application specific or that you define on your own?
Ways to define fixed value quantities in ABAP
Let’s say we have more states than just true and false. Our lamp is not just on or off now but can display different colors (like traffic lights).
Enumeration values as constants
If we want to use constants now (like abap_true and abap_false) we have to define them on our own. But where do we do that? There are three different types of development objects to use:
- TYPE-POOLS: like SLIS, OSCON, ABAP, …
- INTERFACES: like IF_SALV_C_*, generated interfaces in BOPF, …
- CLASSES: either directly in the class with the method that needs the constant or in its own, like CL_GUI_CONTROL=>METRIC_PIXEL
Since type pools are obsolete, interfaces (or also classes) are the way to go nowadays. Here is an example using a constants-interface:
INTERFACE zif_light_states PUBLIC.
TYPES:
gty_light_state TYPE i.
CONSTANTS:
gc_green TYPE gty_light_state VALUE 0,
gc_yellow TYPE gty_light_state VALUE 1,
gc_red TYPE gty_light_state VALUE 2,
gc_red_and_yellow TYPE gty_light_state VALUE 3.
ENDINFERFACE.
CLASS zcl_light_switcher DEFINITION
PUBLIC
CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
"! Switch the light
"! @parameter iv_new_state | The new light color (see ZIF_LIGHT_STATES constants)
"! @raising zcx_illegal_argument | iv_new_state is invalid
switch_light IMPORTING iv_new_state TYPE zif_light_states=>gty_light_state
RAISING zcx_illegal_argument.
PROTECTED SECTION.
DATA:
mv_light TYPE zif_light_states=>gty_light_state.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_light_switcher IMPLEMENTATION.
METHOD switch_light.
TYPES: lty_i_range TYPE RANGE OF zif_light_states=>gty_light_state.
" Validate iv_new_state
IF iv_new_state NOT IN VALUE lty_i_range(
( sign = 'I' option = 'EQ' low = zif_light_states=>gc_green )
( sign = 'I' option = 'EQ' low = zif_light_states=>gc_yellow )
( sign = 'I' option = 'EQ' low = zif_light_states=>gc_red )
( sign = 'I' option = 'EQ' low = zif_light_states=>gc_red_and_yellow )
).
RAISE EXCEPTION TYPE zcx_illegal_argument
EXPORTING
is_message = zcx_illegal_argument=>gc_with_name_and_value
iv_par_name = 'IV_NEW_STATE'
iv_value = iv_new_state ##NO_TEXT.
ENDIF.
mv_light = iv_new_state.
ENDMETHOD.
ENDCLASS.
As you can see the user of the API is pointed to the constants interface in the method documentation and by the type of the importing parameter. The type declaration of gty_light_state in the interface would normally be unnecessary (because the inbuilt type I of course already exists) but it helps to reference the place where the constants are stored. You could of course also create a DDIC data element and refer to the interface in its description. But if it is not documented somewhere, the API user is forced to look into the implementation of the method to find out what actual parameters to use (which is troublesome and not always possible, think of defining your method in an interface without implementation).
So now the possible values are known, but still, as the developer implementing the method (and using design by contract principle) you should validate the actual parameters of the caller. Which means checking if it is one of the constants. In the example above this is done using a range table. You can see another weak point here. If one decides to add another state to our traffic lights (like blinking yellow) the validation logic would have to be updated even if the rest of the method could handle an additional state just fine.
One advantage or disadvantage of using interfaces for constant definitions is that you can implement the interface in the class that uses them. This enables you to access the constants directly (as long as you defined aliases). On the other hand you will also find everything the interface defines in the debugger if you inspect an object instance in the members view. I am personally not a fan of doing that.
DDIC Domain values
Another way is using domains in the data dictionary. These seem particularly well suited for this task at first.
You can easily validate if a variable’s value is defined in a domain using standard function modules or RTTI. Here is the same light switcher class using a domain (/ a data element that refers to a domain):
CLASS zcl_light_switcher DEFINITION
PUBLIC
CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
"! Switch the light
"! @parameter iv_new_state | The new light color (see ZSCN_D_LIGHTSTATE domain)
"! @raising zcx_illegal_argument | iv_new_state is invalid
switch_light IMPORTING iv_new_state TYPE zscn_l_lightstate
RAISING zcx_illegal_argument.
PROTECTED SECTION.
DATA:
mv_light TYPE zscn_l_lightstate.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_light_switcher IMPLEMENTATION.
METHOD switch_light.
" Validate iv_new_state
CALL FUNCTION 'CHECK_DOMAIN_VALUES'
EXPORTING
domname = 'ZSCN_D_LIGHTSTATE'
value = iv_new_state.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_illegal_argument
EXPORTING
is_message = zcx_illegal_argument=>gc_not_in_domain
iv_par_name = 'IV_NEW_STATE'
iv_value = iv_new_state
iv_domname = 'ZSCN_D_LIGHTSTATE' ##NO_TEXT.
ENDIF.
mv_light = iv_new_state.
ENDMETHOD.
ENDCLASS.
However, one thing to keep in mind is that there is no way to refer to each of the values in a static way (as they have to be fetched from the database first). That sometimes leads to a situation where you have a domain and constants you have to keep in sync. For example in a CASE statement. An advantage is though, that you can use traditional dynpro value helps / you get them “for free” with localization support (and also domain appends).
Using new 7.51 enumerations
Since AS ABAP 7.51 there is a construct available at language level for exactly the use case described here. The implementation chosen by SAP (to my surprise) is “value type based” and not object oriented as in many other languages (like Java or C#). In short: by defining an enumeration you define a type (value type to be exact) and associated constants for it, that implicitely get values assigned if you don’t do it yourself. Additionally any assignment to data objects that are typed as an enumeration type will get compile time checks if the associated constants are used or not. If not it results in a syntax error.
I unfortunately do not have access to a 7.51 system (AS ABAP 7.51 developer edition where are you?) but looking at the documentation it should work like this:
CLASS zcl_light_switcher DEFINITION
PUBLIC
CREATE PUBLIC.
PUBLIC SECTION.
TYPES:
BEGIN OF ENUM gte_light_state,
green,
yellow,
red,
red_and_yellow
END OF ENUM gte_light_state.
METHODS:
switch_light IMPORTING ie_new_state TYPE gte_light_state.
(...)
ENDCLASS.
In my opinion these should replace the interface constants in all use cases, as long as you don’t have to worry about backwards compatibility.
I think it makes sense in ABAP to not have enumerations implemented as classes because of the lack of inner classes (local classes are not statically accessable outside of the class). So you would have to create a global class for any small enumeration, which, if you do it consistently, quickly assumes alarming proportions like the SALV library has with constant interfaces.
However in other cases you might want to bundle some functionality with your enumeration instead of just providing the fixed values. Let’s see how you can do that.
Class based enumerations
In other programming languages with OO support (like Java or C#) there are so called enumeration classes. Unlike the new ABAP language construct which is also called enumeration, enumeration classes are “syntax sugar” that generate classes and instances of them for each fixed value. In Java for example you can write something like this for our traffic lights:
package com.flaiker.scnenums;
public enum LightStates {
GREEN, YELLOW, RED, RED_AND_YELLOW;
}
What happens behind the scenes is that a normal Java class is generated with public static final class attributes for each of the fixed values. These are reference variables to their own class LightStates which are initialized in the static class constructor. Since these are just normal Java classes under the hood you can add additional methods, attributes etc. (please don’t quote me on how exactly the compiler does it)
package com.flaiker.scnenums;
public enum LightStates {
GREEN("Green"),
YELLOW("Yellow"),
RED("Red"),
RED_AND_YELLOW("Red and yellow");
private String name;
private LightStates(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
In ABAP we unfortunately do not have enumeration classes. We can however implement them “manually” effectively skipping the step the Java compiler does automatically.
Minimal OO enum implementation
CLASS zcl_light_state DEFINITION
PUBLIC
FINAL
CREATE PRIVATE.
PUBLIC SECTION.
CLASS-METHODS:
class_constructor.
CLASS-DATA:
go_green TYPE REF TO zcl_light_state READ-ONLY,
go_yellow TYPE REF TO zcl_light_state READ-ONLY,
go_red TYPE REF TO zcl_light_state READ-ONLY,
go_red_and_yellow TYPE REF TO zcl_light_state READ-ONLY.
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_light_state IMPLEMENTATION.
METHOD class_constructor.
CREATE OBJECT: go_green, go_yellow, go_red, go_red_and_yellow.
ENDMETHOD.
ENDCLASS.
As you can see just like in the Java enumeration we have static reference variables for each of the enumeration values. These cannot be changed from the outside (because they are READ-ONLY and effectively final). They are loaded on first use of the class using the class constructor and refer to instances of the class itself. It is not possible to create any other instances of the class because the constructor is private (CREATE PRIVATE) and the class is also final (so no publicly creatable subclasses). This means there can only ever be these 4 instances of the class available (*). You can now use the enumeration class similarly to the constants interface but except for comparing constant values now memory addresses will be compared.
CLASS zcl_light_switcher DEFINITION
PUBLIC
CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
"! Switch the light
"! @parameter io_new_state | The new light color
"! @raising zcx_illegal_argument | io_new_state cannot be null
switch_light IMPORTING io_new_state TYPE REF TO zcl_light_state
RAISING zcx_illegal_argument,
"! @parameter ro_state | Current light state
get_current_state RETURNING VALUE(ro_state) TYPE REF TO zcl_light_state.
PROTECTED SECTION.
DATA:
mo_light TYPE REF TO zcl_light_state.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_light_switcher IMPLEMENTATION.
METHOD switch_light.
" Validate io_new_state
IF io_new_state IS NOT BOUND.
RAISE EXCEPTION TYPE zcx_illegal_argument
EXPORTING
is_message = zcx_illegal_argument=>gc_nullpointer
iv_par_name = 'IO_NEW_STATE' ##NO_TEXT.
ENDIF.
mo_light = io_new_state.
ENDMETHOD.
METHOD get_current_state.
ro_state = mo_light.
ENDMETHOD.
ENDCLASS.
For the caller of the switch_light method it is clear that io_new_state must be an instance of zcl_light_state because of the type of the parameter. He can therefore (knowing of class based enumerations) look for the available values in the class-data of the class and use one of the values as the actual parameter, like this:
lo_light_switcher->switch_light( zcl_light_states=>go_green ).
Because the parameter is now an object reference to an instance of a class where there can only ever be the instances created which are defined in itself, the only illegal parameter value is null, which you should probably check for.
Compared to a constants interface or a domain or even 7.51 enumerations you might say this is much more effort. And it is. However, the advantages are that you let the type system check values of parameters at compile time (except null) and you have a full on ABAP objects class, in which you can bundle related functionality to the enumeration, like serialization, additional attributes etc. Here is a more advanced example.
Advanced OO enum example
"! Enumeration to represent states of traffic lights
CLASS zcl_light_state DEFINITION
PUBLIC
FINAL
CREATE PRIVATE.
PUBLIC SECTION.
CLASS-METHODS:
class_constructor,
"! Get enum instance from name
"! @parameter iv_name | Name of the enum value
"! @parameter ro_light_state | Found enum value
"! @raising zcx_illegal_argument | iv_name is not the name of any enum value
from_name IMPORTING iv_name TYPE string
RETURNING VALUE(ro_light_state) TYPE REF TO zcl_light_state
RAISING zcx_illegal_argument.
METHODS:
"! Get the next state
"! @parameter ro_next | Next state
get_next RETURNING VALUE(ro_next) TYPE REF TO zcl_light_state.
CLASS-DATA:
"! Traffic can and should flow
go_green TYPE REF TO zcl_light_state READ-ONLY,
"! Traffic can flow but soon cannot
go_yellow TYPE REF TO zcl_light_state READ-ONLY,
"! Traffic must not flow
go_red TYPE REF TO zcl_light_state READ-ONLY,
"! Traffic must not flow but can soon
go_red_and_yellow TYPE REF TO zcl_light_state READ-ONLY.
DATA:
mv_name TYPE string READ-ONLY.
PROTECTED SECTION.
PRIVATE SECTION.
METHODS:
"! @parameter iv_name | Name of the light state, must be unique!
constructor IMPORTING iv_name TYPE csequence.
CLASS-DATA:
gt_registry TYPE STANDARD TABLE OF REF TO zcl_light_state.
ENDCLASS.
CLASS zcl_light_state IMPLEMENTATION.
METHOD class_constructor.
DEFINE init.
&1 = NEW #( &2 ).
INSERT &1 IN gt_registry.
ASSERT lines( gt_registry[ table_line->mv_name = &2 ] ) = 1.
END-OF-DEFINITION.
init: go_green `Green`,
go_yellow `Yellow`,
go_red `Red`,
go_red_and_yellow `Red + Yellow`.
ENDMETHOD.
METHOD from_name.
TRY.
ro_light_state = gt_registry[ table_line->mv_name = iv_name ].
CATCH cx_sy_itab_line_not_found INTO DATA(lx_ex).
RAISE EXCEPTION TYPE zcx_illegal_argument
EXPORTING
is_message = zcx_illegal_argument=>gc_enum_not_registered
iv_par_name = 'IV_NAME'
iv_value = iv_name
ix_previous = lx_ex ##NO_TEXT.
ENDTRY.
ENDMETHOD.
METHOD constructor.
mv_name = iv_name.
ENDMETHOD.
METHOD get_next.
ro_next = SWITCH #( me WHEN go_green THEN go_yellow
WHEN go_yellow THEN go_red
WHEN go_red THEN go_red_and_yellow
WHEN go_red_and_yellow THEN go_green ).
" If this fails a new enum value has been added and is not yet supported
" by this method.
ASSERT ro_next IS BOUND.
ENDMETHOD.
ENDCLASS.
lo_light_switcher->switch( lo_light_switcher->get_current_state( )->get_next( ) ).
In standard SAP these classes are for example used in the ABAP workbench. If you look in the related development packages you can find classes prefixed with CE (E for enumeration I assume) that are built and used in exactly this way. In fact the workbench is a very good example for enumeration classes because they are ideally used in technical developments because persisting them to the database requires extra steps (they need to be serialized to a constant value again) and for classic user interfaces there will be no generated value help dialogs.
If you look further in the repository infosystem with CL*ENUM* you can find lots of other examples. There are even abstract base classes for enumerations you might want to take a look at (for example CL_BCFG_ENUM_BASE).
Comparison
Technology | Dynpro support | Static access to values | Values can be added without needing to adjust validation logic | DB persistence | Enhancable using additional methods / attributes |
Constants (in Type-Pools, Interfaces, Classes) | No | Yes | No | Yes (using constant) | No |
Domains | Yes | No | Yes | Yes (using domain key) | No |
Enumerations (7.51) | No | Yes | Yes (check for INITIAL needed and maybe more? *2) | Yes (using auto assigned constant) | No |
OO enumeration classes | No | Yes | Yes (only nullpointer validation needed) | Serialization logic must be implemented | Yes (full OO class) |
Combining the two different enumerations
So of course I thought “How can I have the advantages of both technologies, language level value type based enumerations with compile time checks and no nullpointers and also enumeration classes with utility methods and additional attributes?”.
So here’s a try at that, though I am not too happy with the following implementation. I don’t see a way to connect the enumeration constant with the enumeration class instance (“companion object”) without using a static accessor method.
CLASS zcl_light_state DEFINITION
PUBLIC
FINAL
CREATE PRIVATE.
PUBLIC SECTION.
TYPES:
BEGIN OF ENUM gte_light_state,
green,
yellow,
red,
red_and_yellow,
END OF ENUM gte_light_state.
CLASS-METHODS:
get IMPORTING ie_enum TYPE gte_light_state
RETURNING VALUE(ro_light_state) TYPE REF TO zcl_light_state.
METHODS:
get_next RETURNING VALUE(ro_next) TYPE REF TO zcl_light_state.
DATA:
mv_name TYPE string READ-ONLY,
me_enum TYPE gte_light_state READ-ONLY.
PROTECTED SECTION.
PRIVATE SECTION.
TYPES:
BEGIN OF gty_registry,
enum TYPE gte_light_state,
instance TYPE REF TO zcl_light_state,
END OF gty_registry.
METHODS:
constructor IMPORTING iv_name TYPE csequence
ie_enum TYPE gte_light_state.
CLASS-DATA:
gt_registry TYPE SORTED TABLE OF gty_registry WITH UNIQUE KEY enum.
ENDCLASS.
CLASS zcl_light_state IMPLEMENTATION.
METHOD class_constructor.
DEFINE init.
NEW #( iv_name = &1 ie_enum = &2 ).
END-OF-DEFINITION.
init: `Green` gte_light_state-green,
`Yellow` gte_light_state-yellow,
`Red` gte_light_state-red,
`Red + Yellow` gte_light_state-red_and_yellow.
ENDMETHOD.
METHOD get.
TRY.
ro_light_state = gt_registry[ enum = ie_enum ].
CATCH cx_sy_itab_line_not_found INTO DATA(lx_ex) ##NEEDED.
ASSERT 1 = 2.
ENDTRY.
ENDMETHOD.
METHOD constructor.
mv_name = iv_name.
me_enum = ie_enum.
INSERT VALUE #( instance = me enum = ie_enum ) INTO TABLE gt_registry.
ASSERT sy-subrc = 0.
ENDMETHOD.
METHOD get_next.
ro_next = SWITCH #(
me->me_enum
WHEN gte_light_state-green THEN get( gte_light_state-yellow )
WHEN gte_light_state-yellow THEN get( gte_light_state-red )
WHEN gte_light_state-red THEN get( gte_light_state-red_and_yellow )
WHEN gte_light_state-red_and_yellow THEN get( gte_light_state-green )
).
ASSERT ro_next IS BOUND.
ENDMETHOD.
ENDCLASS.
No comments:
Post a Comment