The dependency inversion principle
In the literature the dependency inversion principle is often presented as a very abstract principle. High level classes must not depend on low level classes is the first phrase, the clean-code-developer initiative used to explain this principle. Especially in the ABAP procedural world, it’s very rare, to find some good use cases for this principle. But in my recent projects, i got a good use case, which helped me understanding the phrase High level classes must not depend on low level classes a lot. The use case was a TCO (total cost of ownership) calculator. The TCO was composed from the purchasing price and some surcharges. Different material types had a different surcharging calculation algorithm.
I started implementing the calculation algorithm with TDD (test-driven-development). My first draft was the following:
CLASS ztco_calculator DEFINITION FINAL. PUBLIC SECTION. TYPES: BEGIN OF tco_and_surcharges, tco TYPE ztco, surcharges_logistics TYPE zsurcharges_logistics, process_costs TYPE zprocess_costs, END OF tco_and_surcharges. METHODS constructor IMPORTING purchase_order_item TYPE ekpo. METHODS calculate_tco_with_surcharges RETURNING VALUE(result) TYPE tco_and_surcharges. PROTECTED SECTION. DATA: purchase_order_item TYPE ekpo. METHODS get_purchasing_price RETURNING VALUE(result) TYPE zpurchase_price. METHODS get_surcharges_logistics RETURNING VALUE(result) TYPE zsurcharges_logistics. METHODS get_process_costs RETURNING VALUE(result) TYPE zprocess_costs. ENDCLASS. CLASS ztco_calculator IMPLEMENTATION. METHOD constructor. me->purchase_order_item = purchase_order_item. ENDMETHOD. METHOD calculate_tco_with_surcharges. result-surcharges_logistics = get_surcharges_logistics( ). result-process_costs = get_process_costs( ). result-tco = get_purchasing_price( ) + result-surcharges_logistics + result-process_costs. ENDMETHOD. ENDCLASS.
The second draft was test-friendlier. After moving the methods
get_process_costs (process-costs were some kind of surcharges) in an interface, i could stub them in the unit-test.
INTERFACE ztco_surcharge_calculator. METHODS get_surcharges_logistics RETURNING VALUE(result) TYPE zsurcharges_logistics. METHODS get_process_costs RETURNING VALUE(result) TYPE zprocess_costs. ENDINTERFACE. CLASS ztco_calculator DEFINITION FINAL. PUBLIC SECTION. METHODS constructor IMPORTING surcharge_calculator TYPE REF TO ztco_surcharge_calculator purchase_order_item TYPE ekpo. METHODS calculate_tco_with_surcharges RETURNING VALUE(result) TYPE tco_and_surcharges. PROTECTED SECTION. DATA: surcharge_calculator TYPE REF TO ztco_surcharge_calculator, purchase_order_item TYPE ekpo. METHODS get_purchasing_price RETURNING VALUE(result) TYPE zpurchase_price. ENDCLASS. CLASS ztco_calculator IMPLEMENTATION. METHOD constructor. me->surcharge_calculator = surcharge_calculator. me->purchase_order_item = purchase_order_item. ENDMETHOD. METHOD calculate_tco_with_surcharges. result-surcharges_logistics = surcharge_calculator->get_surcharges_logistics( ). result-process_costs = surcharge_calculator->get_process_costs( ). result-tco = get_purchasing_price( ) + result-surcharges_logistics + result-process_costs. ENDMETHOD. ENDCLASS.
Assemble the whole thing
In the first step the different surcharging calculation algorithms must be created. They must all implement the interface
ztco_surcharge_calculator. We had the following three classes:
But we don’t use them in the main-class
ztco_calculator. Instead of we just used the interface
ztco_surcharge_calculator. This works with a technique called polymorphism.
In the second step a interface reference variable for the material type was created and instantiated.
After the second step, the main-class
ztco_calculator could be created.
DATA: sucharge_calculator TYPE REF TO ztco_surcharge_calculator, material_type TYPE mtart. SELECT SINGLE mtart INTO material_type FROM mara WHERE matnr = purchase_order_item-matnr. CASE material_type. WHEN 'EOIL'. " engine oil surcharge_calculator = NEW ztco_surcharges_engine_oil( ). WHEN 'BEAR'. " ball bearing surcharge_calculator = NEW ztco_surcharges_ball_bearing( ). OTHERS. surcharge_calculator = NEW ztco_surcharges_default( ). ENDCASE. DATA(tco_calculator) = NEW ztco_calculator( surcharge_calculator = surcharge_calculator purchase_order_item = purchase_order_item ). cl_demo_output=>display( tco_calculator->calculate_tco_with_surcharges( ) ).
To make it more configurable, we could even create a customizing-table, which links the material type and the surcharging calculation algorithm. The interface reference variable can then be created dynamic (Idea from Gábor Márián).
Do you realize how the dependency between the surcharging calculation algorithms and the TCO calculation was minimized in the second draft? In the first draft, the different surcharging calculation algorithms must be squeezed in two methods. These would cause at least the anti-pattern code-duplication (CASE-condition).
In the second draft the different surcharging calculation algorithms are separated. The main-class
ztco_calculator only needs two interface methods.
This draft adds flexibility for enhancements or revisions, the main goal of the dependency inversion principle.
You make me remember in the middle of learning "things" HANA. I have a lot to do to brush up my other skills as well. Balance learning more about classes with learning more about CDS with learning more about BRF+ with learning more about.... OK too much - my head may split into 1000 pieces. And that is what makes this blog so valuable to me.
This has taken a difficult topic and explained it nicely. Exactly what I need. TDD? Still not sure about that. I know I'll get there at some point.
I like the result. But how about making the surcharge calculator configurable, so no source code modification is required when a new algorithm is introduced?
Good idea, i didn’t thought of it before. I added it to the blog-post.
Hi Johannes, thanks that you wrote a blog about this topic. I've been working on my understanding of the topic for some time 🙂
Eventually people may have a quick look at chapter Testing > Injection of ABAP Clean Code.
Yeah, this is good information with a concrete example. Based on my understanding of dependency inversion principle (aka dip), it shows the power of combining abstraction and polymorphism together to achieve a code that is less rigid and fragile. Using Interfaces/Abstract classes and their implementations in sub classes to vary the behavior (i.e. polymorphism) to make your code less susceptible to future change is a good definition of dip. There is also a good documentation on dip.
Thank you for the blog.
All the best,