In the previous blog of this series, we saw the benefits of adopting to a modern development paradigm mainly using functional programming which leveraged some of the powerful features that the ABAP language offers, like the Resumable Exceptions.
In this blog, we will continue where we left off at Part 1, with a focus on the concept of Separation of Concern (SoC). As our original list of business requirement grows, we will handle them with modern ABAP mainly using functional programming as well as understand and apply the principle of Separation of Concern (SoC), mainly by using Encapsulation.
What is a Separation of Concern (SoC)?
As the name suggests, it is a clear separation of concerns between the different objects in an application. “It is a design principle for separating a computer program into distinct sections such that each section addresses a separate concern” – Wikipedia.
– But why do we need SoC in our developments?
Simply to manage the different objects in a much better way within a single application i.e. to make sure each object only does what it is supposed to do and nothing more (or nothing less) and also to keep a clear separation between them i.e. their own business logic. This principle allows developers freedom not only to manage those objects better but also to simplify the build that makes it much more maintainable within the application lifecycle. Let me provide some examples on the concept of SoC:
– Real World example on the concept of SoC
Imagine a number of Rooms on an empty block. Each room has four walls, a roof and a door and there are people living in them. People in each room do not know how the other room is (messy or organised, clean or dirty, its dimensions, the furniture or anything about the other people living in them). The only way to know anything about any other room is to enter through its door. To put it in the context, the rooms are the Objects, the furniture, its dimensions, the people living in it etc. are its Attributes, the fact that people in each room manages their own business (room) is the core concept of a SoC and the door is the Interface via which the rooms communicate with each other to exchange information.
– Example of SoC in an Application
Say an application has two objects; an Employee object and an User object. An Employee is employed by the organisation whereas an User is an (system) User ID to be able to login to a particular system. It’s very easy to mix them up and we often see (even senior) developers will fall into the trap of building a single object and within it will process the user data as well as the employee data. That is a direct violation of the SoC principle – it may look harmless in the beginning but it is potentially expensive within the application lifecycle as we will see later in this blog series.
Smart developers, of course, will have these two (data provider) objects (concerns) separated hence applying the SoC principle which we will explore soon in this blog.
Business Requirement (extended)
The business requirement is more or less the same as in the previous blog except some system User data is additionally requested in the same report. Assume that the source of the Employee data is database Z1 and source of the User data is database Z2 in this system.
The solution architect has also pointed out to the developers that he would like the implementation to adhere to the SoC principle for ease of future extensibility and maintenance and therefore would like it to be well encapsulated.
Our smart developer, Emily, puts some effort in thinking in order to conceptualise the design prior to implementation while our boring (and lazy) developer, Bob, does not think; he quickly starts implementing it without any design.
The first thing that Emily thinks of is that a separation of business logic is necessary to achieve the SoC and perhaps she could separate the Employee object, the User object and finally the caller/consumer i.e. the report itself could be separated as well to establish a clear separation between the different objects in play.
Emily quickly comes up with a design idea of having an (common) Interface between the Employee and the User objects in order to exchange data between them. Emily is sure that the Interface will be useful when another master data object comes into the scope in the future; it would perhaps be worth the time in building it now as it has a strong potential for reuse as the number of applications continue to grow within the system landscape.
Implementation (with SoC)
Emily designs the Interface with two method definition; SET_DATA( ) with the idea that the implementing Class will set the data with its business logic to its own Private Section and GET_DATA( ) which will return this data to any consumer (calling program) in a generic way. She implements it as a Global Interface.
INTERFACE zif_masterdata. METHODS set_data RETURNING VALUE(masterdata) TYPE REF TO zif_masterdata RAISING RESUMABLE(cx_no_data_found). METHODS get_data EXPORTING result TYPE any RETURNING VALUE(masterdata) TYPE REF TO zif_masterdata. ENDINTERFACE.
She adds the Interface Usage in the (data) provider classes.
CLASS lcl_employee DEFINITION FINAL CREATE PUBLIC. " Interface Usage added PUBLIC SECTION. INTERFACES zif_masterdata.
The implementation of the Employee object is more or less the same (as in the Part 1 of this blog) except she now implements the two new methods of the above Interface. As a result of the Interface usage, the Public Method GET_DATA in the previous version has become redundant; Emily deletes it.
CLASS lcl_employee IMPLEMENTATION. METHOD zif_masterdata~set_data. me->empl_data = VALUE #( empid = me->get_emp_id( ) emptyp = me->get_emptyp( ) salary = me->get_salary( ) phone = me->get_phone( ) ). masterdata = me. ENDMETHOD. METHOD zif_masterdata~get_data. result = me->empl_data. masterdata = me. ENDMETHOD. ENDCLASS.
Emily then implements the User object using the same Interface:
CLASS lcl_user DEFINITION FINAL CREATE PUBLIC. PUBLIC SECTION. INTERFACES zif_masterdata. TYPES: BEGIN OF ty_data, userid TYPE string, role TYPE string, END OF ty_data. METHODS constructor IMPORTING i_empid TYPE int4. PRIVATE SECTION. DATA emp_id TYPE int4. DATA user_data TYPE ty_data. ENDCLASS. CLASS lcl_user IMPLEMENTATION. METHOD constructor. me->emp_id = i_empid. ENDMETHOD. METHOD zif_masterdata~set_data. me->user_data = VALUE #( userid = SWITCH #( me->emp_id WHEN 1 THEN |USER001| WHEN 2 THEN |USER002| WHEN 3 THEN |USER003| WHEN 4 THEN |USER004| WHEN 5 THEN |USER005| ELSE THROW RESUMABLE cx_no_data_found( rel_proc_id = CONV #( me->emp_id ) ) ) role = SWITCH #( me->emp_id WHEN 1 THEN |Business Analyst| WHEN 2 THEN |Configurer| WHEN 3 THEN |Developer| "WHEN 4 THEN |User| WHEN 5 THEN |Tester| ELSE THROW RESUMABLE cx_no_data_found( rel_proc_id = CONV #( me->emp_id ) ) ) ). masterdata = me. ENDMETHOD. METHOD zif_masterdata~get_data. result = me->user_data. masterdata = me. ENDMETHOD. ENDCLASS.
She defines the Consumer (caller) object as a Global Class without marking it as FINAL so it can be reused (Inherited) in the future if required – for this reason she moves some of the Private Attributes to the Protected Section (we will find out why in the next/concluding blog of this series).
CLASS zcl_report DEFINITION PUBLIC CREATE PUBLIC. PUBLIC SECTION. INTERFACES if_oo_adt_classrun. TYPES: BEGIN OF extract, empid TYPE int4, "Employee ID emptyp TYPE string, "Org Assignment data salary TYPE decfloat16, "Pay data phone TYPE numc10, "Communication data userid TYPE string, "System User ID role TYPE string, "System Role END OF extract, tty_extract TYPE SORTED TABLE OF extract WITH UNIQUE KEY empid. CLASS-METHODS create IMPORTING i_all_instances_t TYPE int4_table RETURNING VALUE(report) TYPE REF TO zcl_report. METHODS process RETURNING VALUE(report) TYPE REF TO zcl_report. PROTECTED SECTION. DATA all_instances_t TYPE int4_table. DATA error_t TYPE string_table. METHODS get_data RETURNING VALUE(report) TYPE REF TO zcl_report. METHODS get_errors RETURNING VALUE(rt_result) TYPE string_table. PRIVATE SECTION. DATA extract_t TYPE tty_extract. ENDCLASS.
She then proceeds to implement the Global Class.
CLASS zcl_report IMPLEMENTATION. METHOD create. report = NEW zcl_report( ). report->all_instances_t = i_all_instances_t. ENDMETHOD. METHOD process. DATA empl_data TYPE lcl_employee=>ty_data. DATA user_data TYPE lcl_user=>ty_data. LOOP AT me->all_instances_t ASSIGNING FIELD-SYMBOL(<instance>). TRY. me->extract_t = VALUE #( LET o_employee = NEW lcl_employee( employee )->zif_masterdata~set_data( ) if_empl = o_employee->get_data( IMPORTING result = empl_data ) o_user = NEW lcl_user( employee )->zif_masterdata~set_data( ) if_user = o_user->get_data( IMPORTING result = user_data ) IN BASE me->extract_t ( CORRESPONDING #( CORRESPONDING extract( BASE ( CORRESPONDING extract( empl_data ) ) user_data ) ) ) ). CATCH BEFORE UNWIND cx_no_data_found INTO DATA(lx_no_data_found). IF lx_no_data_found->is_resumable = abap_true. RESUME. ELSE. me->error_t = VALUE #( BASE me->error_t ( lx_no_data_found->get_text( ) ) ). ENDIF. ENDTRY. ENDLOOP. report = me. ENDMETHOD. METHOD get_errors. rt_result = me->error_t. ENDMETHOD. METHOD get_data. report = me. ENDMETHOD. METHOD if_oo_adt_classrun~main. DATA(lo_report) = zcl_report=>create( VALUE #( ( 1 ) ( 2 ) ( 3 ) ( 4 ) ( 5 ) ) )->process( ). out->begin_section( |DATA| )->write( lo_report->get_data( )->extract_t )->begin_section( |ERRORS| )->write( lo_report->get_errors( ) ). ENDMETHOD. ENDCLASS.
Implementation (without SoC)
Bob avoids creating so many objects, a new interface and so on as he believes its “too much” to do to achieve such a simple requirement; he does not understand why and what all this fuss is about!
He however liked the way Emily had implemented the Resumable Exceptions (in Part 1 of this blog series) so he decides to follow her design and proceeds to implement the additional requirements by adding two new methods to the existing class which will provide for the system user data of the employee.
CLASS lcl_employee DEFINITION FINAL CREATE PUBLIC. PUBLIC SECTION. * He extends the data structure by adding two new fields TYPES: BEGIN OF empl_data, empid TYPE int4, "Employee ID emptyp TYPE string, "Org Assignment data salary TYPE decfloat16, "Pay data phone TYPE numc10, "Communication data userid TYPE string, "System User ID role type string, "System Role END OF empl_data, empl_data_t TYPE SORTED TABLE OF empl_data WITH UNIQUE KEY empid. PRIVATE SECTION. * And adds two new methods for the additional requirement METHODS get_userid RETURNING VALUE(r_result) TYPE string RAISING RESUMABLE(cx_no_data_found). METHODS get_role RETURNING VALUE(r_result) TYPE string RAISING RESUMABLE(cx_no_data_found). ENDCLASS. CLASS lcl_employee IMPLEMENTATION. METHOD get_userid. r_result = SWITCH #( me->get_emp_id( ) WHEN 1 THEN |USER001| WHEN 2 THEN |USER002| WHEN 3 THEN |USER003| WHEN 4 THEN |USER004| WHEN 5 THEN |USER005| ELSE THROW RESUMABLE cx_no_data_found( rel_proc_id = CONV #( me->emp_id ) ) ). ENDMETHOD. METHOD get_role. r_result = SWITCH #( me->get_emp_id( ) WHEN 1 THEN |Business Analyst| WHEN 2 THEN |Configurer| WHEN 3 THEN |Developer| "WHEN 4 THEN |User| WHEN 5 THEN |Tester| ELSE THROW RESUMABLE cx_no_data_found( rel_proc_id = CONV #( me->emp_id ) ) ). ENDMETHOD. ENDCLASS.
He then extends the GET_DATA( ) method to bring in the two new fields.
METHOD get_data. rs_result = VALUE #( empid = me->get_emp_id( ) emptyp = me->get_emptyp( ) salary = me->get_salary( ) phone = me->get_phone( ) userid = me->get_userid( ) role = me->get_role( ) ). ENDMETHOD.
And finally he reviews the calling program – he is very happy to see that no changes are required here.
DATA extract_t TYPE lcl_employee=>empl_data_t. DATA error_t TYPE string_table. START-OF-SELECTION. DATA(all_employees_t) = VALUE int4_table( ( 1 ) ( 2 ) ( 3 ) ( 4 ) ( 5 ) ). LOOP AT all_employees_t REFERENCE INTO DATA(dref). TRY. INSERT NEW lcl_employee( dref->* )->get_data( ) INTO TABLE extract_t. CATCH BEFORE UNWIND cx_no_data_found INTO DATA(lx_no_data). IF lx_no_data->is_resumable = abap_true. RESUME. ELSE. error_t = VALUE #( BASE error_t ( lx_no_data->get_text( ) ) ). ENDIF. ENDTRY. ENDLOOP. cl_demo_output=>new( )->write( extract_t )->write( error_t )->display( ).
To be concluded…
We understood the object oriented principle of Separation of Concern (SoC) and demonstrated its use (or not) within our applications noticing the differences in their technical designs and implementations.
For the SoC implementation, we simplified its design by introducing an Interface between the multiple objects at play in our application and finally we used encapsulation to achieve the exchange of data between the different objects (concerns) with the help of a global Interface.
So far both Emily and Bob have achieved the same results as far as the business requirement is concerned although their design and implementation are quite different to each other.
In the final blog of this series, we will find out the benefits of Emily’s approach and the disadvantages (cost) of Bob’s and understand why is it so when a similar but a new business requirement comes into the scope.