Using interfaces to standardize your ABAP OO Development
Hello!
This is my first post here on the forum, so i hope it will be useful for some of less experienced developers.
TLDR:
- You can utilize interfaces to move class functionalities to higher abstraction level
- You make code cleaner and nicer to look at
- Interfaces enable you to create mockup objects and increase code testability
- SOLID approach can be achieved
- With interfaces you make your class enabled for extension, but closed for modification
- Methods can use different object of differrent classes without using RTTS and casting unless they implement the same interface.
As i started to learn ABAP and had former experience with other object oriented languages i struggled with one missing feature here – unability to overload methods.
What’s a method overloading you might ask?
It’s ability to use the same name of method, but with different arguments passed to the methods and with different implementations.
Without this feature, at some point, classes might become too large and it’s hard to keep track on all methods doing similar activities, but having different names.
Interfaces do not provide overloading abilities, yet they can tidy up and streamline your coding by limiting amount of methods with different names but similar functionalities.
In ABAP we have single class inheritance (so each class can have only one parent) and multiple interface implementation ability.
For example, presented above LCL_Child_Class inherits all not private variables, methods, types and constants from LCL_Parent_Class and must implement all functionalities from LINF_Utility and LINF_Saver interfaces.
To explain what’s an interface i will use “not that professional” description- it’s a class-like entity, but it carries no implementation of declared methods, yet it might carry over constants and types and variables. Interface cannot be initialized.
By default all methods of interface have to be implemented – this is general rule enforced by object-oriented programming creed. It’s possible to make interface method not obligatory to implement, but this will not be described in the scope of this blog post.
“Real life” use case.
Let’s imagine we have a program that needs to feed table SFLIGHT with data from various sources:
- Excel Upload
- RFC Upload
- Upload of modified and inserted lines during program runtime.
Of course we could also add ADBC sources and consumption of JSON/XML soruces fetched via HTTP client object to the list, but i just want to show the gist, not describe all possible examples in details.
As this is just a presentation of possibiilties – i didn’t create real working program start to finish.
Declaration of interfaces:
We are going to create two interfaces, but just one is really necessary in this example.
First one is the most important. I named it linf_sflight_career as this is local interface which is implemented by local classes for excel, rfc and local table carriers.
interface linf_sflight_carrier.
types: tt_sflight type standard table of sflight with default key,
st_sflight type sorted table of sflight with non-unique key mandt carrid connid,
ht_sflight type hashed table of sflight with unique key mandt carrid connid fldate.
methods: "! Returns hashed table SFLIGHT contents
"! @parameter r_sflight |
get_hashed_records returning value(r_sflight) type ht_sflight,
"! Returns sorted table SFLIGHT contents
"! @parameter r_sflight |
get_sorted_records returning value(r_sflight) type st_sflight,
"! Returns standard table SFLIGHT contents
"! @parameter r_sflight |
get_standard_records returning value(r_sflight) type tt_sflight.
endinterface.
Interface carries various table types and three methods to be implemented by the classes of excel, rfc and table carrier.
Next interface will be implemented by class responsible for saving data to the database.
interface linf_sflight_saver.
constants: "! Table lock types
begin of lock_types,
exclusive type enqmode value 'E',
end of lock_types.
constants: "! Scopes for table lock
begin of scope_range,
_2 type char01 value '2',
end of scope_range.
constants: _sflight type tablename value 'SFLIGHT'.
methods: "! Save data from carrier object to SFLIGHT table
"! @parameter i_carrier | Carrier object
save_data importing i_carrier type ref to linf_sflight_carrier.
endinterface.
At this point you might ask – why do we need so many classes to fulfill such simple task? Why so many interfaces, when we could achieve similar effect utilizing class inheritance approach or even using single class for all the purposes.
Answer is obvious – SOLID. If you’d like to know more about this approach then please let me know in the comments and i will create another blog post purely about SOLID.
Returning to the topic – now class presentation:
class lcl_excel_carrier definition.
public section.
interfaces: linf_sflight_carrier.
aliases: tt_sflight for linf_sflight_carrier~tt_sflight,
st_sflight for linf_sflight_carrier~st_sflight,
ht_sflight for linf_sflight_carrier~ht_sflight,
get_hashed_records for linf_sflight_carrier~get_hashed_records,
get_sorted_records for linf_sflight_carrier~get_sorted_records,
get_standard_records for linf_sflight_carrier~get_standard_records.
protected section.
private section.
data: standard_sflight type tt_sflight,
sorted_sflight type st_sflight,
hashed_sflight type ht_sflight.
endclass.
class lcl_excel_carrier implementation.
method get_hashed_records.
r_sflight = hashed_sflight.
endmethod.
method get_sorted_records.
r_sflight = sorted_sflight.
endmethod.
method get_standard_records.
r_sflight = standard_sflight.
endmethod.
endclass.
class lcl_rfc_carrier definition.
public section.
interfaces: linf_sflight_carrier.
aliases: tt_sflight for linf_sflight_carrier~tt_sflight,
st_sflight for linf_sflight_carrier~st_sflight,
ht_sflight for linf_sflight_carrier~ht_sflight,
get_hashed_records for linf_sflight_carrier~get_hashed_records,
get_sorted_records for linf_sflight_carrier~get_sorted_records,
get_standard_records for linf_sflight_carrier~get_standard_records.
protected section.
private section.
data: standard_sflight type tt_sflight,
sorted_sflight type st_sflight,
hashed_sflight type ht_sflight.
endclass.
class lcl_rfc_carrier implementation.
method get_hashed_records.
r_sflight = hashed_sflight.
endmethod.
method get_sorted_records.
r_sflight = sorted_sflight.
endmethod.
method get_standard_records.
r_sflight = standard_sflight.
endmethod.
endclass.
class lcl_table_carrier definition.
public section.
interfaces: linf_sflight_carrier.
aliases: tt_sflight for linf_sflight_carrier~tt_sflight,
st_sflight for linf_sflight_carrier~st_sflight,
ht_sflight for linf_sflight_carrier~ht_sflight,
get_hashed_records for linf_sflight_carrier~get_hashed_records,
get_sorted_records for linf_sflight_carrier~get_sorted_records,
get_standard_records for linf_sflight_carrier~get_standard_records.
protected section.
private section.
data: standard_sflight type tt_sflight,
sorted_sflight type st_sflight,
hashed_sflight type ht_sflight.
endclass.
class lcl_table_carrier implementation.
method get_hashed_records.
r_sflight = hashed_sflight.
endmethod.
method get_sorted_records.
r_sflight = sorted_sflight.
endmethod.
method get_standard_records.
r_sflight = standard_sflight.
endmethod.
endclass.
Classes presented above have same functionality, but fully implemented classes would have some specific methods for each of their carrier purposes (like filtering, retrieving sflight data from raw format excel data et cetera.
All carrier classes implement linf_sflight_carrier – therefore we didn’t have to define methods all over again. I did however add aliases for better code readability
Next class we’re going to create is database saver called lcl_database_saver
class lcl_database_saver definition.
public section.
interfaces: linf_sflight_saver.
aliases: lock_types for linf_sflight_saver~lock_types,
scope_range for linf_sflight_saver~scope_range,
save_data for linf_sflight_saver~save_data,
_sflight for linf_sflight_saver~_sflight.
protected section.
private section.
methods: "! Creates table lock key for database lock
"! @parameter i_sflight_ref | Reference to SFLIGHT table line
"! @parameter r_varkey | Varkey returned
create_varkey importing i_sflight_ref type ref to sflight
returning value(r_varkey) type vim_enqkey,
"! Locks table using passed varkey
"! @parameter i_varkey | Table lock key
"! @parameter i_tabname | Table name
"! @parameter r_subrc | Information on lock creation. 0 = okay
lock_table_line importing i_varkey type vim_enqkey
i_tabname type tablename default _sflight
returning value(r_is_locked) type abap_bool,
"! Unlocks locked table line
"! @parameter i_varkey | Table lock key
"! @parameter i_tabname | Table name
unlock_table_line importing i_varkey type vim_enqkey
i_tabname type tablename default _sflight.
endclass.
class lcl_database_saver implementation.
method save_data.
loop at i_carrier->get_standard_records( ) reference into data(standard_line).
data(varkey) = create_varkey( standard_line ).
if lock_table_line( i_varkey = varkey ).
modify sflight from standard_line->*.
unlock_table_line( exporting i_varkey = varkey ).
endif.
endloop.
endmethod.
method lock_table_line.
call function 'ENQUEUE_E_TABLEE'
exporting
mode_rstable = lock_types-exclusive " Lock mode for table RSTABLE
tabname = i_tabname " 01th enqueue argument
varkey = i_varkey " 02th enqueue argument
_scope = scope_range-_2
exceptions
foreign_lock = 1
system_failure = 2
others = 3.
r_is_locked = xsdbool( sy-subrc = 0 ).
endmethod.
method unlock_table_line.
call function 'DEQUEUE_E_TABLEE'
exporting
mode_rstable = lock_types-exclusive " Lock mode for table RSTABLE
tabname = i_tabname " 01th enqueue argument
varkey = i_varkey " 02th enqueue argument
_scope = scope_range-_2.
endmethod.
method create_varkey.
r_varkey = |{ i_sflight_ref->mandt }{ i_sflight_ref->carrid }{ i_sflight_ref->connid }{ i_sflight_ref->fldate }|.
endmethod.
endclass.
And finally – we run the example:
initialization.
data(excel_carrier) = new lcl_excel_carrier( ).
data(rfc_carrier) = new lcl_rfc_carrier( ).
data(database_saver) = new lcl_database_saver( ).
try.
database_saver->save_data( i_carrier = excel_carrier ).
catch cx_sy_assign_cast_illegal_cast.
catch cx_sy_assign_cast_unknown_type.
catch cx_sy_assign_cast_error.
endtry.
try.
database_saver->save_data( i_carrier = rfc_carrier ).
catch cx_sy_assign_cast_illegal_cast.
catch cx_sy_assign_cast_unknown_type.
catch cx_sy_assign_cast_error.
endtry.
As you can see by moving abstraction to an interface level we can assure that literally ANY class correctly implementing interface linf_sflight_carrier can be passed to saver method and be processed correctly.
Another advantage of this approach is ability to create mockup objects in fast and easy way for your unit tests. Testable code – better code.
That’s all for today – i really hope you liked it 🙂
Cheers,
Cezary
Very nice! The code is easy to understand. That's great because I hate to use hard to read code. The next person looking it after me tends to not like it. (I use it when I have to)
First blog? I hope it is the first of many.
Thank you!
Michelle
Nice blog, thanks for sharing! I dug up some older blogs on the interface subject in case someone is interested in further reading:
OO Programming with ABAP Objects: Interfaces by James Wood (2009, wow!)
Optional interface methods (shortest blog ever 🙂 )
About SOLID - I think Paul Hardy has already been talking our ears off in his blog series. 🙂
I had to summarise what interfaces were the other day, as an abstract concept as opposed to the way they work in ABAP. I came up with the following:-
Note that the most important thing to me is that the interface describes to the outside world what functionality you can expect as the calling program does not know or care what concrete class is actually going to be used to do the work, only that it is able to do the work.
Thus during "injection" the importing parameter is typed as the interface rather than to any type of concrete class. The same deal goes for factory methods. Moreover the ABAP Mocking Framework works off interfaces also.
Just like with the Liskov Substitution program, where any subclass can do the job of its parent without the caller being any the wiser, whatever actual class that implements the interface is chosen, the calling program can call a method and knows the job will be done.
Given that, the idea of optional interface methods seems rather bizarre.
Thank you
This is a great blog, and also some great comments too. My company is in the process of transitioning to agile development, and this kind of decoupling techniques will be 100% essential, so thanks to all for providing such useful material here. It is also important for my colleagues to understand that this is a recognized and supported way of working in the dev community, and this blog will help there. Many thanks.
None of my blog posts here were so detailed, so it's really nice to see the work you've done Cezary. Hope you'll not stop (like me at the moment), as sharing and explaining is this is one of the best ways to learn.
Keep on going!
Wow very well written. It actually gives more clarity to me about the role of interfaces and how important they are. Will be really happy if you can blog about SOLID via ABAP code as an example.
Thank you a lot for the feedback!
It's nice to see that such great people from SAP Community encourage me to keep on my work!
This is really a nice kickstart so i will do my best to keep my blog useful for the greater good - SAP Community 🙂
Yes! finally an example i can understand.....and also thanks to Paul for providing some conceptual context.
Hello Cezary,
I generated those diagrams from your code using PlantUML
JNN
Hi Jacques!
UML for ABAP? Count me in, this is amazing!
Frankly some developers i talked to neglect impact of UML diagrams in ABAP development and - this i leave without a comment - they sometimes neglect keeping technical documentation of their code.
Changing undocumented code is really taking a highway to hell, so it's good to see that somebody cares for keeping documentation and code clear!
Cheers,
Cezary
Hi Cezary,
I have nothing more to add here, other forum members have already echoed my sentiments.
But being the critical person that i am, i must ask if you had considered having a “Factory” to deliver the caller the relevant instance to work with?
BR, Suhas
Suhas thanks a lot for the comment!
To be honest - i did consider using factory here. But i had one issue with that - i tried to explain interface concept as simply as possible.
I believe it's better to present concept by concept and then connect them instead of putting too much abstract concepts at the same time and trying to explain all at once.
At least this approach worked for me - little slices at the time and we're going to eat entire huge steak at ease! 🙂
Cheers,
Cezary
Hello Cezary,
Nice job
Best Regards
By George
Hello Cezary,
Fantastic job!!!!! It's unbieveable that this was your first post. I learned a lot! Since I am a beginner of ABAP OO. I decided to read your blog once more.
Thanks for your sharing.