Dependency Injection
There is many a true word, Spoken Inject
One line summary:-
One way to write OO programs with many small classes with less lines of code.
Back Story
The other day there was a blog on SCN about Dependency injection.
http://scn.sap.com/community/abap/blog/2014/01/06/successful-abap-dependency-injection
I thought – I know what that is – if an object (say a car object) needs an engine object to work, you don’t have the car object create the engine object, you pass in the engine object through the car objects constructor.
I had thought that was solely concerned with unit testing, but if you look at the comments at the bottom, when I started talking about this on the blog comments, people soon put me right, it turns out it has a much wider scope.
As soon as I realised I was barking up the wrong tree, I read all I could on the subject, for example …
http://en.wikipedia.org/wiki/Dependency_injection
http://martinfowler.com/articles/injection.html
http://www.jamesshore.com/Blog/Dependency-Injection-Demystified.html
… ending with this blog by Jack Stewart
http://scn.sap.com/community/abap/blog/2013/08/28/dependency-injection-for-abap
I always thought the idea was great – often you have to create a bunch of objects and then “wire them together” by passing them into each other’s constructors so they know about each other.
This gives you the flexibility to pass in subclasses to alter the behaviour of the application – as I said I had first heard about this in the context of unit testing, but when I thought about it again naturally you can pass in any sort of subclass to change the way the program runs e.g. different subclass based on whatever criteria makes sense, just like the BADI filter mechanism.
That is a wonderful thing to be able to do, and subclassing is one of the few benefits of OO programming that one of my colleagues can get his head around, but it does tend to involve a lot of “boiler plate” programming i.e. lots of CREATE OBJECT statements, passing in assorted parameters.
Many Small Classes, make Light Work
http://scn.sap.com/community/abap/blog/2013/08/22/the-issue-with-having-many-small-classes
The idea is that the smaller and more focused your classes are, the easier they are to re-use and maintain. An OO principle is that a class should only have one reason to change i.e. it should do one thing only. If you follow that principle you get loads of benefits, but you have to create loads of classes in your program.
When I first started playing around with OO programming I was too lazy to keep writing CREATE OBJECT so I made everything static. That is not actually a sensible thing to do just to avoid work, as then you can’t subclass things. SAP itself found that out when they initially made ABAP proxy classes static.
The NEW Objects on the Block
In the Java programming language you create objects by saying GOAT = NEW GOAT as opposed to CREATE OBJECT GOAT.
In the “Head First Design Patterns Book” it gives a bunch of about five rules of programming which every Java programmer should aspire to but are in fact impossible to follow in real life.
One of those revolved around the rule being never to use the NEW statement because that hard coded the exact type of class you were creating, but how can you create objects if the only way to create them is to use the NEW statement?
In both Java and ABAP interfaces come into play here, you declare the ANIMAL object as an interface of type FARM ANIMAL (which GOAT implements) and say CREATE OBJECT ANIMAL TYPE GOAT. Perhaps a better example is in ABAP2XLS when you declare the object that writes out the file as an interface and then create it using the TYPE of the EXCEL version you want e.g. 2007.
Now you are always going to have to say the specific type (subclass) you want somewhere, but is it possible to decouple this from the exact instant you call the CREATE OBJECT statement?
Since you can have a dynamic CREATE OBJECT statement, you would think so, but how does this apparent diversion link back to what I was talking about earlier?
Jack Black and his blog Silver
Going back to Dependency Injection the blog by Jack Stewart contained a link to download some sample code. I downloaded it, had a look, thought it was great, and then totally re-wrote it. That is no reflection on the quality of the original; I am just physically incapable of not rewriting every single thing I come across.
I am going to include a SAPLINK file in text format at the end of this blog, but first I shall go through the code, top down. Firstly, this test program shows exactly what I am trying to achieve i.e. the same thing in less lines of code.
I have created some dummy Y classes which just have constructors to pass in a mixture of object instances and elementary data object parameters, my dear Watson. They only have one method each, just to write out if they are a base class or a subclass. The important thing is the effort involved to create them.
The Da Vinci Code Samples
First of all, a basic structure to get some elementary parameters and say if we want to use a test double or not. I am sticking with the unit test concept for now, but as I mentioned, you can pass in any old subclass you want, according to the good old, every popular, Liskov Substitution principle.
*&---------------------------------------------------------------------*
*& Report Y_INJECTION_TEST
*&
*&---------------------------------------------------------------------*
* Show two ways to create linked objects, one using dependency injection
*--------------------------------------------------------------------*
REPORT y_injection_test.
PARAMETERS : p_valid TYPE sy-datum,
p_werks TYPE werks_d,
p_test AS CHECKBOX.
INITIALIZATION.
p_valid = sy-datum.
p_werks = '3116'.
START-OF-SELECTION.
PERFORM do_it_the_long_way.
PERFORM do_it_the_short_way.
It’s a Long Long Way, from there to here
Firstly, the traditional way….
*&---------------------------------------------------------------------*
*& Form DO_IT_THE_LONG_WAY
*&---------------------------------------------------------------------*
* Normal way of doing things
*----------------------------------------------------------------------*
FORM do_it_the_long_way .
DATA: lo_logger TYPE REF TO ycl_test_logger.
DATA: lo_db_layer TYPE REF TO ycl_test_db_layer.
DATA: lo_mock_db_layer TYPE REF TO ycl_test_mock_db_layer.
DATA: lo_simulator TYPE REF TO ycl_test_simulator.
CREATE OBJECT lo_logger.
IF p_test = abap_true.
CREATE OBJECT lo_mock_db_layer
EXPORTING
io_logger = lo_logger
id_valid_on = p_valid.
CREATE OBJECT lo_simulator
EXPORTING
id_plant_id = p_werks
io_db_layer = lo_mock_db_layer
io_logger = lo_logger.
ELSE.
CREATE OBJECT lo_db_layer
EXPORTING
io_logger = lo_logger
id_valid_on = p_valid.
CREATE OBJECT lo_simulator
EXPORTING
id_plant_id = p_werks
io_db_layer = lo_db_layer
io_logger = lo_logger.
ENDIF.
lo_simulator->say_who_you_are( ).
SKIP.
ENDFORM. " DO_IT_THE_LONG_WAY
Get Shorty
Now we do the same thing, using a Z class I created to use dependency injection.
*&---------------------------------------------------------------------*
*& Form DO_IT_THE_SHORT_WAY
*&---------------------------------------------------------------------*
* Using Constructor Injection
*----------------------------------------------------------------------*
FORM do_it_the_short_way .
* Local Variables
DATA: lo_simulator TYPE REF TO ycl_test_simulator.
zcl_bc_injector=>during_construction( :
for_parameter = 'ID_PLANT_ID' use_value = p_werks ),
for_parameter = 'ID_VALID_ON' use_value = p_valid ).
IF p_test = abap_true.
"We want to use a test double for the database object
zcl_bc_injector=>instead_of( using_main_class = 'YCL_TEST_DB_LAYER'
use_sub_class = 'YCL_TEST_MOCK_DB_LAYER' ).
ENDIF.
zcl_bc_injector=>create_via_injection( CHANGING co_object = lo_simulator ).
lo_simulator->say_who_you_are( ).
ENDFORM. " DO_IT_THE_SHORT_WAY
I think the advantage is self-evident – the second way is much shorter, and it’s got Big Feet.
If the importing parameter of the objectconstructor was an interface it would not matter at all. You just pass the interface name in to the INSTEAD_OF method as opposed to the main class name.
I have done virtually no error handling in the below code, except throwing fatal exceptions when unexpected things occur. This could be a lot more elegant, I am just demonstrating the basic principle.
Firstly the DURING CONSTRUCTION method analyses elementary parameters and then does nothing more fancy than adding entries to an internal table.
* Local Variables
DATA: lo_description TYPE REF TO cl_abap_typedescr,
ld_dummy TYPE string ##needed,
ld_data_element_name TYPE string,
ls_parameter_values LIKE LINE OF mt_parameter_values.
ls_parameter_values-identifier = for_parameter.
CREATE DATA ls_parameter_values-do_value LIKE use_value.
GET REFERENCE OF use_value INTO ls_parameter_values-do_value.
CHECK sy-subrc = 0.
CALL METHOD cl_abap_structdescr=>describe_by_data_ref
EXPORTING
p_data_ref = ls_parameter_values-do_value
RECEIVING
p_descr_ref = lo_description
EXCEPTIONS
reference_is_initial = 1
OTHERS = 2.
IF sy-subrc <> 0.
RETURN.
ENDIF.
SPLIT lo_description->absolute_name AT '=' INTO ld_dummy ld_data_element_name.
ls_parameter_values-rollname = ld_data_element_name.
INSERT ls_parameter_values INTO TABLE mt_parameter_values.
It’s the same deal with the INSTEAD_OF method for saying what exact subclass you want to create, except it’s even simpler this time.
METHOD instead_of.
* Local Variables
DATA: ls_sub_classes_to_use LIKE LINE OF mt_sub_classes_to_use.
ls_sub_classes_to_use-main_class = using_main_class.
ls_sub_classes_to_use-sub_class = use_sub_class.
"Add entry at the start, so it takes priority over previous
"similar entries
INSERT ls_sub_classes_to_use INTO mt_sub_classes_to_use INDEX 1.
ENDMETHOD.
Now we come to the main CREATE_BY_INJECTION method. I like to think I have written this as close to plain English as I can, so that this is more or less elf-explanatory.
METHOD create_via_injection.
* Local Variables
DATA: lo_class_in_reference_details TYPE REF TO cl_abap_refdescr,
lo_class_in_type_details TYPE REF TO cl_abap_typedescr,
lo_class_to_create_type_detail TYPE REF TO cl_abap_typedescr,
ld_class_passed_in TYPE seoclass-clsname,
ld_class_type_to_create TYPE seoclass-clsname,
ls_created_objects LIKE LINE OF mt_created_objects,
lt_signature_values TYPE abap_parmbind_tab.
* Determine the class type of the reference object that was passed in
lo_class_in_reference_details ?= cl_abap_refdescr=>describe_by_data( co_object ).
lo_class_in_type_details = lo_class_in_reference_details->get_referenced_type( ).
ld_class_passed_in = lo_class_in_type_details->get_relative_name( ).
"See if we need to create the real class, or a subclass
determine_class_to_create(
EXPORTING
id_class_passed_in = ld_class_passed_in
io_class_in_type_details = lo_class_in_type_details
IMPORTING
ed_class_type_to_create = ld_class_type_to_create
eo_class_to_create_type_detail = lo_class_to_create_type_detail ).
READ TABLE mt_created_objects INTO ls_created_objects WITH TABLE KEY clsname = ld_class_type_to_create.
IF sy-subrc = 0.
"We already have an instance of this class we can use
co_object ?= ls_created_objects-object.
RETURN.
ENDIF.
"See if the object we want to create has parameters, and if so, fill them up
fill_constructor_parameters( EXPORTING io_class_to_create_type_detail = lo_class_to_create_type_detail
IMPORTING et_signature_values = lt_signature_values ).
create_parameter_object( EXPORTING id_class_type_to_create = ld_class_type_to_create
it_signature_values = lt_signature_values " Parameter Values
CHANGING co_object = co_object ). " Created Object
ENDMETHOD.
There is not a lot of point in drilling into this any further – I would encourage you to download the SAPLINK file, and then run this in debug mode to see what is happening.
In summary, I am always on the lookout for ways to reduce the so called “boiler plate” code, so the remaining code can concentrate on what the application is supposed to be doing as opposed to how it is doing it. This dependency injection business seems ideally suited so this purpose.
Now, while I am here.
Did I mention I am giving a speech at the “Mastering SAP Technology 2014” conference at Melbourne on the 31/03/2014 – it’s about unit testing of ABAP programs.
What’s that? I’ve already mentioned this? Many times?
Oh dear, that must have slipped my mind. In that case I won’t go on about it, and I’ll sign off.
Cheersy Cheers
Paul
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
5 | |
5 | |
3 | |
2 | |
2 | |
2 | |
1 | |
1 | |
1 | |
1 |