This week, I faced a situation where I needed to implement a new business object for one of our applications. As business objects usually need persistence, I created a new repository for it to handle the database access. This repository covered the standard database functions like Create, Update, Read and Delete (CRUD).
While implementing the repository, the question of what should happen to new business objects during the Create-operation came up, specifically who should create a new document number. I decided to request new number ranges for new business objects while in the SAVE-Method of the repository and then the trouble started to grow…
The first design was a mess:
In this fictitious example, ZCL_BOOKING represents an entity, a booking record, which will be saved to the database.
ZCL_BOOKING_REPOSITORY takes care of saving this entity correctly, with some more functions like retrieving those entities back from database or delete them from the DB.
The SAVE-Method looked like this:
CLASS ZCL_BOOKINGS_REPOSITORY IMPLEMENTATION. ... METHOD ZIF_BOOKING_REPOSITORY~save. DATA lv_bookid TYPE S_BOOK_ID. DATA ls_header TYPE bookings. lv_bookid = io_booking->get_bookid( ). ls_header = io_booking->get_booking( ). IF lv_bookid IS INITIAL. CALL FUNCTION 'NUMBER_GET_NEXT' EXPORTING nr_range_nr = 'ZFL_BOOKID' object = '01' IMPORTING number = rv_bookid EXCEPTIONS interval_not_found = 1 number_range_not_intern = 2 object_not_found = 3 quantity_is_0 = 4 quantity_is_not_1 = 5 interval_overflow = 6 buffer_overflow = 7 OTHERS = 8. * implement proper error handling here... io_booking->set_bookid( lv_bookid ). CALL FUNCTION 'Z_FM_INSERT_BOOKINGS' IN UPDATE TASK EXPORTING iv_bookid = lv_bookid is_header = ls_header. ELSE. CALL FUNCTION 'Z_FM_UPDATE_BOOKINGS' IN UPDATE TASK EXPORTING iv_bookid = lv_bookid is_header = ls_header. ENDIF. ENDMETHOD. ... ENDCLASS.
Running out of time? I’M RUNNING OUT OF NUMBERS!!!
Everything went well, until I started to write a unit test for the SAVE-method.
I started to write a test which created a new instance of type ZCL_BOOKING without any BOOKID. I expected the instance to be inserted to the database. This worked pretty well, but when I tried to implement the TEARDOWN method I had some issues. As Unit tests need to be processed as often as you need, and should leave the system in the same state from where you started from. This means, the inserted record needed to be deleted.
As the SAVE-method requests a new number for each object that has not yet an ID, I don’t really know which ID has just been inserted.
I could have asked the instance of type ZCL_BOOKING which ID has been set for it. This would solve at least the issue that I need to clean up the database after the test-insert.
But, more severe, the current number in the number range interval has increased by one with each unit test. This was not acceptable.
So the unit test revealed the bad design: In fact, the repository had a dependency on a number range object. Actually, it should not care about it.
This step introduces a new class to the design, which is called ZCL_NUMBER_RANGE_REQUEST. It implements an interface ZIF_NUMBER_RANGE_REQUEST which is now used by ZCL_BOOKING_REPOSITORY to handle its number range requests.
The number range object is created before ZCL_BOOKING_REPOSITORY is getting created in order to hand it over to the constructor of the repository.
The result is: Instead of creating new document numbers by its own, the repository asks another object for it.
This has a huge benefit: As the number range object is specified by an interface, we can fake this interface in a unit test and pass it to the repository’s constructor. The fake object of course does not request a real number from a real number range but returns “1” all the time.
So that’s what the new implementation of the classes looks like:
CLASS ZCL_BOOKING_REPOSITORY IMPLEMENTATION. ... METHOD ZIF_BOOKING_REPOSITORY~save. DATA lv_bookid TYPE S_BOOK_ID. DATA ls_header TYPE bookings. lv_bookid = io_booking->get_bookid( ). ls_header = io_booking->get_booking( ). IF lv_bookid IS INITIAL. lv_bookid = mo_number_range->get_next_number( ). io_booking->set_bookid( lv_bookid ). CALL FUNCTION 'Z_FM_INSERT_BOOKINGS' IN UPDATE TASK EXPORTING iv_bookid = lv_bookid is_header = ls_header. ELSE. CALL FUNCTION 'Z_FM_UPDATE_BOOKINGS' IN UPDATE TASK EXPORTING iv_bookid = lv_bookid is_header = ls_header. ENDIF. ENDMETHOD. METHOD constructor. mo_number_range = io_number_range. ENDMETHOD. ENDCLASS.
The implementation of the number range object requests the current number by using the standard function module. The input paramaters for this function module have been provided in the CONSTRUCTOR of the object.
CLASS ZCL_NUMBER_RANGE_REQUEST IMPLEMENTATION. ... method GET_NEXT_NUMBER. CALL FUNCTION 'NUMBER_GET_NEXT' EXPORTING nr_range_nr = mv_nrobj object = mv_nrnr IMPORTING number = rv_number EXCEPTIONS interval_not_found = 1 number_range_not_intern = 2 object_not_found = 3 quantity_is_0 = 4 quantity_is_not_1 = 5 interval_overflow = 6 buffer_overflow = 7 OTHERS = 8. * implement proper error handling here... endmethod. ENDCLASS.
How can I really get a fake?
Fake objects can be created using local classes in the unit test. As an alternative, mocking Frameworks can help to automate this task by providing a declarative API.
In the real life use case I created the fake object using a mocking framework with this call:
mo_number_range_request ?= /leos/cl_mocker=>/leos/if_mocker~mock( ‘/leosb/if_number_range_request’)->method( 'GET_NEXT_NUMBER' )->returns( 1 )->generate_mockup( ).
I hate puzzles
This kind of architecture eventually leads to tons of classes, each having their own responsibility. In a real life application you would need to set up the repository instances and their dependencies at least in some specific method at startup:
CREATE OBJECT lo_number_range TYPE ZCL_NUMBER_RANGE_REQUEST CREATE OBJECT lo_repository TYPE ZCL_BOOKING_REPOSITORY EXPORTING io_number_range = lo_number_range.
IoC Containers help you in managing these dependencies by allowing you to register specific classes for each interface in customizing. Their purpose is to resolve all dependencies for a root object, when a root object like a repository is requested by the application. The container creates this root object and hands it back to the caller with just one single line of code.