ABAP to the future – my version of the BOPF chapters – Part 7: Actions
8.2.8 Responding to user input with Actions
In the previous chapters we’ve already been learning various types of behavior (which is in some kind also a response to user input). Actions however are modeled interactions with the business object. They are not side-effects of another interaction (like determinations), but are executed explicitly by a consumer in order to fulfil a predefined business purpose. Therefore, they are traditionally visualized on the UI with buttons.
Figure 56 – Executing an action in the Test-UI: Making a monster with a head howl
Figure 57 – Sulley’s howling three times the BOPF way
But the same purpose could be also triggered by another consumer such as a background-job or an inbound process agent. Usually, the action itself triggers one or multiple modifications to the business object (precisely the business object node instance) or related entities. Those modifications shall either succeed or fail as a whole. The complete sequence can be protected by one or multiple action validations. Cancellation processes are typical samples: The system validates (with action validations) prior to the action execution whether all pre-requisites for the cancellation are fulfilled (e. g. that no successor-document has not been created yet, another one may validated that the document itself is not cancelled yet). If the document can be cancelled, other successor documents might be cancelled as well or succeeding processes (such as a payment request) are being triggered. Finally, some status-attribute of the document itself is getting transferred to another domain value.
Wrapping it up: actions can be compared to public class methods of a UML class diagram which are being triggered as transitions between states (of a state machine).
BOPF is in some kind very strict with respect to object orientation: All modifying interactions on a business object (node instance) may only result in changes of the affected instances’ states. There is no knowledge about the business outside the business objects. Consequently, also actions cannot return any values but “only” result in changes of the state.
The actual process of defining an action is comparatively easy: The node at which you model the action is the one which instances are passed into the action implementation, so there are only the parameters and the multiplicity you need to decide upon.
As written, there’s no value being returned from an action, so all the parameters are “importing” and all of them are in fact optional. Complex structures such as tables are not supported.
Figure 58 – Configuration of a parametrized action
The multiplicity defines on how many instances an action can be executed with one call. Usually, if the business logic does not functionally exclude that the same logic is applied to multiple instances at a time, the unbound cardinality should be selected. The reason for this is – you might have guessed it – performance.
Figure 59 – Origin locations of a mass-enabled action identify the message-source
Figure 60 – Mass-enabled howling
Only in very few cases, a “single instance” action is really necessary. One example which comes to my mind would be “rank to top”, because finally there will be only one top with one instance on it. Factory-actions with a “static” cardinality have very rarely crossed my way. “Create from reference” is the only sample I remember with the referred instance semantic key being the parameter. But even then, one could argue whether a determination on create would not be the better option.
Figure 61 – Something quite advanced: If properties of the action might change, you can model which interaction makes them necessary to be re-retieved. Also the change of an associated node can trigger this invalidation.
Besides the modeled (“standard”) actions, there are the so-called “framework actions”. These actions are not meant to be called by a consumer (using the core-service do_action), but they are only called internally by the framework as a consequence of another core-service:
For each node,
- CREATE_<NODE>, UPDATE_<NODE>- and DELETE_<NODE>actions exists. These actions are being executed by the framework when performing a corresponding modification.
- LOCK_<NODE> is getting triggered internally, if a retrieve with an edit_mode other than read-only is requested
- UNLOCK_<NODE>is getting triggered internally, after a save with transaction pattern “save and exit” or a cleanup” or a cleanup via the transaction manager have been completed
Personally, I wish the service manager asserted that only standard actions can be consumed, this would eliminate some confusion for BOPF newbies / self-students trying to execute e. g. the action DELETE_ROOT and wonder why nothing happens.
Why I am listing those actions here if they can’t be consumed, you may ask. The reason can be found in the previous chapter: Framework-actions can be subject to action validations too! Consequently, a validation for e. g. an update can prevent the modification from being performed.
We’ll stick to the sample of howling at the moon. But we’ll imagine a bit of a different business logic: We’ll model the action at the ROOT and make each head howl the parametrized number of times. And will introduce some tiny more bit of complexity: The timestamp of the howling-request shall be persisted in the root-node. The sample in the book does not modify the state at all (which is very unrealistic) and gives me some more options to rant about delegation to a domain-model-class later on 😉
CLEAR et_failed_key. “don’t use failed keys in actions in general, rather implement proper separate action validations
eo_message = /bobf/cl_frw_factory=>get_message( ). “Initialize the message container. We’ll at least create one message in this method.
* The signature – as usual – is quite generic with an untyped data reference to the parameters.
* Casting it to a data reference of the actual parameters type saves you some field-symbol-handling and will furthermore raise a proper
* move-cast-exception if the consumer provides a data reference of the wrong type
DATA ls_howl_request_parameters TYPE REF TO zmonster_sa_howl_at_the_moon.
ls_howl_request_parameters ?= is_parameters.
* The action is mass-enabled as the howls of different monsters don’t interfere (from a business point of view, physically of course they do).
* So we’ll get all monsters’ heads mass-enabled as this may result in a DB-SELECT
* we’re not interested in any data of the instances (of ROOT nodes), but we want to make each head howl.
* For this, we also don’t need any data of the head, we just want to related the howls to the head instances
it_key = it_key
iv_node = zif_monster_c=>sc_node–root
iv_association = zif_monster_c=>sc_association–root–head
iv_fill_data = abap_false “we just need the heads’ keys
et_key_link = DATA(lt_link_root_head) ).
LOOP AT it_key INTO DATA(ls_root_key).
LOOP AT lt_link_root_head INTO DATA(ls_link_root_head) WHERE source_key = ls_root_key–key.
DO ls_howl_request_parameters->no_of_howls TIMES.
* We’ll create a howl for each head by making the message refer to the head which howled (using the origin location).
* The would allow the consumer (e. g. a UI) to graphically visualize the message (think of speech-bubbles in a comic-strip)
* Also note that we separated the actual business logic of howling into an own non-public-method.
eo_message->add_cm( NEW zcm_monster(
textid = zcm_monster=>howl
severity = zcm_monster=>co_severity_success
mv_howl = me->produce_howl( )
ms_origin_location = VALUE #(
bo_key = zif_monster_c=>sc_bo_key
node_key = zif_monster_c=>sc_node–head
) ) ).
ENDLOOP. “at the monster’s heads
* Finally, update the monster’s last howl timestamp – this comment is actually superfluous, as the next command is as readable 😉
GET time stamp FIELD DATA(lv_now). “I guess there’s also an inbuilt-function now, but I’ve don’t got a system I could look into at the moment 😉
iv_node = zif_monster_c=>sc_node–root
iv_key = ls_root_key–key
is_data = NEW zmonster_s_root( last_howl = lv_now )
it_changed_fields = VALUE #( ( zif_monster_c=>sc_node_attribute–root–last_howl ) )
ENDLOOP. “at monsters
rv_howl = ‘Oooooooooooooooooooooooh’(001). “Might by language dependent 😉
Failing an action
You probably noticed that the execute-method offers an et_failed_key parameter. If you read my commented code carefully, I recommended not to use it. The reason can also be found in the previous chapter on validations: There is a dedicated core-service check_action which can be used by the consumer in order to check whether an action can be executed. The framework will execute all action validations which are requested for the action, but not the action itself. Thus, failures which are only coded in the action implementation cannot be detected with check_action.
This is the major “technical” reason why I recommend to implement the checks for failure outside of the action. Furthermore, it easily messes up code if checking and manipulating logic is combined in one class or even in a single method.
Also remember that if you detect an instance as failed during action implementation, you need to undo all changes made so far, there’s no automatic roll-back.
About delegating an action to a model class
I have large complained about delegating business logic to a model class in the chapter on determinations. With actions, two shortcomings become more apparent:
- To fail or not to fail: Paul has used an exception to propagate failure of the action. I’m not a big fan of using exceptions for behavior which can be expected (and that an action cannot be performed is for sure expectable). Particularly if you are making use of mass-processing, raising exceptions can result in tricky situations as the program flow is being interrupted and the next instance might not be processed although it could be. In the case of no failure, a success-message often informs the human consumer about his powerful click-on-the-button. How would this propagated from a model-class without creating a dependency towards BOPF?
- The major purpose of an action execution is to manipulate the state of an instance. How could this be done inside a without creating a dependency to /BOBF/IF_FRW_MODIFY (io_modify)? Honestly, I don’t know. Having an own state in the model class instance will result in inconsistencies with the BOPF buffer, so I believe it’s most likely going to result in a stateless-model-class (which basically groups functions). As extensively described in the chapter on determinations, I believe that a composite with each class separating as much logic as possible in stateless non-public-methods of the different determination/action/validation-classes is the better architectural choice.