Speaking in ABAP Patterns
A colleague’s trying to identify my accent made me reflect on how I care more about idioms and patterns present in my ABAP code than about my german pronunciation.
ABAP as a 4GL language was positioned as a glue to empower experts I call data professionals with a firm grasp of business processes and the specific data model to build queries translating functional requirements into Useful Software(TM).
The expert programmer first instinct is to create beautiful code, the data professional will focus on modeling, leveraging the right data model using existing tools, e.g.
- screen programming, web dynpro and the RESTfull programming model all involve operating system independent abstractions of the user interface (UI).
- imperative data processing is made simple with ABAP SQL and the versatile idioms/statements for internal tables like LOOP and MOVE-CORRESPONDING.
Is Code beautiful?
Reading code triggers emotions, but everyone exposed to code develops views on code quality that might be different from mine. Uncle Bob prefers clean, tidy code written with care with barely nothing left to chance. I want code that is design driven, where the main problem is solved in a simple, elegant way with a minimum of complexity. I see it as the natural result of a coder understanding of a problem domain and choosing abstractions carefully.
But while maintaining ABAP code I am keenly aware of the difference between legacy and contemporary code styles. I have observed how the subset of ABAP commands in use has evolved with time and target platform. Personally…
- I prefer local classes to globals. So I have no problem with many small classes. I often like to extract the logic of global classes in local classes. Because of this, I never have some issues but already had to find solutions to another class of issue.
- I like macros and use them to avoid complicated generic code.
- I use hungarian notation.
- I value inheritance, but I prefer to avoid it.
So my approach might be different and my code different from yours. Before using the qualifiers good/awesome/bad/very bad or saying something is clean/dirty, please provide some context, a implicit or explicit reference to your evaluation paradigm. Specify your vantage point.
What is this code trying to achieve? Can you recognize the intent by reading it?
IF gv_last_approver_index IS NOT INITIAL AND gv_last_approver_index LT is_approver-approval_index.
It is part of a release logic with an approval chain. The logic checks the approval sequence:
- has any approval been done yet? and
- is the final approval still pending?
How would you have written it? If you are familiar with the problem at hand, you will look for clues where the code fits in the process.
If you have experience in writing code for the domain, you will look for best practices, idioms, tricks of the trade, pitfalls avoidance, know problems you are now well aware of. So if the code is a solution to a recurring problem, you would want to give advice to the developer (maybe your younger self), you would want to teach from experience. I was compelled to change this code
UNASSIGN <ls_approver>. ASSIGN COMPONENT lv_comp OF STRUCTURE ls_appr TO <ls_approver>. CHECK <ls_approver> IS ASSIGNED.
because I know it is a dynamic ASSIGN to:
ASSIGN COMPONENT lv_comp OF STRUCTURE ls_appr TO <ls_approver>. CHECK sy-subrc EQ 0.
Quality is in the eye of the beholder.
There are differences in the way people think about design, construction, and testing“. These alternative viewpoints tend to come up as a result of of someone saying I never (find a need to) use that pattern or I never tun into that smell. Understanding that others subscribe to a different philosophy helps us appreciate why they do things differently. (Meszaros, xUnit Test Patterns).
We want to review code written in styles and idioms we do not use and still and recognize its value, its quality. We want to recognize useful patterns. This is what a pattern language tries to do.
Speaking in Patterns
Pattern languages have been developed to pass knowledge by integrating different vantage points. Implementations diverge. Just saying “Mine is good, it is proven, use it” will trigger discussions and counterpoints. My ABAP pattern language below formats a solution to a recurrent problem with focus on the vantage points. I try to say: I had this problem, I used this solution and it was helpful in this context. With the hope that someone will add the limits of the solution by providing a context where it does not work well. A well written pattern is an enlightening discussion of contexts for a given problem.
Have you already seen a pattern language for ABAP? Clean ABAP comes close for best practices. Github works well as a discussion platform. But where would you go to see pattern of ERP development? I will provide some for ABAP reports here. Feel free to discuss in the comments.
I will try to use the following pattern description:
- Category (e.g. Best Practices)
- Name – (e.g. Initialization, describes intent)
- Problem – (e.g. variables are not in a valid state before code is executed, prerequisite are not met i.e. for helper variable in a LOOP. runtime errors occur)
- Approach – (e.g. we want to ensure a default valid state before code is executed)
- How it works – (e.g. Initialize your variable before you use it, make sure helper variables in LOOP are initialized before usage).
- When to use it – while coding, review each temp. variable
- Further Notes – this can be enforced with the Create Object pattern in Object Oriented code. By making the default constructor private or protected, a factory method can be enforced as only way to create an object for external clients of the class.
Category: Objects Discovery for ABAP Reports
ABAP Report Model View Controller
Name: ABAP Report Model View Controller
Problem: We have a data-driven report in Input / Processing / Output style and want to convert it to objects. How shoud an ABAP report be structured? How can we partition a report in an object oriented way?
Approach: The general design pattern is layered architecture. We define distinct layers which interact only through defined interfaces with each other. The most popular pattern is MVC.
How it works: Create an application class that will be the CONTROLLER. It has an entry point for the program that will be called at the START-OF-SELECTION event and other possibly static method of events like AT-SELECTION-SCREEN-OUTPUT.
- Create an view class that will be used to implement the output, e.g. ALV.
- Create a model class that will contains the business logic, e.g. how to retrieve the data.
When to use it: while converting classical reports to ABAP Objects. While refactoring existing reports.
ABAP Report Parameter Object
The suggested refactoring process for ABAP reports resonates with me. For some time now I have been extracting selection screen parameters into an object as one of the first steps. If anyone else has done the same, then I would suggest to elevate this practice to a pattern:
Name: ABAP Report Parameter Object
Problem: How do we handle the global selection screen parameters in an ABAP Objects program?
Approach: We create a new parameter object. The parameter object is passed to all routines that need access to the global variables. It can be generated with specific values in unit tests.
How it works: Create a class with a private method GET_SELECTION( ) that is called in the constructor. With this, a VALUE OBJECT is created with all current selection parameters as member variables as public read-only members of the class.
When to use it: while converting classical reports to ABAP Objects
Further Notes: The parameter object is a dumb object (data only) with minimal behavior. After refactoring, more behavior could be moved to its class.
If we have this selection-screen definition:
SELECT-OPTIONS s_class FOR vseoclass-clsname. PARAMETERS: p_dtls AS CHECKBOX DEFAULT 'X', p_subco AS CHECKBOX.
then we can create this class
TYPES tt_r_clsname TYPE RANGE OF vseoclass-clsname. CLASS lcl_param DEFINITION. PUBLIC SECTION. DATA r_clsname TYPE tt_r_clsname READ-ONLY. DATA dtls TYPE flag READ-ONLY. DATA subco TYPE flag READ-ONLY. METHODS constructor. PRIVATE SECTION. METHODS get_selection. ENDCLASS. CLASS lcl_param IMPLEMENTATION. METHOD constructor. get_selection( ). ENDMETHOD. METHOD get_selection. r_clsname = s_class. dtls = p_dtls. subco = p_subco. ENDMETHOD. ENDCLASS.
If the developer does not use the global program variables, they can be renamed/removed later.
The intent is to remove any reference to global selection variables in the code an replace it by access to an attribute of the PARAMETER object. The attribute are READ-ONLY to avoid having them changed everywhere. The PARAMETER class can offer friendship to a test class to support test data creation. I think this creates a path for further refactoring.
I usually define a CONTROLLER class, that interacts with a MODEL and a VIEW class. The controller passes the PARAMETER object to all other classes.
ABAP Report User Object
In ABAP reports we see authorization checks all over the place, but they are usually not well tested. The question is always which user group will use this report, which roles are needed. Code can fail to perform correctly in production because some parts are not being executed due to missing authorization, so I suggest this practice:
Name: ABAP Report User Object
Problem: How do make sure the all authorization checks are correctly handled without creating a set of test users specific for each report?
Approach: We create a new user object. The user is passed to all routines that need authorization checks. It can be replaced by mock users in unit tests.
How it works: Create a user class and move all authorization checks as public methods of the user class. The CONTROLLER class can create an object of this class and pass it around
When to use it: while converting classical reports to ABAP Objects, while refactoring ABAP reports
ABAP Report Log Object
Name: ABAP Report Log Object
Problem: How do we collect all errors and messages in an application? Can we avoid passing a message table parameter to all routines?
Approach: We create a new log object. The log object could be initialized with a parameter object. It is passed to all routines who can generate errors/messages.
How it works: Create a class with a private attribute MT_MESSAGES and some public method that can add system messages, exceptions and other messages to the message log. For system messages, the MESSAGE statement is executed with an INTO variant and the system variables SY-MSGV1 to SY-MSGV4 and other SY-MSGTY are evaluated.
When to use it: while converting classical reports to ABAP Objects, while refactoring.
Further Notes: The log object can include additional behavior like displaying the list of messages. But it should be foremost a message collector. Adding too much behavior here could hide application specific objects that should be extracted to a new class.
Category: Best Practices
Problem: Poor performance because of an activity with high costs (e.g. ABAP SQL query database field) is repeated often in the client code.
Approach: Memoization is a specific form of caching you have propably already implemented in ABAP code to buffer ABAP SQL queries: The value is read from the data source the first time it is accessed and saved. Subsequent reads just return the saved value.
How it works: Implement a lazy read operation. For ABAP SQL the best solution is usually to define the table as buffered. The second best solution is to manage an sorted/hashed internal table that buffers the results. With table expression, we can use the DEFAULT option in VALUE operator to implement the pattern
rv_level = VALUE i( mt_list[ mv_idx + 1 ]-from_level DEFAULT c_default_level ).
Looks like a nice way to replace the ubiquitous SY-SUBRC check after READ TABLE, but there is more. After DEFAULT you have a general expression position. (Horst Keller’s sound bite), so you can really call a function
rv_index = VALUE #( mt_actor[ object = is_object ]-index DEFAULT index_of( is_object ) ).
and also buffer cache misses.
CLASS lcl_marc_proxy DEFINITION FRIENDS lif_unit_test. PUBLIC SECTION. METHODS constructor IMPORTING iv_plant TYPE werks_d. METHODS exists IMPORTING iv_matnr TYPE matnr RETURNING VALUE(rv_flag) TYPE boole_d. PRIVATE SECTION. TYPES: BEGIN OF ts_hit, matnr TYPE matnr, found TYPE flag, END OF ts_hit. TYPES tt_hit TYPE SORTED TABLE OF ts_hit WITH UNIQUE KEY matnr. DATA mv_werks TYPE werks_d. DATA mt_hit TYPE tt_hit. METHODS fetch IMPORTING iv_matnr TYPE matnr RETURNING VALUE(rv_found) TYPE boole_d. ENDCLASS. CLASS lcl_marc_proxy IMPLEMENTATION. METHOD constructor. mv_werks = iv_plant. ENDMETHOD. METHOD fetch. SELECT SINGLE matnr FROM marc INTO @DATA(lv_matnr) " Existence check WHERE matnr = @iv_matnr AND werks = @mv_werks. rv_found = xsdbool( sy-subrc EQ 0 ). INSERT VALUE #( matnr = iv_matnr found = rv_found ) INTO TABLE mt_hit. ENDMETHOD. METHOD exists. " existence check with chache rv_flag = VALUE #( mt_hit[ matnr = iv_matnr ]-found DEFAULT fetch( iv_matnr ) ). ENDMETHOD. ENDCLASS.
When to use it: When you cannot delegate the buffering operation to the data access object.
Further Notes: cf. Multiton.
The following has still to be written an a pattern language. I suppose the pattern is the functional error monad that could make my head explode. Still, I am writing this down here as a starting point for the poor man’s options we have in ABAP.
Let us abstract a processing as a type transformation, say input x, output a:
a of Type A = calculate_from( x : Type X ).
But what do you normally can expect is either an object of type A, or an error, an exception object! So the transformation result is better described as either of type of A (standard path) or an exception object of type… let’s say CX_ROOT. This is a so called algebraic type in functional programming.
We handle simple cases in ABAP with return codes (e.g. SY-SUBRC), exceptions objects and the Null Object.
Exceptions allow to separate error handling from the normal processing.
TRY. a = calculate_from( X ). b = derive_from( a ). " standard path c = determine_from( b ). CATCH cx_root INTO lx_error. MESSAGE lx_error TYPE 'I'. " exception ENDTRY.
But some may like return codes or have legacy code that needs them:
* create proxy instance TRY. CREATE OBJECT lo_proxy EXPORTING logical_port_name = 'Z_LP_MY_PORT'. CATCH cx_ai_system_fault. RETURN. ENDTRY.
But there is also the option to return a Null Object. Define a Null Value for each Type – with this we can encapsulate error code checking code.
If we use extract method, each functional method can start with
return value = Null Value for this type
We make sure the Null Object does not crash the rest of our application, but it does not do anything useful either. Like 0 for the addition, so the follow up calculation can continue.
Jason Mc C. Smith: Elemental Design Patterns, Addison-Wesley, Pearson Education Inc.
Started as https://www.informatikdv.de/speaking-in-patterns/
The buffer is awsome
It is a modern expression of the buffer/proxy/memoization that leverages DEFAULT.
What a great blog! Thanks so much for sharing.
I've been hearing about "patterns" for many years now but it's always been a challenge to align the theory with ABAP practice. I had high hopes for the SAP Press book on ABAP object patterns but was rather disappointed with it, mostly because the structure was "upside down": it started with the pattern list and then went into ABAP examples for each. But on practice, it's not how we search for information.
Your blog is much more useful to me because it focuses on the specific, and very common, ABAP tasks. I wish we had more content like that instead of some pattern examinations that can be too academic.
Thank you for the feedback.
Did you checked a design pattern site like sourcemaking.com?
I think a pattern examination is useful for those who have/had the recurrent problem. e.g. I found a use of the iterator pattern in ABAP in the context of the event handler for ALV.
Following the MVC separation with data (internal table) in the MODEL and a copy or reference in the VIEW, what happens when the user selects a subset of the rows for processing (update)? where will the event handler be implemented? It should be in the and CONTROLLER, but then how to access the MODEL data?
What I see often is that the model data (internal table) is made global.
A better solution is to make the controller return an iterator to the model data. Each NEXT( ) call returns a reference to the next row selected by the user.
Good blog. Just a few comments.
I have no problem with many small global classes. I find local classes that are tightly linked to their global class very useful – also a local main class in a report program. One advantage of global classes is that if you change a method implementation, you can transport just that method. If the class is local, you have to transport the whole thing.
Macros cannot be debugged. I have never had a case where a macro solved a problem of complicated code that couldn’t be solved in another way. This is even more the case since the ABAP simplification of 7.4.
I do not, because I agree with DSUG and SAP’s own ABAP guidelines. I do use lcl, ltd, ltc for local classes, test double and test classes. I also use prefixes for parameters, indicating what kind of parameter they are – importing, exporting, changing or returning. For returning, I use r_result (or ret_result if the client naming conventions insist). I never use me->.
Yes it is. I’ve often replaced the sy-subrc check with “IF not_found( )”, where not_found is a functional method that evaluates sy-subrc and returns abap_true if not 0. I’ve done this because my most common coding error is forgetting the NOT…
I tend to avoid it as well. I much prefer to use interfaces nowadays. Most classes I create (internal or external) will be with respect to an interface, to make it easier to use abap unit test with injection.
Global vs. local classes: I see the difference in visibility, ease of handling/change.
If find local classes malleable. I can easily extract a dumb data object without method to extract some attributes that belong together and add behavior (methods) later.
This of course can be done with a global class, but local classes are easier to manage in SE80. Even in ADT I find it easier to work with 10 local classes in a single INCLUDE while I would have to keep 10 tabs open for the global class. So a global class have some overhead and I will have a problem with many global classes.
In my view, the real benefit is that the public interface of local class is not published, as in public vs. published. I feel free to make significant changes (e.g. rename methods, change method signature, move from inheritance to composition and vice versa) late in the process to accommodate a better design.
As a local class is usually in one or two includes, transporting is simpler then transporting a global class with its public/protected/private parts. i.e. I do not see a disadvantage.
Macros are useful when they enhance readability. I agree macros are not needed, but even the documentation has of examples of good as well as bad examples, and you once said they were tempting for unit test fixtures... I specifically referenced complex generic code, but I generally do not limit myself by taking macros out of the equation.
On hungarian notation I will state the ABAP stack heavily uses prefixes to convey information on development objects. Your notation does not eliminate prefixes altogether, you follow a different set of rules.
My thesis: setting an enforceable naming convention is A Good Thing. Hungarian notation is an acceptable alternative to achieve this.
Thanks for starting the discussion
I came here on a search how I can past on selection-screen data of a report to a global class, and it did help me:
I think does 4 lines express what I learned:
But I already see your blog has a whole lot more good info, I'll bookmark it as a read later.
I like the "ABAP Report Parameter Object"