Design Patterns in ABAP – Visitor (Part 2)
To very briefly recap, in this exercise I am trying to solve a real world business problem (heavily disguised using monsters) using the so-called “Visitor” design pattern, implemented in ABAP.
In the preceding blog in this series I did not even go near the solution, I just created the test class to represent the desired outcome once the problem is solved.
https://blogs.sap.com/2017/01/13/design-patterns-in-abap-visitor-part-1-of-3/
The Visitor is one of a series of “patterns” designed to be able to change the behaviour of an existing program (more specifically a hierarchy of classes) without changing anything that already exists, on the grounds that changing code that currently works might break it.
One of the best generic examples of the Visitor pattern is the one I found on the “source making” website which goes as follows:-
The Visitor pattern represents an operation to be performed on the elements of an object structure without changing the classes on which it operates. This pattern can be observed in the operation of a taxi company. When a person calls a taxi company (accepting a visitor), the company dispatches a cab (visitor) to the customer. Upon entering the taxi the customer, is no longer in control of his or her own transportation, the taxi (driver) is.
My specific example is a disguised version of an actual business problem I had in real life. I have “monsterised” the problem so that it now involves how to deal with data mapping between SAP and a Monster Making machine, where the logic has to vary based on (a) the make of machine (b) the country of manufacture of the monster and (c) some unspecified future factor we do not know about yet.
My starting point will be the example of the Visitor Pattern as created by Robert C. Martin (Uncle Bob) in his 2002 book about “Pattern Languages of Program Design”
She wants to Build Me Up, Just to Knock Me Down
I am going to do exactly the same as I did with the “Decorator” pattern I examined in an earlier blog, and then go one stage further. So there will be three parts to this:-
That is, initially I will just do a straight translation of the Java code in the example (by Robert Martin) into ABAP. I’ll get that to the stage where the unit test class passes with green lights everywhere.
Then I shall come to the conclusion that is not actually what I want, and start messing around with the example until it bears no resemblance to the original. Once again, the revised solution should of course also make the test class pass.
Lastly I will consider if I should have bothered with any of this in the first place, by considering is standard SAP negates the need for the Visitor pattern completely. I have seen assorted people on the internet saying that every design can be replicated in ABAP, but I get the feeling some of them are already there, baked right into the language, albeit under different names.
Unified Top Model Language
Time for the first stage – a direct translation from Java into ABAP; this sort of thing gets easier over time as with each release ABAP becomes more like Java. Interestingly enough with each release both ABAP and Java also become more like JavaScript with the addition of functional programming features. This is like the “ice cream on the beach” situation where eventually all the ice cream stalls end up in the same spot.
One concept I cannot directly translate into ABAP is the concept of “overloading”. In languages like Java you can declare the exact same method name many times in a class, with different parameters each time. Then depending on how the method is called i.e. with what parameters then the appropriate method implementation is picked. You can sort of simulate that with optional parameters in ABAP and conditional logic within the method, but that breaks the idea that a method does one thing and one thing only. If a method does lots of different things depending on what optional parameters are passed in the that concept does not really apply.
Robert Martin – like virtually everybody else who I have encountered writing examples of how to program – starts with a UML diagram. When I started down the OO path I was told in no uncertain terms that the very first step was to learn UML as that was always the starting point in creating OO programs. So I read a sort of “UML for Idiots” book by Martin Fowler (UML Distilled) and it was great.
When writing by own book I was very interested to see two separate initiatives to create/change UML diagrams in Eclipse and have the appropriate classes and what have you automatically generated/changed inside SAP. That made a lot of sense to me, you have the design of the program abstracted, and when you are happy you can automatically generate/update the classes in any language you want – except ABAP.
In fact the two frameworks I tested did in fact enable you to generate the classes in ABAP, but neither worked all that well, as both were prototypes, one open source, one intended to be a licenced product. In the case of the former the author was too busy to finish it off, in the case of the latter the company (Obeo) came to the conclusion that there was zero demand for this, and thus they have a very tough time selling something no-one wanted (though some companies seem to manage this, it could be said that is the sole purpose of advertising to make someone buy something they do not actually want/need) and so decided not to proceed.
If it was a feature eagerly desired by many, I imagine SAP would have built this into the “ABAP in Eclipse” framework by now. Inside SAP you can generate a UML diagram once you have created all your classes (i.e. when you least need it) and the result looks like something Picasso painted on one of his bad days. It’s not quite as ugly as the SAP graphical workflow builder but it’s getting there.
So, no point in doing the auto-generation, but at the very minimum I can create the UML diagram in Eclipse. There is no standard tool to create one in Eclipse, but about half a million free ones that can be installed as plug-ins. I have stuck with the Obeo one, because (a) they were very helpful when I was writing my book and (b) they sound a bit like a woodwind musical instrument.
Figure 002 – Initial Visitor UML Diagram
The change I was forced to make during translation was as follows – in the original example the visitor interface had multiple VISIT methods, each taking a different type of class as its input parameter – the aforementioned overloading. Naturally I cannot do that in ABAP. Why did it need lots of different visit methods? Due to each concrete “machine” class having different attributes, independent of the interface.
The obvious consequence of this is that every time you create a new machine type, you have to add a new method to the monster machine visitor interface. That sounds really bad. It is even worse in languages like Java. What puzzles us ABAP types when reading the Java (and other language) examples on the internet is the constant worry about “compile time”. In ABAP compilation is trivial as everything is in the repository, but in most languages all the code is in a directory of files on the local machine and compiling on the code that depends on each other can take hours. I don’t know what language Dilbert writes in, but I know it is not ABAP as he told his pointy haired boss that he checked his emails whilst he waited for his code to compile. In summary, we are spoilt rotten in ABAP world.
You will notice the different machine types have some attributes unique to a particular make. For example the ACME machine plays disco music whilst making a monster, whilst the BLOGGS make lets off fireworks. In some countries certain disco albums and firework colours are outlawed so the Visitor would have to deal with that.
I studied Economics at university, so in true Economist style I am going to handle the problem of the different machine classes having machine type unique attributes by pretending it does not exist. This is because it is not a problem I can solve with the design as it is above. I will come back to this problem later, when I start stuffing around with radical design changes.
Normally I like doing this sort of exercise with all local classes, but here we have interfaces with cross dependencies, so the only way to create them is as Z objects with SE24.
Here we have the source code based versions of the two interfaces:-
INTERFACE zif_monster_machine
PUBLIC .
DATA make TYPE zde_monster_machine_make .
DATA instructions TYPE zst_mlink_monster_input_data .
METHODS set_instructions
IMPORTING
!is_instructions TYPE zst_mlink_monster_input_data .
METHODS get_instructions
RETURNING
VALUE(rs_instructions) TYPE zst_mlink_monster_input_data .
METHODS make_monster .
METHODS accept
IMPORTING
!io_visitor TYPE REF TO zif_monster_machine_visitor .
ENDINTERFACE.
INTERFACE zif_monster_machine_visitor
PUBLIC .
METHODS visit
IMPORTING
!io_machine_to_be_visited TYPE REF TO zif_monster_machine .
ENDINTERFACE.
In a nutshell, any class implementing the monster machine interface can be visited by any class implementing the monster machine visitor class. Let us have a look at our two concrete visitor classes:-
CLASS zcl_french_machine_adapter DEFINITION
PUBLIC
CREATE PUBLIC .
PUBLIC SECTION.
INTERFACES zif_monster_machine_visitor .
ALIASES visit
FOR zif_monster_machine_visitor~visit .
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_french_machine_adapter IMPLEMENTATION.
* <SIGNATURE>-----------------------------------------------------------------------------------+
* | Instance Public Method ZCL_FRENCH_MACHINE_ADAPTER->ZIF_MONSTER_MACHINE_VISITOR~VISIT
* +---------------------------------------------------------------------------------------------+
* | [--->] IO_MACHINE_TO_BE_VISITED TYPE REF TO ZIF_MONSTER_MACHINE
* +----------------------------------------------------------------------------------</SIGNATURE>
METHOD zif_monster_machine_visitor~visit.
DATA(adapted_instructions) = io_machine_to_be_visited->get_instructions( ).
adapted_instructions-mortgage_ability = '1'.
"Cannot Sell Mortgages
io_machine_to_be_visited->set_instructions( adapted_instructions ).
ENDMETHOD.
ENDCLASS.
CLASS zcl_usa_machine_adapter DEFINITION
PUBLIC
CREATE PUBLIC .
PUBLIC SECTION.
INTERFACES zif_monster_machine_visitor .
ALIASES visit
FOR zif_monster_machine_visitor~visit .
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_usa_machine_adapter IMPLEMENTATION.
* <SIGNATURE>-----------------------------------------------------------------------------------+
* | Instance Public Method ZCL_USA_MACHINE_ADAPTER->ZIF_MONSTER_MACHINE_VISITOR~VISIT
* +---------------------------------------------------------------------------------------------+
* | [--->] IO_MACHINE_TO_BE_VISITED TYPE REF TO ZIF_MONSTER_MACHINE
* +----------------------------------------------------------------------------------</SIGNATURE>
METHOD zif_monster_machine_visitor~visit.
DATA(adapted_instructions) = io_machine_to_be_visited->get_instructions( ).
adapted_instructions-mortgage_ability = '3'.
"Must Sell Mortgages
io_machine_to_be_visited->set_instructions( adapted_instructions ).
ENDMETHOD.
ENDCLASS.
The concrete visitor classes only know about the publically available methods of the monster machine interface, not the make specific attributes like firework colour or disco album. As I said, that problem will be addressed in due course.
In a lot of the visitor examples I have seen the Visitor goes about changing any of the attributes of the visited class willy-nilly, without a care in the world. I find that a bit scary, so I have the Visitor make its changes using a SET method which can apply some sanity checks to the proposed change. In the “Design by Contract” pattern there is something called a “class invariant” which means that after every operation the class must end up in a logically consistent state. The example I always use is that a Monster cannot be pink and fluffy after a method has been called on a Monster instance, because then it would not be a proper Monster anymore.
In the same way if the Visitor tried to make a fatal change to the state of the visited object you could have logic in the SET method to raise an exception. That way we can guard against an “auto destruct” class that implemented the machine Visitor interface coming along and blowing up our monster machines.
Now is the time to have a Butchers Hook at the Monster Making Machine classes. In fact we need only look at one of them, as they are virtually identical.
CLASS zcl_acme_machine DEFINITION
PUBLIC
CREATE PUBLIC .
PUBLIC SECTION.
INTERFACES zif_monster_machine .
ALIASES accept
FOR zif_monster_machine~accept .
ALIASES get_instructions
FOR zif_monster_machine~get_instructions .
ALIASES make_monster
FOR zif_monster_machine~make_monster .
ALIASES set_instructions
FOR zif_monster_machine~set_instructions .
METHODS constructor .
METHODS: get_disco_album RETURNING VALUE(rd_result) TYPE string,
set_disco_album IMPORTING id_disco_album TYPE string.
PROTECTED SECTION.
PRIVATE SECTION.
DATA: disco_album TYPE string.
ALIASES instructions
FOR zif_monster_machine~instructions .
ALIASES make
FOR zif_monster_machine~make .
ENDCLASS.
CLASS zcl_acme_machine IMPLEMENTATION.
METHOD constructor.
make = 'ACME'.
ENDMETHOD.
METHOD get_disco_album.
rd_result = me->disco_album.
ENDMETHOD.
METHOD set_disco_album.
me->disco_album = id_disco_album.
ENDMETHOD.
METHOD zif_monster_machine~accept.
io_visitor->visit( me ).
ENDMETHOD.
METHOD zif_monster_machine~get_instructions.
rs_instructions = instructions.
ENDMETHOD.
METHOD zif_monster_machine~make_monster.
* Send final instructions to ACME Machine via a PI Proxy
ENDMETHOD.
METHOD zif_monster_machine~set_instructions.
instructions = is_instructions.
instructions-max_lightning_bolts = 5.
ENDMETHOD.
ENDCLASS.
The BLOGGS machine is exactly the same apart from the private attribute being Firework Colour rather than Disco Album, and the maximum lightning bolts being 10 instead of 5.
As you are no doubt aware in language like Java and JavaScript a reference to a “member” variable of the current instance of the class can be accessed using the term THIS as is:-
THIS->KISS_ITS_CRIMINAL
In ABAP world SAP had to be different, so the term you use is ME as in:-
GO_UP_THE_APPLES_AND_PEARS( ME->OLD_CHINA ).
The SAP developers must have spent a lot of time in London, watching “Minder”, and I imagine they often sing the following:- All right my son, say no more, leave it out, no problem, as it happens it’s your shout. Do what? Pull the other, in a right two and eight, ere what’s the damage and, who’s your mate?
Now that I have utterly lost anyone who has never even heard of Cockney Rhyming Slang or “Minder” I had better get back to talking about sensible languages like ABAP.
Now that we have quite finished
messing around we should have enough code by now to be able to finish off the unit tests. In the prior blog I had finished 95% of the test code; in fact all of the code in the test class - the only thing missing was the implementation of the DO_MAPPING method in the class under test, as that could not be written until the four concrete classes had been created.
Here is the end result:-
CLASS lcl_monster_mapper IMPLEMENTATION.
METHOD do_mapping.
MOVE-CORRESPONDING is_source_data TO rs_result.
CASE machine_make.
WHEN 'ACME'.
monster_machine = NEW zcl_acme_machine( ).
WHEN 'BLOGGS'.
monster_machine = NEW zcl_bloggs_machine( ).
ENDCASE.
monster_machine->set_instructions( rs_result ).
CASE country.
WHEN 'FR'.
machine_visitor = NEW zcl_french_machine_adapter( ).
WHEN 'US'.
machine_visitor = NEW zcl_usa_machine_adapter( ).
ENDCASE.
machine_visitor->visit( monster_machine ).
rs_result = monster_machine->get_instructions( ).
ENDMETHOD.
With that final piece of the puzzle in place I go into my SE38 test program where the local classes live and take the menu path PROGRAM->EXECUTE->UNIT_TESTS. There are no errors, so everything is working perfectly.
So am I happy with this? No I am not. Not in the slightest. We have got to the “first make it work” part, and now is the “then make it good” part of the TDD wheel. There are more problems with the current design than you can shake a stick it, the major one being it doesn’t really even use the Visitor pattern properly.
In Part 3 of this blog (next week) I will list the myriad of problems I have with my code at the moment, and take an axe to the current design until I have totally rebuilt it into something I am a bit more happy with…