OOP geek journal – separate responsibilities
I could give number of reasons why I think we should commonly use Object Oriented Programming style and probably equal number of reasons why I think is it so magical. Anyhow, this blog is not intended to write a thesis, but to share with you, the Reader, with couple nice tricks I have learnt recently when I decided to take OO approach for some trivial issue. I leave it to your decision, whether you smuggle any of these to your applications or just share your thoughts about that.
In SAP HR module, different objects can be stored in infotype 1000 (table HRP1000, where PLVAR (2char) OTYPE (2char) and OBJID (8 numc) are key info of the object). Among others we can have i.e. priority object. The object can have long description stored in infotype 1002 (co-joined tables HRP1002 and HRT1002). SAP delivers by standard a function RH_OBJECT_DESCRIPTION_WRITE which allows to store the text w/o bothering of how it is splited b/w these tables. All we should do is to give object coordinates (key info) and its description with PT1002 table type to function.
In my case I had the data originated from excel file. Long description was provided in form of a STRING. So, I needed a conversion from STRING to PT1002 table type. Seems that function HRHAP_CONVERT_STRG_TO_PT1002 was ideal for this task. It turned out, however, it could not parse the STRING correctly. I wanted to handle extra chars (i.e. new line) but it didn’t. Instead it parsed the text at 79th char. So the split could happen i.e. in the middle of a word. This caused that below snippet…
data lv_data type string. data: lt_pt1002 type hap_t_pt1002, lw_pt1002 type pt1002. concatenate 'Do I/we...' '... collaborate?' '... communicate openly ?' '... share ideas?' '... leverage knowledge?' '... have a focus outside and inside (about customers, partners)?' into lv_data separated by cl_abap_char_utilities=>newline. call function 'HRHAP_CONVERT_STRG_TO_PT1002' exporting string_description = lv_data importing t_desc_tab = lt_pt1002. loop at lt_pt1002 into lw_pt1002. write: / lw_pt1002-tline. endloop.
… resulted in
Do I/we…#… collaborate?#… communicate openly ?#… share ideas?#… lever
age knowledge?#… have a focus outside and inside (about customers, partners)?
This was wrong. I should have data splitted both at new line and prevent split at 79th char. So I came with custom solution.
The requirement was to:
- split the string into lines
- if a line doesn’t fit 79th chars restriction, it should break into words and split should happen after last fitting word
- I decided to build more general solution where I could split at char level if a word does not fit restriction other than my module defined
I ended up with procedular solution encapsulated within function module and some local routines. Please don’t study the code, just take a brief look how the flow is organizated.
function z_glpm_fum_sstr_to_pt1002. *"---------------------------------------------------------------------- *"*"Local Interface: *" IMPORTING *" VALUE(STRING_DESCRIPTION) TYPE STRING *" EXPORTING *" REFERENCE(T_DESC_TAB) TYPE PTM_T_PT1002 *"---------------------------------------------------------------------- * this is a custom version of function HRHAP_CONVERT_STRG_TO_PT1002 * which splits string either at 79th char or cut it if it doesn't fiy data: lw_row type pt1002. data: lt_lines type string_table, lw_line type string. data: lt_words type string_table, lw_word type string. data: lt_chars type string_table, lw_char type string. data: lv_fits type flag, lv_windex type i, lv_cindex type i, lv_exc type c. **-------------------------------------------------------------------- LINES start * get lines perform get_lines using string_description changing lt_lines. * for each line loop at lt_lines into lw_line. * line fits row? perform is_line_fitting using lw_line lw_row changing lv_fits. if lv_fits = 'X'. * add line perform conv_line_2_row using lw_line changing lw_row. else. **-------------------------------------------------------------------- WORDS start * get words perform get_words using lw_line changing lt_words. * for each word do. add 1 to lv_windex. read table lt_words into lw_word index lv_windex. if sy-subrc <> 0 . exit. else. * word fits row? perform is_word_fitting using lw_word lw_row changing lv_fits. if lv_fits = 'X'. * add word perform conv_word_2_row using lw_word changing lw_row. else. * word does not fit empty row? if lw_row is initial. **-------------------------------------------------------------------- CHARS start * get chars perform get_chars using lw_word changing lt_chars. * for each char do. add 1 to lv_cindex. read table lt_chars into lw_char index lv_cindex. if sy-subrc <> 0. exit. else. * char fits row? perform is_char_fitting using lw_char lw_row changing lv_fits. if lv_fits = 'X'. * add char perform conv_char_2_row using lw_char changing lw_row. else. * char does not fit empty row? if lw_row is initial. * the smallest possible entity does not fit, this situation can't be handled, leave the application lv_exc = 'X'. exit. else. * EOR - end of row append lw_row to t_desc_tab. clear lw_row. * char does not fit, redo process for the char once again subtract 1 from lv_cindex. "Format2 endif. endif. endif. enddo. if lv_exc = 'X'. exit. endif. clear lv_cindex. **-------------------------------------------------------------------- CHARS end else. * EOR - end of row append lw_row to t_desc_tab. clear lw_row. * word does not fit, redo process for the word once again subtract 1 from lv_windex. endif. endif. endif. enddo. **-------------------------------------------------------------------- WORDS end endif. if lv_exc = 'X'. "Converter exit. endif. * EOR - end of row append lw_row to t_desc_tab. "Converter clear: lw_row, lv_windex. endloop. **-------------------------------------------------------------------- LINES end endfunction. form get_lines using ip_data type string changing ct_lines type string_table. ... endform. "get_lines form get_words using ip_data type string changing ct_words type string_table. ... endform. "get_words form get_chars using ip_data type string changing ct_chars type string_table. ... endform. "get_chars form is_line_fitting using is_line type string is_row type pt1002 changing cp_fits type flag. ... endform. "is_line_fitting form is_word_fitting using is_word type string is_row type pt1002 changing cp_fits type flag. ... endform. "is_word_fitting form is_char_fitting using is_char type string is_row type pt1002 changing cp_fits type flag. ... endform. "is_char_fitting form conv_line_2_row using is_line type string changing cs_row type pt1002. ... endform. "conv_line_2_row form conv_word_2_row using is_word type string changing cs_row type pt1002. ... endform. "conv_word_2_row form conv_char_2_row using is_char type string changing cs_row type pt1002. ... endform. "conv_char_2_row
The flow of a converter FM is controlled with three loops and bunch of conditions. Although some separation was achieved by introducing routines, the main part isn’t state-of-the-art programming example.
Pros and cons
What if we want to:
- reuse the same module in other application with different length restriction and different definition what is a line, a word or a char. Sure, we could distinguish them with number of parameters but how will that look like?
- split just by words and chars not by lines. We would need to introduce ugly conditions which would check that. Assuming we would also like to have combination of lines and words, the condition might get complicated more.
Basically I would like to have a “dynamically configurable” tool. I want the configuration be done at code level, not in the DB where, I still would need to maitain the entries. As I feel frustrated seeing to much of a complicated logic at one spot I would probably want to simplify it. Hey, isn’t that a perfect situation for OO solution? Sure it is! Let’s see what objects can propose us.
Object Oriented Analysis & Design
The first thing to do would we be looking for the candidates for the objects. Let’s create CRC cards first. Basically CRC card denotes Class Responsibility Collaboration card and serve the purpose for simplier design. It let us visualize what the candidates are responsible for and who they will interact with. CRCs are cool as they don’t require technical tools. For instance I use office yellow stickers for them. Seeing the candidates will ease seeing the proces they support by moving them on the table. Besides such candidate can be easily replaced or removed at all, if we decide it doesn’t meet our needs.
With these candidates on board, let’s see if there is any common design pattern that we could use as a base solution. After googling a bit, I have found a Builder Pattern that seems suitable for our case. Let’s see how we can adapt it to our situation.
We have a Product, we have a Builder (Converter), we have also some extra candidate Formatted Part (I will call it Part for short), but this seems to be used only internally. We are missing a Director, who controls the flow and delegates job to build certain parts to concrete Builder. Here I decided that the Builder itself can serve the purpose of a Director too. Moreover, who would pay this guy for doing nothing that the rest of the team couldn’t handle itself 🙂
Ok, so let’s change our Converter candidate to be called Builder. It now looks like (changes in red):
Intentionally I am not working with classes yet, just with CRC cards, so that we can easily change them if required.
We still miss one thing here. Although the Builder defines uniform approach for the Parts to be put together to a Product, it must introduce different ways of producing these Parts (its initial responsibility). I.e. building Parts will be different for Lines, Words, Chars. That means we will need different kinds of Builders to be able to decide which Part it want to use as Product parts. Quick modification to CRC card with give us below:
Builder now also serves the purpose of an Factory Method, where in general, concreate Builders are responsible for building concrete Parts.
Final look at candidates
A lot has happened to our candidates, so let’s take final look at what we have on the table.
There is one major feature emphasized here. We have a Builder who is able to build a Product based on Part type we choose. This would suffice if we wanted to split the text either to Lines or Words or Chars. But how about utilizing them all at a time. We need Lines and Words and Chars to correctly address the split process. In other words, if concrete Builder cannot process the request (cannot add the parts), we should delegate this to sub Builder which we try to process it at more detailed level. If that one fails too, we will again delegate to Sub Builder of the current sub Builder. Such chain of one task delegation can be easily implemented with variant of a Decorator Pattern .
Again we will need to adapt it, just to ease its structure. In fact all we need to add to the Builder, is a reference to its sub Builder, so that we know who we can delegate certain job to. Our candidate then gains new responsibility.
Note that this new Responsibility is a private one. From the Client perspective it should be transparent how the build is done. It is only the Builder who knows how to do the job.
Centralized control style
The candidates can defend they candidacy, so we are probably fine with turning them into classes. Anyhow, before that happens, let’s take closer look how the build process will look like (in the Builder).
Create Parts based on the Builder we choose
For each part
Does Part fits product buffer (i.e. currently filled work area)?
Append to buffer
Does part fit empty buffer at all (i.e. empty work area)?
Create Parts based on the sub Builder we have
For each Part
…we repeat the steps for the conditions and delegate the job to another sub Builder in case it cannot be processed at this level
Finalize current context (i.e. append buffer work area to output table)
Flush buffer and add the part to it
Finalize the Part add (i.e. put line break at the end of part join to the output table)
The above logic reflects the procedural code I shared at the beginning. There are lots of conditions to be handled at one spot. This doesn’t look good. What we can remark here:
- The Builder controls entire consolidation process, he is the master of a ceremony
- It decides when to delegate the process to sub Builder if it cannot be processed at given granularity
- Part and Product are very simple, unintelligent candidates
Tendency of one object to gather all the responsibility of an application for process control, is an example of centralized control style. Such object usually lies at application services layer. In fact here we have some variation of this kind of control, namely a clustered control style. This means that the control is spread across the objects of same type (Builders), but they still reside at same layer of an abstraction (application).
Before we go further we would like to see some code. The abstraction we have is enough to build classes from the candidates.
I used abstract classes not interfaces because the classes share common behaviour with their implementation (sub classes). With a bit of a struggle, we could also use interfaces and delegate common behaviour to i.e. external utility classes. In this case it would however introduce extra complexity. The aim here is to show how to design classes responsibility and how these classes can collaborate to fulfill application responsibility.
class lcl_builder definition abstract. public section. methods: constructor importing value(ir_subbuilder) type ref to lcl_builder OPTIONAL, build_product importing ip_text type string changing value(cr_product) type ref to lcl_product. protected section. methods: get_formatted_type abstract importing ip_text type string returning value(rr_formatted_type) type ref to lcl_formatted_part. private section. data: product type ref to lcl_product, subbuilder type ref to lcl_builder. methods: add_part importing ir_part type ref to lcl_formatted_part, parse_to_formatted importing ip_text type string returning value(rt_formatted_part) type lcl_formatted_part=>tt_formatted_parts. endclass.
Let’s leave the implementation details now. We only need to know that:
- get_formatted_type method returns instance of a Part. It is concrete type of Builder who decides what type of Part instance to return. The method is protected and abstract as it needs to be redefined in the Builder subclass. Here is where Factory Method comes into play.
- parse_to_formatted method gets Parts instances based on given text. This one need to return the same type that above method will return in order we work only with one type of Part objects. Details will come later.
- build_product method is the heart of application control. This one makes all the decisions as for when and how to add the Parts to the Product.
class lcl_formatted_part definition abstract. public section. types tt_formatted_parts type table of ref to lcl_formatted_part with default key. methods: constructor importing ip_text type string, get_formatted returning value(rp_formatted) type string, get_length returning value(rp_len) type i, get_separator returning value(rp_sep) type string. private section. data formatted type string. endclass.
Part class gives us following information:
- All of its methods are public and are used purely to expose some information of its internal state
- As with the previous class it will receive their own implementation in the subclass, i.e. the separator is different for Line and different for Word
class lcl_product definition abstract. public section. methods: is_fitting_buffer importing ir_part type ref to lcl_formatted_part returning value(rp_fits) type flag, is_fitting_new_buffer importing ir_part type ref to lcl_formatted_part returning value(rp_fits) type flag, add_to_buffer importing ir_part type ref to lcl_formatted_part, add_to_new_buffer importing ir_part type ref to lcl_formatted_part, finalize_part importing ir_part type ref to lcl_formatted_part. protected section. methods get_max_buffer_length abstract returning value(rp_len) type i. private section. data product type table of string. data buffer type string. endclass.
Here what we notice that Product:
- Has lots of public methods which Builder will call based on decisions it makes. Basically these will be used to control process of adding the parts to the Product
- get_max_buffer_length method decides about the output restriction (product length). Again, as in case of a Builder, the subclass which redefines this method will decide on this factor
Delegated control style
The delegated control programming style is preferred over above one in order to avoid application which consists of one big intelligent object and several “dumb” ones. Instead we will have application intelligence spread across all of them, resulting in small, decent objects that can make some decision upon themselves and the application they belong to. The idea is to split the responsibility b/w objects and get involved those from the domain application layer,
Ok. Nice words, but how to inject some intelligence to our Part and Product, taking over some of a Builder’s burden. Let’s update our CRCs.
The Builder now has transformed from the Controller to a simple Coordinator. It does not do any decision anymore, it simply gathers generated Parts (from within Part class itself) and redirects them to the Product which knows how to add them. Finally it delegates the job of finalizing part addition to the Part itself. This one knows how to finalize itself by calling appropriate method of a Product.
Let’s have a look how classes interface has changed.
class lcl_builder definition abstract friends lcl_formatted_part. public section. methods: constructor importing value(ir_subbuilder) type ref to lcl_builder OPTIONAL, build_product importing ip_text type string changing value(cr_product) type ref to lcl_product. protected section. methods: get_formatted_type abstract importing ip_text type string returning value(rr_formatted_type) type ref to lcl_formatted_part. private section. data: product type ref to lcl_product, subbuilder type ref to lcl_builder. methods: add_part importing ir_part type ref to lcl_formatted_part, parse_to_formatted importing ip_text type string returning value(rt_formatted_part) type lcl_formatted_part=>tt_formatted_parts. endclass.
- in parse_to_formatted method use static method of Part to generate objects. To assure type consistency it passes its reference in order that static method could determine type of Part Builder is attached to. It simply utilizies method get_formatted_type of a Builder and gets an instance of the object Builder is responsible to build. As the method is protected (we don’t want someone from the outside generate formatted parts) we must ensure Part is on friends list of the Builder. Then it is able to use its protected and private merhods.
class lcl_formatted_part definition abstract. public section. types tt_formatted_parts type table of ref to lcl_formatted_part with default key. methods: constructor importing ip_text type string, get_formatted returning value(rp_formatted) type string, get_length returning value(rp_len) type i, finalize importing ir_product type ref to lcl_product, get_separator returning value(rp_sep) type string. class-methods: get_parts importing ir_builder type ref to lcl_builder ip_text type string returning value(rt_formatted_parts) type tt_formatted_parts. protected section. methods parse abstract importing ir_builder type ref to lcl_builder ip_text type string returning value(rt_formatted_parts) type tt_formatted_parts. private section. data formatted type string. endclass.
- introduces static get_parts method which receives a Builder instance who requested parsing
- each its subclass provides different way of parsing input text i.e. Line needs to be split at \n, while Char needs to be analyzed char by char
- when static method receives concrete Builder, it gets the Part with entire initial text out of it. It then delegates parsing the text to the concrete Part. This one knows how to parse it in order to generate set of objects of same type (Lines, Words or Chars)
- was enhanced with new method finalize. Now concrete Part type knows how to finalize its addition to the Product by calling its appropriate method. This decision is no longer made in the Product. It was simply spread across subclasses of Part and the polimorphism did the job.
class lcl_product definition abstract. public section. methods: add_part importing ir_part type ref to lcl_formatted_part returning value(rp_added) type flag, finalize_part. protected section. methods get_max_buffer_length abstract returning value(rp_len) type i. private section. data product type table of string. data buffer type string. methods: is_fitting_buffer importing ir_part type ref to lcl_formatted_part returning value(rp_fits) type flag, is_fitting_new_buffer importing ir_part type ref to lcl_formatted_part returning value(rp_fits) type flag, add_to_buffer importing ir_part type ref to lcl_formatted_part, add_to_new_buffer importing ir_part type ref to lcl_formatted_part. endclass.
- introduces method add_part which makes the decision whether and how the part should be added to current Product buffer. Only this one is called from within the Builder
- all the methods which handle complexity of the process are now private and only above method knows how to correctly use them (defines the process)
- gets new finalize_part method which Part could call directly from its finalize method (if finalizing this part is required)
Below a pseudo UML diagram showing the flow of the application.
As we can see now:
- Client indicates the Product he wishes to get
- Client create the Builders and nest them within one another as sub Builders
- Client request to build_product to the Builder
- Builder needs to get Parts out of non converted input text, for this it sends its reference to Part via static method get_parts
- This Builder reference is used to get Part object which the Builder is responsible to build (method get_formatted_part)
- Using above Part object, creation process (method parse) takes place and objects of same type are generated, representing text as set of Part objects
- The Parts are returned to the Builder
- The Builder iterates over returned Parts (of one type) and sends them to Product requesting add_part
- Product does necessary internal check and makes decision if this part can be added at all (i.e. line does not fit Product restriction of 20 chars)
- Builder receives information if add process succedded
- If so, Part is finalized by calling it’s finalize method. Concrete Part itself decides then how to finalize its build (i.e. we want to call Products finalize_part method at the end of a Line or Word, but not necessary at the end of a Char). This way we avoid Builder to check what concrete Part type is the Part object of
- If not, build_product is triggered against sub Builder as long as there is one available; the process repeats for new sub Builder
- At the end of process, the Product gets returned to the Client
This is what application generates for 79 char Product – only Line Builder comes into play
… communicate openly ?
… share ideas?
… leverage knowledge?
… have a focus outside and inside (about customers, partners)?
For 20 char Product – both Line and Word Builders used
… share ideas?
… have a focus
outside and inside
For 6 char Product – utilizing Line and Word and Char Builders
There are no perfect solutions, neither is this one. Here are some reasons why:
- Product methods add_part / finalize_part are made public in order Builder or Part could call them at right time. For the price of exposing Product very intrinsic details, we encapsule the decision of how Parts are added to it. As the Client has direct access to the Product, he can easily change Product’s intrenal state
- In order Part can call protected method get_formatted_type of a Builder (to generate objects of same type Builder is responsible for), we must put Part on Product’s friends list. This makes these classes coupled. Such situation is acceptable for the objects at same hierarchy level (i.e. siblings) but here Bulider and Part are of different type, so the design “sucks” a bit
- There is probably too much of an abstraction especially for the Product. We could delegate some of Product sub classes responsibility to the Builder i.e. getting required Product lenght. Anyhow this would contradict separation of concerns where we would mix application service layer (Builder) with a domain layer (Product)
Modularity / Encapsulation
As we introduced clear object distinction, each object received unique responsibility. The objects by fulfilling their goals, collaborate with each other and allow to achieve more general aim.
We have clear separation b/w objects from the domain layer (Product, Part) and application services layer (Builder). Though, the Builder need to know someting about the Part, it is fine as this object it is application specific. Bad design would do the opposite – couple domain object with the application one.
By introducing delegated style, objects match their stereotype and focus on elementary tasks, rather than on huge range of tasks. We end up with simplier to read and easier to maintain application. The objects also fit more their existence purpose and are not overloaded with additional heavy responsibilities.
Object oriented programming style gives us ability to reuse same objects in different context. We can easily use this functionality in some other application which has different Product length and Part requirements. All we do, is to use different kind of object that share common interface and polimorphism will do the job. The beauty sits in the dynamically configurable application which depends on what Client chooses. The Builder is even not aware which objects the Client has selected.
Maintainability via separation
Changing one object doesn’t affect the other one. You may i.e. change the way Product conducts “add part” process and the rest of the application will stay untouched. It will still magically work.
Adding new Parts is not a problem anymore. We create a class which inherits from the Formatted Part and define entity which introduces new semantic text presentation. I.e. we could ask to split by Hex digits, double char mark etc. The same applies to Product, where adding new length restriction is not a problem now. With some efford we could even request some more complex form of internal product representation.
Although there are some gaps in this design and we could further study it to enhance a bit, it show us how we should do an Object Oriented Analysis and Design in order to break the responsibility of the entire applicaiton into smaller, more unique units. Using delegated style let us inject some inteligence to the objects used by the application (residing at domain level). Patterns are helpful here, but don’t need to be the heart of the design. If we pick one, we should always try to adpat it to our needs and context. Using only the most valuable part of them in conjunction with adaptation to our needs, creates more reliable and custom-made applications.
Next time you have a chance, pick the Object Oriented approach and create your own solution.
Questions? Ideas? Share your thoughts please!