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.