State Machine Part 3 : Team Procedural vs. Team OO Take 2
Table of Contents
Introduction
Pretend new user requirements
Change the OO Program
Change the Procedural Program
Verdict
Epilogue – Functional Programming
This is going to be a long, long blog…
Most of it will be ABAP code though, which makes it seem longer than it actually is… I also have the horrible feeling the formatting will vanish the instant I publish it. It look OK on the screen I am looking at, but it is always a bit of a roulette wheel how it looks once published, it certainly is not a WYSIWYG situation. I think all the gaps between the paragraphs will vanish.
The Story so Far….
A long long time ag, in a galaxy far, far away, Sumanth Kristam wrote the following blog:-
http://scn.sap.com/community/abap/blog/2014/01/09/classical-way-to-abap-oo-style-of-coding
and instantly the whole SAP programming world were at each other’s throats in a life and death struggle between procedural programing and object orientated programming.
I thought I would enter the fray, and as everyone was screaming out for examples I thought as an experiment I would write two versions of the same program, one in OO, one in a procedural style, and then see how easy it was to change them.
The OO version lives in this blog:-
http://scn.sap.com/community/abap/blog/2014/01/15/enemy-of-the-state
And the procedural one in this:-
http://scn.sap.com/community/abap/blog/2014/01/29/team-procedural-vs-team-oo
The both have SAPLINK nuggets so you can see the relevant code.
When I left you I was half way through - I can’t leave you hanging so I have to finish this off, and then I most likely won’t write many more blogs this year as I’ve got a book to write for SAP Press (I still can’t believe this is happening to me) - anyway the two programs were both fairly simple and did the exact same thing and thus far there is not much to choose between them, the procedural version is shorter and looks more straightforward (to me) and the OO one is following all the OO rules I see in all the books.
That’s the 10% of the software lifecycle where you create the program dealt with. The acid test is what happens during the 90% of the software lifecycle where you users decide they want more, more, more. To quote the Pointer Sisters, who did a lot of software development:-
Oh users, I'll take your requirements down, I'll take them down
Where no one's ever wanted this before
And then you want more, yes you want more, more, more
I’ll JUMP to change the code, JUMP IN , oh ho ho ho
JUMP if you want to add requirements
In the night then, JUMP, JUMP off we go…..
Head First for Knowledge
I mentioned in the earlier blogs that the original two programs were based on a Java example by Michael Feathers and were about a Gothic Security System. My original thought was if this is such a good re-usable framework I can re-use it in the Head First Design Patterns example, which is all to do with using the State Pattern to write a program to control a Gumball Machine.
So step one, is to copy both programs and then change what they do. This part should actually be quite trivial in both cases, as each one has the “what does the program do” isolated from the “how do I do it”.
I also mentioned in the prior blog there is no point going into detail about what the “Gumball” program is supposed to do, as the configuration section and unit tests should make this obvious. In essence a Gumball is a spherical piece of chewing gum, you put your “quarter” in the machine, turn the handle and out one pops.
Doctor Who and the Procedurals of Doom
I’ll do the procedural one first … the first thing is to change the “configure” part of the code….
FORM configure_gumball_machine .
* Events
possible_events_are :
'quarter_was_inserted' 'QWIN',
'eject_button_pressed' 'QWEJ',
'crank_was_turned' 'CWTN',
'ball_was_dispensed' 'BWDI',
'machine_was_refilled' 'MWRF'.
* Commands
possible_commands_are :
'dispense' 'DISP'.
* States
possible_states_are :
'sold',
'sold_out',
'no_quarter',
'has_quarter'.
* Behaviour
* No Quarter State
state_changes_after_event:'no_quarter' 'quarter_was_inserted' 'has_quarter'.
* Has Quarter State
state_changes_after_event:'has_quarter' 'eject_button_pressed' 'no_quarter',
'has_quarter' 'crank_was_turned' 'sold'.
* Sold State
state_reached_sends_command: 'sold' 'dispense'.
state_changes_after_event: 'sold' 'ball_was_dispensed' 'no_quarter'.
* Sold Out State
state_changes_after_event: 'sold_out' 'machine_was_refilled' 'no_quarter'.
ENDFORM. " configure_gumball_machine
The idea is that this should read like natural language, so you should be able to guess what the program does from having a look at the above…. As might be imagined changing that did not take very long at all, and now I am half way through … I just need to rewrite the unit test.
That involved about ten “find / replace” commands. The end result is thus:-
LASS lcl_test_class IMPLEMENTATION.
METHOD setup.
PERFORM configure_gumball_machine.
ENDMETHOD.
*--------------------------------------------------------------------*
* Actual Test Methods
*--------------------------------------------------------------------*
METHOD sell_people_gumballs.
* GIVEN.....
PERFORM given_machine_has_gumballs.
* WHEN user excutes the steps in the correct order
PERFORM when_quarter_is_inserted.
PERFORM when_crank_is_turned.
* THEN_the user has been sold a gumball....
cl_abap_unit_assert=>assert_equals( act = gd_current_state
exp = 'sold' ).
ENDMETHOD.
ENDCLASS."Test Class Implementation
That was trivial … this framework can clearly be used by a totally different program with next to do effort.
I run the unit test to confirm all is working as expected.
Who is Mr.Orientated, and to what does he object?
I will know do the exact same thing with the OO version. I expected the change effort to be pretty much identical, as once again I am only changing the configuration and unit test sections.
This took almost exactly the same amount of time i.e. virtually none at all.
The fact that in both cases the changes were so easy is a testament to the “domain specific language” idea that Martin Fowler was trying to demonstrate in the first place. You don’t want to keep writing the “boiler plate” code again and again, you want to concentrate on what makes the program unique. This is the ever popular “separate the things that change from the things that stay the same”.
Here are some extracts from the OO Gumball program:-
* Events
possible_events_are :
quarter_was_inserted 'quarter_was_inserted' 'QWIN',
eject_button_pressed 'eject_button_pressed' 'QWEJ',
crank_was_turned 'crank_was_turned' 'CWTN',
ball_was_dispensed 'ball_was_dispensed' 'BWDI',
machine_was_refilled 'machine_was_refilled' 'MWRF'.
* Commands
possible_commands_are :
dispense 'Dispense' 'D1UL'.
* States
possible_states_are :
sold_state 'sold_state',
sold_out_state 'sold_out_state',
no_quarter_state 'no_quarter_state',
has_quarter_state 'has_quarter_state'.
CREATE OBJECT system_resetter
EXPORTING
io_start_state = no_quarter_state.
system_resetter->add_reset_event( machine_was_refilled->md_code ).
* No Quarter State
no_quarter_state->state_changes_after( event = machine_was_refilled
to_target_state = no_quarter_state ).
no_quarter_state->state_changes_after( event = quarter_was_inserted
to_target_state = has_quarter_state ).
* Has Quarter State
has_quarter_state->state_changes_after( event = eject_button_pressed
to_target_state = no_quarter_state ).
has_quarter_state->state_changes_after( event = crank_was_turned
to_target_state = sold_state ).
* Sold State
sold_state->state_reached_sends_command( dispense ).
sold_state->state_changes_after( event = ball_was_dispensed
to_target_state = no_quarter_state ).
* Sold Out State
sold_out_state->state_changes_after( event = machine_was_refilled
to_target_state = no_quarter_state ).
And
METHOD sell_people_gumballs.
given_machine_has_gumballs( ).
* WHEN user excutes the steps in the correct order
when_quarter_is_inserted( ).
when_crank_is_turned( ).
then_gumball_has_been_sold( ).
ENDMETHOD.
I run the unit test; I can be sure the OO program works. That was the easy bit.
Like a dream alive, a reason, everything must change, everything must change
OK let’s inject some extra requirements into the equation. Some people have been saying recently that if you just write your program properly in the first place and then cross your fingers and hope REALLY HARD then that will stop the end users asking for extra things.
Does anyone want to say to me “at MY Company we write things correctly and there are never any change requests ever. The users take what they are given and are so filled with joy that everyone lives happily ever after” ?
I am going to make the assumption that sometimes there is a need to add something extra to a program; no matter how well written it was in the first place. Maybe that makes me a lunatic.
In this case the extra requirements take the following form – the example program in the Head First Design Patterns book has two extra requirements that the “Gothic” example did not need.
The first is that in the Gothic example secrecy was the order of the day. If a burglar was trying to break in, and did the steps that opened the safe in the wrong order the last thing you want to do is say “that was wrong, you need to do it like this”. With a Gumball machine and the like though if the user forgets to enter money before turning the handle to get a gumball you want to tell them what they did wrong.
In IT terms this means that if an event is triggered which is not on the list of events which do anything, sometimes – and only sometimes – you want to send a message to the external system so it can inform the user what they have done wrong. In the nineteen sixties that would have involved flashing a light or – if you were really lucky – displaying some sort of blocky text on an LED screen. Nowadays even fridges have video screens and passport photograph machines talk to you.
In the Gothic Security system the safe action (event) always caused the same reaction. In the Gumball program we need some conditional logic – sometimes – e.g. you cannot buy a gumball if the machine is sold out. In the formal documentation of the State Pattern this is known as a “boundary condition”.
Wimbledon Common Requirements
If these new requirements were deemed so unusual they were unlikely to never be repeated then modifying the new program would be the go. However, if, as in this case, you think to yourself “these are quite reasonable things to ask for, I bet in the future lots of new programs will want to do this sort of thing” then it is the time to add some new features to the framework.
OO and a Bottle of Rum
I’ll change the OO framework first. I need to look at this from the point of view of the programmer who, in six months’ time, needs to add another illegal combination of current state and user triggered event to the program. I want them to do that in the “configure” section by adding another line to the area where macros are used to say in plain English what the program should do.
So, as well as:-
no_quarter_state->state_changes_after( event = quarter_was_inserted
to_target_state = has_quarter_state ).
I want to add a line like:-
NO_QUARTER_STATE->RESPONDS_TO( EVENT = CRANK_WAS_TURNED
WITH_ERROR = ‘Please enter money before turning crank’ ).
It’s not perfect English, but its close enough for government business. I’m working backwards here. I started with a macro – what would that macro do? The same as all the macro before it, it would trigger an event of the STATE class which in turn creates an object to handle the instruction you have just given it.
The “state changes” method creates a TRANSITION object. The immediate question comes up, should I enhance the transition object so that it sends out error messages when the wrong combination (as specified) is supplied? Or create a new class?
Well this is a tricky one, so as always I defer to the experts on a subject I am just learning about. So I turn to the definitive work on the subject “Advanced Mega-Principles of Paradigm Shifting Game Changing In-Memory Mobile Cloud Based Object Orientated Programming with Special Attention to Big Data” by UK boy band “One Direction”. I am sure every serious programmer has that on their bookshelves. I thought Boney M did a particularly good job of writing the foreword.
In the chapter entitled “Hey Now, Hey Now, Get Out of My Head” the 1D gurus advise that any given class has to do “that one thing” only. I could have sworn I had heard that principle somewhere else before, but anyway that’s clear enough.
The job of the “transition” class is to move the system from one state to another. The job of the class I am considering is to STOP the system moving from one state to another. Is that the same? Is that different? I could make arguments for both sides. I am going to say it is different.
I create a new class ZCL_SM_ILLEGAL_COMBINATIONS (after spending fifteen minutes agonizing over the name). Then I think – this is all to do with errors – should this be an exception class? However I think the idea is to separate code that works out if something is in error from the mechanism of declaring that an error has occurred from the code the deals with the error situation. That’s what it says in the “egg book” anyway.
http://www.sap-press.com/products/Enhancing-the-Quality-of-ABAP-Development.html
The State Machine framework I have been creating consists of really tiny classes that don’t do very much. This is line with Robert Martins’ “extract till you drop” approach of splitting up programs into tinier and tinier pieces until you split the atom. He got a lot of grief on the internet for suggesting this, so opinion is strongly divided.
This is just a “data object” which to my mind is a structure with ideas above its station. However, unlike a structure it can be enhanced with behaviour at a future date if I so desire e.g. I can check for combinations of attributes I don’t like etc… I don’t want to do anything like that at the moment, but the point is, you never know what the future may bring….
Now I have this lovely new class, I need an attribute in the STATE class to store a table of them, and a method to add a new illegal combination, which will get called by the macro.
Then I change my configuration routine in the main program as I wanted to do earlier:-
* No Quarter State
no_quarter_state->state_changes_after( event = machine_was_refilled
to_target_state = no_quarter_state ).
no_quarter_state->state_changes_after( event = quarter_was_inserted
to_target_state = has_quarter_state ).
no_quarter_state->responds_to( event = crank_was_turned
with_error = 'Please enter money before turning crank' ).
I am only half way there. I have told the system how I want it to re-act, but it also needs to read back that instruction at the correct point. I add another method into my global “state” class:-
METHOD does_not_allow_event.
READ TABLE mt_illegal_combos TRANSPORTING NO FIELDS
WITH KEY event_code = id_event_code.
IF sy-subrc = 0.
rf_this_causes_an_error = abap_true.
ELSE.
rf_this_causes_an_error = abap_false.
ENDIF.
ENDMETHOD.
Now I will create a good old exception class, ZCX_SM_ILLEGAL_COMBINATION, which takes in the “illegal combination” object. This is obviously overkill, but I am trying to do this the “right” way according to all the books on the subject.
I now adjust the method in the “controller” which responds to an inbound event coming in from the outside world:-
METHOD handle_inbound_event.
* Preconditions
CHECK id_event_code IS NOT INITIAL.
* Make sure we actually have a current state...
IF mo_current_state IS INITIAL.
transisition_to( mo_system_resetter->mo_start_state ).
ELSEIF mo_system_resetter->is_reset_event( id_event_code ) = abap_true.
transisition_to( mo_system_resetter->mo_start_state ).
ENDIF.
* No respond to the combination of the external event and the current state...
IF mo_current_state->does_not_allow_event( id_event_code ) = abap_true.
RAISE EXCEPTION TYPE zcx_sm_illegal_combination
EXPORTING
io_illegal_combination = mo_current_state->illegal_combination( id_event_code ).
ELSEIF mo_current_state->changes_state_after_event( id_event_code ) = abap_true.
transisition_to( mo_current_state->target_state_after_event( id_event_code ) ).
ENDIF.
ENDMETHOD.
This is all easy and straightforward isn’t it? I don’t know what all the fuss is about. I am still not there yet, I have decided that there is an error, and I have shouted out that there is an error, that just leaves the third part, which is doing something about it.
I am sending out the error message to the external system, so I need to modify the external system interface.
At long last it is time to put the cherry on top of the cake. I need to tell the “main” routine in the controller to respond to the exception thrown.
METHOD run_gumball_machine."of class gumball_machine
* Local Variables
DATA: lo_external_system TYPE REF TO lcl_external_system,
lo_controller TYPE REF TO zcl_sm_system_controller,
lo_error TYPE REF TO zcx_sm_illegal_combination,
ld_inbound_event_code TYPE string.
EXIT."To stop anybody actually running this test program!
CREATE OBJECT lo_external_system.
CREATE OBJECT lo_controller
EXPORTING
io_external_system = lo_external_system
io_system_resetter = me->system_resetter.
WHILE ld_inbound_event_code NE 'STOP'.
ld_inbound_event_code = lo_external_system->poll_for_event( ).
TRY.
lo_controller->handle_inbound_event( ld_inbound_event_code ).
CATCH zcx_sm_illegal_combination INTO lo_error.
lo_external_system->send_error_message( id_event_code = lo_error->io_illegal_combination->mo_trigger_event->md_code
id_error_message = lo_error->io_illegal_combination->md_error_message ).
ENDTRY.
ENDWHILE.
ENDMETHOD."Run Gumball Machine
Did I mention I love this sort of thing? All the team procedural people will be looking on in horror, but take it from me, this OO stuff becomes addictive really quickly. Mind you, so does crack cocaine apparently, so that is not a wonderful analogy, but there you go.
How do I know this works? I need to add a unit test.
METHOD not_sell_to_poor_people."for testing
given_machine_has_gumballs( ).
* WHEN user tries to turn the crank before entering money...
TRY.
when_crank_is_turned( ).
CATCH zcx_sm_illegal_combination.
"This is what I am expecting
RETURN.
ENDTRY.
cl_abap_unit_assert=>fail( msg = 'Gumball Machine sold gumballs with no money entered' ).
ENDMETHOD."Not Sell to Poor People
That works perfectly so that part is done. That was nice and easy (???) so let’s see how this stacks up to making the same change in the procedural program.
It’s the Same Old Song
In exactly the same way, I start by needing a new macro in the “configuration” routine in the program. True to the “domain specific language” principle, this is going to look pretty much the same as the OO version.
* Behaviour
* No Quarter State
state_changes_after_event: 'no_quarter' 'quarter_was_inserted' 'has_quarter'.
state_errors_after_event: 'no_quarter' 'crank_was_turned'
'Please enter money before turning crank'.
Just like with TDD I am working backwards here. I have not even written the macro yet, let alone any actual code to do what I want. So, the macro comes next, it’s all global variables here, I don’t need to worry about the philosophical considerations about class design that plagued me in the last example.
DEFINE state_errors_after_event.
clear gs_illegal_combos.
gs_illegal_combos-source_state = &1.
gs_illegal_combos-event_name = &2.
gs_illegal_combos-error_message = &3.
append gs_illegal_combos to gt_illegal_combos.
END-OF-DEFINITION.
I then define a global type, then structure, then table, then my program compiles. So far so good.
Now I move onto the actual code to handle errors I no longer have to walk backwards for Christmas, in procedural world you can declare the procedure you want, then double clik on it and a skeleton is produced – what WILL they think of next?
FORM handle_event USING pud_event_code TYPE char04.
* Preconditions
CHECK pud_event_code IS NOT INITIAL.
READ TABLE gt_events INTO gs_events WITH KEY event_code = pud_event_code.
CHECK sy-subrc = 0.
PERFORM check_for_errors USING gs_events-event_name
gd_current_state
CHANGING gd_subrc."Oh look, a return code!
CHECK gd_subrc = 0.
PERFORM transition USING gs_events-event_name
CHANGING gd_current_state.
ENDFORM. " HANDLE_EVENT
*&---------------------------------------------------------------------*
*& Form CHECK_FOR_ERRORS
*&---------------------------------------------------------------------*
* See if the event triggered by the user is not allowed for the
* current state the system is in
*----------------------------------------------------------------------*
FORM check_for_errors USING pud_event_name TYPE string
pud_current_state TYPE string
CHANGING pcd_subrc TYPE sy-subrc.
pcd_subrc = 0.
READ TABLE gt_illegal_combos INTO gs_illegal_combos
WITH KEY source_state = pud_current_state
event_name = pud_event_name.
CHECK sy-subrc = 0.
pcd_subrc = 4.
PERFORM send_error_to_ext_system USING gs_illegal_combos-error_message.
ENDFORM. " CHECK_FOR_ERRORS
*&---------------------------------------------------------------------*
*& Form SEND_ERROR_TO_EXT_SYSTEM
*&---------------------------------------------------------------------*
FORM send_error_to_ext_system USING pud_error_message TYPE string.
* Code to send out an error message to be displayed by an external system
* Most likely a proxy call to PI, which is of course OO, but there is no
* escape from some things...
ENDFORM. " SEND_ERROR_TO_EXT_SYSTEM
This appears to be going faster because I am doing this all within one program … but hang on, am I not comparing apples with oranges? When I did the OO version I had all Z repository objects. The reason is, there is virtually no support for local classes in the ABAP editor, so the fastest way to proceed OO wise is with global classes.
Lastly, it is unit test time, to prove this works.
CLASS lcl_test_class DEFINITION FOR TESTING
RISK LEVEL HARMLESS
DURATION SHORT
FINAL.
PRIVATE SECTION.
METHODS: setup,
"IT SHOULD.....................
sell_people_gumballs FOR TESTING,
not_sell_to_poor_people FOR TESTING.
ENDCLASS."Test Class Definition
METHOD not_sell_to_poor_people.
* GIVEN.....
PERFORM given_machine_has_gumballs.
* WHEN user tries to turn the crank before entering money...
PERFORM when_crank_is_turned.
* THEN_the system state does not change....
cl_abap_unit_assert=>assert_equals( act = gd_subrc
exp = 4
msg = 'Gumball was sold with no money entered' ).
ENDMETHOD."Not sell to poor people
The unit test passes, all is well.
I’m Judge Dredd, and You Creeps are Under Arrest
So, that is the first change made, what’s the verdict? It was certainly faster to change (enhance) the procedural version. However the OO version seems to lend itself better to reading more like natural language (which is one of the main aims of the game) and the error handling, which some would describe as more cumbersome, does tend to convey a clearer idea of what actually went wrong.
I will break off here and finish off in s separate blog.... I have already written this, so it won't be long in coming...