The Global Variable Dilemma
When doing complex coding, there’s always the dilemma of using or non using global variables. We all know that global variables are dangerous because you never know where they are set and what value they carry in a specific method.
On the other hand, if I keep everything local, I am ending with huge lists of parameters in every method just to pass the values needed to the deepest nested method where I need it.
An example out of my daily work: I am refactoring a huge print program where a lot of global variables are set in various locations of the code. Let’s take one simple variable, which is the plant code. It is used in various methods because many utility classes I use take it as parameter. The plant is determined when the order that is to be printed is being read from db. If I make it local, I have to pass it to dozens of methods that need to know about the plant. On the other hand, if I make an instance attribute of it, I never can be sure that it is set when using it somewhere around in the code.
What I have startet to use for this case is a class I call data provider that keeps all data to be re-used throughout the code. It is instantiated at the very start of processing and keeps only data that is set once and remains set to its first value during the whole process. All attributes of that class are populated with instantiation and can not be changed any more (there are only getter methods for the values). In my main class, I instantiate the data provider as soon as possible and store the reference in an instance attribute so it is available to all methods.
Example
In this example report, a pp order is given and all occurring work centers in the operations are being determined and sent to output. In this simple program the usage of a data provider may be too much overhead, but if the program grows and you need to access the order data in many nested methods, it is as handy as global data but without the danger of overwriting it accidentally. The data provider gives also transparency about where the data comes from. Just use the where-used list and you understand that the data is provided in the constructor.
I would appreciate to know about your approaches!
*&---------------------------------------------------------------------*
*& Report ZP_TMP_DATA_PROVIDER
*&
*&---------------------------------------------------------------------*
*& Demo for SCN blog post "The global variable dilemma"
*& This report creates a list of all work center header data
*& used in a production order
*&---------------------------------------------------------------------*
report zp_tmp_data_provider.
parameters p_ordid type aufnr.
class lcl_data_provider definition.
public section.
types gty_t_operations type standard table of afvc with default key.
methods constructor importing iv_orderid type aufnr.
methods get_header returning value(rs_res) type aufk.
methods get_pp returning value(rs_res) type afko.
methods get_operations returning value(rt_res) type gty_t_operations.
private section.
data: ms_order_header type aufk,
ms_order_pp type afko,
mt_operations type gty_t_operations.
endclass.
class lcl_data_provider implementation.
method constructor.
" general order data
select single * from aufk
where aufnr = @iv_orderid
into @ms_order_header.
" production-related order data
select single * from afko
where aufnr = @iv_orderid
into @ms_order_pp.
" order operations
select * from afvc
where aufpl = @ms_order_pp-aufpl
into table @mt_operations.
endmethod.
" getter methods for read-only access to the global data
method get_header.
rs_res = ms_order_header.
endmethod.
method get_pp.
rs_res = ms_order_pp.
endmethod.
method get_operations.
rt_res = mt_operations.
endmethod.
endclass.
class lcl_app definition.
public section.
types: gty_t_crhd type standard table of crhd with default key.
methods constructor importing iv_orderid type aufnr.
methods get_workcenter_list returning value(rt_res) type gty_t_crhd.
private section.
data: mo_data_provider type ref to lcl_data_provider.
methods get_workcenter_oper
importing is_oper type afvc
returning value(rs_res) type crhd.
endclass.
class lcl_app implementation.
method constructor.
" instantiate the data provider
mo_data_provider = new #( iv_orderid ).
endmethod.
method get_workcenter_list.
" use data provider for accessing global data without being able
" to modify it
loop at mo_data_provider->get_operations( ) into data(ls_oper).
data(ls_workcenter) = get_workcenter_oper( ls_oper ).
collect ls_workcenter into rt_res.
endloop.
endmethod.
method get_workcenter_oper.
call function 'CR_WORKSTATION_READ'
exporting
id = is_oper-arbid " Work center ID
msgty = 'E' " Message type
importing
ecrhd = rs_res. " Header data
endmethod.
endclass.
start-of-selection.
cl_demo_output=>display(
new lcl_app( p_ordid )->get_workcenter_list( ) ).
Hello Jörg,
globals are a danger when they share mutable state. I think you recognized this by using immutable objects. I follow 2 GRASP here:
IMO your data provider is an example of the pure fabrication pattern. I often create a parameter class with PUBLIC READ-ONLY DATA objects to implement an immutable objects (No getters/No setters).
JNN
"share mutable state" and "GRASP" are new to me. Thank you for the input - I will try to get deeper into this.
And even the read-only addiction to public attributes is new to me! This will be very useful to me.
Thank you for sharing!
I generally objectify at a business level, because almost all data is in a SAP document/object somewhere. So your example I would actually have everything in global classes (with instance management for performance). Global, because a production order and work centre are standard SAP objects/documents, but for a PoC local classes will do just as well.
So my equivalent code would be along the lines of:
Mike,
I like this approach! If SAP didn't have an object, I'd create one. Although Jorg may be using the example so we can see all the code.
Jorg,
Great blog! Anything that makes me think a bit is a great blog. I did not know about GRASP either.
I am "living" this approach too - but not always. Especially when performance matters constraint me to do mass reads (in a report, for example), it can be very time-consuming creating a program object for each business object. If I have to analyze thousands of orders, I just cannot read every order separately.
In the code above, the work center table CRHD would be read multiple times with a single select, right?
This was always my dilemma when thinking of objectifying on business level. How do you approach this?
That is why I mentioned instance management and performance. There are lots of ways to deal with it.
You could work with a collection class that provides the tabular data and instantiates objects on demand (e.g. methods get_list, get_instances and get_instance that return a flat table, instance table and single instance respectively).
A Work Center class could get it's data from a private get_data method that checks a static table attribute and SELECTs a whole company code/plant/country's worth into the buffer if not there yet.
Those are just two approaches, you can get quite intricate by using references and boxed tables for large volumes. The aim here is to only ever work with a single copy of the data.
I would really like to read a blog about this. 🙂
Is that a hint or a challenge? 🙂
It's a subtle motivation. 🙂
Hi Jörn,
thanks for sharing!
I usually keep variables local and just accept, that I have to have lots of parameter to pass them into methods.
But I do like your approach with to data provider, maybe I can use it some time!
best
Joachim
Data Providers are a nice tool!
Encapsulated in a singleton pattern you can easily use the data within different classes/ programs without the need of reading the data again (performance issue)
Plus the data provider knows all (complex) additions and customer specialities of the data object (determing the status of the data object, assigned contracts, and so on). So the knowledge of what additional data (customer tables, involved plants, special dates etc) is inside this object. If there is an error in reading the data (not respected deletion marks, outdated data, different additional data depending on the status, ...) there is one place to correct it.
Data provider classes also can be used in derived classes without adapting the signatures of methods. If you have a special PP order that needs special additions you can derive this special data object from the main data provider "pp order" and add the supplemental stuff.
Well, I am a cowboy (or rather cowgirl) developer, so I blatantly disregard OOP and use global variables sometimes. Boo hoo, so sue me!
I know it's frowned upon these days but in all honesty I just can't bring myself to turn a simple report with 2 SELECTs into a much longer program for no good reason. However, this is driven by the knowledge that this development has no reusability potential and will have a rather short lifespan. I would not encourage anyone to create "technical debt" when it's a different situation.
I really appreciate this blog and the comments as I have no intention to stubbornly continue doing the same thing forever. I really loved the table headers but eventually (albeit begrudgingly) moved on, see? 🙂 ). I'm always curious how we can improve, as long as it's practical.
I also treat a 2 select report in a different way. Just no modularization at all, everything in one start-of-selection block. But as soon as I would need a subroutine, I wrap my coding with a static class in order to be able to create a method. I really hate performs...