Object oriented burritos: Wrapping DYNPRO screens into object oriented ABAP
This post is inspired by Module Pool Programming from scratch article by Sinela Anwar and also a recent conversation on the abapGit slack channel. In my experience, I found that few ABAP developers attempt to apply object-oriented principles to screens. On the one hand, the reason is clear: screens, as a technology, matured long before OO got popular in ABAP and then no one dared to modify a working and reliable concept. On the other hand, why not try? The benefits are there:
- more clear code structure
- less global variables
- better code decoupling and better maintainability
- potential unit testability (potential …)
Here are a couple of experiments I was applying within my team. I don’t pretend them to be perfect or even complete but I hope they could be interesting for the audience. And the constructive discussion in comments is welcomed (there is certainly room for opinions and improvements).
Principles/Goals (ideally …):
- minimize global variables
- minimize code in modules – pass control to OO as soon as possible
- class code and screen must know nothing about each other, the class may potentially become a global one
- minimize DDIC usage
Let’s consider the parts we have
- we have a global variable with the screen data
- we have an event of screen entry – PBO
- we have field events – PAI
- and we have user commands, fortunately, communicated in a single fashion – buttons, menu, toolbar
This can be described as the following interface.
interface zif_screen_controller. methods bind_screen_data changing c_scr_fields type any. methods on_user_command importing iv_cmd type sy-ucomm. methods pbo_screen_init. methods on_pai_check. endinterface.
We will see how it works later. Now let’s define a specific class and the screen data structure.
class lcl_my_scr_ctl definition. public section. interfaces lif_screen_controller. types: begin of ty_screen_fields, budat type bkpf-budat, ... end of ty_screen_fields. private section. data m_scr type ref to ty_screen_fields. methods handle_user_command importing iv_cmd type sy-ucomm raising cx_static_check. methods clear_screen_fields. endclass. class lcl_my_scr_ctl implementation. method lif_screen_controller~bind_screen_data. get reference of c_scr_fields into m_scr. clear c_scr_fields. " force clean screen fields on start, though this logic might differ for some cases endmethod. method lif_screen_controller~on_user_command. data lx type ref to cx_static_check. try. handle_user_command( iv_cmd ). catch cx_static_check into lx. message lx type 'E'. endtry. endmethod. method lif_screen_controller~pbo_screen_init. set pf-status 'MYSCR_STATUS'. set titlebar 'MYSCR_TITLE' with 'My screen'(001). m_src->budat = sy-datum. " ... and other initialization loop at screen. ... modify screen. endloop. endmethod. method lif_screen_controller~on_pai_check. data lx type ref to cx_static_check. try. if m_src->budat is initial. ... " Note that we have read/write control here catch cx_static_check into lx. message lx type 'E'. endtry. endmethod. method handle_user_command. case iv_cmd. when 'SAVE'. " Process commands ... when 'BACK'. leave to screen 0. when 'XXX'. ... endcase. endmethod. endclass.
Again, this might look incomplete without the wiring which comes below. But the main essence is:
- The class defines a public type ty_screen_fields for screen structure. No DDIC. This comes at a price e.g. screen translation should be maintained manually. But on the other hand, we obtain flexibility and convenience with data structure definition if we need to add or rename fields, and also the type is close to the code which works with it which is important.
- The class implements the interface defined above. User commands and PAI should be quite obvious. But a few comments to the rest:
- bind_screen_data binds an externally passed variable to a reference member inside the class. Thus you decouple the class from global data while keeping the full read/write access. With this approach you can even theoretically run unit tests over your screen logic … theoretically … there are obstacles and I frankly didn’t push it to the live results yet.
- pbo_screen_init does the initialization. And there are 3 parts to it.
- screen status and title. Well, the code as I showed above a coupled with the program, so there is room for improvement. One solution is to move it to the modules as an exception. Another is to pass IDs as parameters.
- setting initial values of the screen. This class is super-simplified, but a real example you have certain business logic and will probably do some initialization. It is possible due to m_src reference
- loop at screen. And again a room for improvement because screen is a global var. One solution is to pass it as a changing variable – this works well in fact. To ensure decoupling you should do it. For sure, if you want to write unit tests for your logic. If not, then maybe (maybe!), accessing to screen is an acceptable exception because it is kind of a calling context – you will always access the right “screen” unless you’re doing something wrong … well … disputable anyway. In case of doubts – go with changing c_screen.
Finally the wiring:
" TOP program section should have 2 global vars per screen data gscr_my_scr type lcl_my_scr_ctl=>ty_screen_fields. data gi_my_scr_order_ctl type ref to zif_screen_controller. " Modules would look like module pbo_0100 output. if gi_my_scr_order_ctl is not bound. create object gi_my_scr_order_ctl type lcl_my_scr_ctl. endif. gi_my_scr_order_ctl->bind_screen_data( changing c_scr_fields = gscr_my_scr ). gi_my_scr_order_ctl->pbo_screen_init( ). " maybe with changing c_screen = screen endmodule. module user_command_0100 input. gi_my_scr_order_ctl->on_user_command( ok_code ). endmodule. module pai_0100. gi_my_scr_order_ctl->on_pai_check( ). " maybe with changing c_screen = screen endmodule.
For this to work you must also properly refer to gscr_my_scr in your screen definition. So the fields should refer to e.g. gscr_my_scr->budat.
So what we achieved: the “CPU time” in modules is minimal, and control is immediately transferred to OO code. No superfluous global variables. All business logic is tightly grouped within a relevant class. The class is approaching unit testability, though there is some way to go to achieve it.
Caveats, not all is solved and there are spots for doubts:
- screen – should you pass it with a param or use it directly? discussed above.
- status and title – should you call it inside the class or in the module? it leaves room for opinions.
- translations – yeah, this is a weak spot, you will have more effort on constructing the screen definition itself.
- leave to screen 0 on BACK – that you might have noticed above does also couples the class to the screen logic, I’m not sure how to unify it, maybe it should be in modules as well
- calling other screens – this is a bit tricky. Occasionally you have to call screen XXXX from another screen. I didn’t find anything really elegant here, but my approach would be to have an additional “super controller” interface, passed to screen controllers at construction time that would have meaningful methods like “display_dialog_x” or “show_line_detail“. The interface would be implemented in a local class of the program and “know” which screen is for what. This wouldn’t be a huge complication and keep the code decoupled.
Anyway, I hope this was useful or at least interesting. And welcome to constructive comments! 🙂
My 2 cents: SCREEN is not an internal table, the only possibility to use SCREEN is via the special statements LOOP AT SCREEN and MODIFY SCREEN (where SCREEN looks like an internal but is not), you can't change SCREEN, you can't pass it as parameter. But it's not a problem. It will work inside a method, provided that it runs during PBO or PAI.
You're right. Just tried it and it does not work. But I was sure it would because this worked:
Anyway good to know.
"LOOP AT SCREEN" alone transfers the line to the eponymous system structure SCREEN. It's deprecated now, we should use "LOOP AT SCREEN into data(your_own_structure)". Edit: and also "MODIFY SCREEN from your_own_structure".
I second that. Use a work area.
Cool! Not sure if you know about it but the BUS framework which is used in the business partner application attempts to abstract dynpros using ABAP OO as well and there are quite a few similarities in your approach 🙂 It's a bit heavier though (message handler, setup for table controls, popups, subscreens etc.) and also it's not released, the only use I know of by SAP is for the business partner maintenance (behind / in addition to BDT).
Since I need the DDIC features most of the time I bind the tables variable to a static variable and only access the global variable using that reference in the class. That ties the data structure together with the logic that uses it. (Code typed off the top of my head, this will not compile....)
Note you technically can also bind the dynpro fields to class members (even though they don't show up in the import fields from program dialog). Something like this:
Though you then lose some DDIC features (translation will always be manual and the other flags stay the same and are not synced to DDIC anymore if you imported the field from DDIC before renaming it). Therefore I opted for the approach with the reference.
Cool, I didn't know about it. Looks like a similar approach.
(many other applications use BDT like FM, RE-FX, but here BDT screen programming is based on function modules, I don't see cl_bus_abstract_main_screen being used, so I guess there are several ways to do screen programming in BDT)
(As I understand it BUS is an implementation detail of BDT which itself is entirely function module based, something which BDT used to do on its own before maybe. There's also "classic BDT" not sure if that's related. Or the dialog application connects BUS and BDT (*BUPA_DIALOG_JOEL* for BP), that would make more sense because you don't need BUS in direct input context. But you can set breakpoints in the screen classes and they do get hit in transaction BP and maybe in FM and RE-FX too.)
It's worth mentioning that more complex screens might require a more complex binding (a separate PAI module for each field/block).