The issue with having many small classes
The previous blog post was about applying design principles to the architecture of a simple parser application, which served as an exercise application. It led to quite a few, small classes in the end which all had their own responsibility.
As most of the classes deal with solving a certain aspect of the exercise, one class has the responsibility to arrange the work of the other classes to solve what needs to be solved. This class implements the interface ZIF_OCR_PARSING_ALGORITHM . An instance of that class is given to every consumer which needs to parse some sample input.
Every low level class that this class is dealing with to “orchestrate” the program flow, is registered as an dependency. Dependencies can be declared in the constructor of this course grained class. This makes creation of that class impossible without satisfying its dependencies.
methods CONSTRUCTOR importing !IO_FILE_ACCESS type ref to ZIF_OCR_FILE_ACCESS !IO_LINES_SEPERATOR type ref to ZIF_OCR_LINES_SEPERATOR !IO_CHARACTERS_SEPERATOR type ref to ZIF_OCR_CHARACTERS_SEPERATOR !IO_CHARACTER_PARSER type ref to ZIF_OCR_CHARACTER_PARSER .
Providing dependencies during object creation by an external caller is called “dependency injection” (DI) as opposed to having classes that create their dependencies on their own. As the creation of the low level instances is not in the control of the class that orchestrates the parser flow, this paradigm is also called “Inversion of control” (IoC).
However, this approach has another challenge ready. That issue has been described very clearly by Oliver as a reply to the previous blog post
“Injection in the constructor – even to an interface – looks like quite tight coupling to me as it leaves the consumer of the coarse-grained class (the algorithm, in your case) to instantiate the composites.”
Of course the consumer of the composite object model should not create it on its own. One possibility would be to utilize the factory pattern. Instead of composing the main object and all of its dependent objects on its own, the consumer delegates this task to a factory method, which could be a static method call in its most simple realization:
DATA lo_parser TYPE REF TO ZIF_OCR_PARSING_ALGORITHM. lo_parser = ZCL_PARSER_FACTORY=>CREATE_NEW_PARSER( ).
Within CREATE_NEW_PARSER( ) the same magic takes place as before: in the first step, all low level objects are created, in the second step the main parser object is created, while its dependencies declared by the CONSTRUCTOR are satisfied. This approach is called “Poor man’s dependency injection”
*alternative 1: poor man's DI DATA lo_ocr TYPE REF TO zif_ocr_parsing_algorithm. DATA lo_char_sep TYPE REF TO zif_ocr_characters_seperator. DATA lo_char_parser TYPE REF TO zif_ocr_character_parser. DATA lo_file_access TYPE REF TO zif_ocr_file_access. DATA lo_lines_seperator TYPE REF TO zif_ocr_lines_seperator. CREATE OBJECT lo_char_sep TYPE zcl_ocr_characters_seperator. CREATE OBJECT lo_char_parser TYPE zcl_ocr_character_parser. CREATE OBJECT lo_file_access TYPE zcl_ocr_gui_file_access. CREATE OBJECT lo_lines_seperator TYPE zcl_ocr_lines_seperator. CREATE OBJECT lo_ocr TYPE zcl_ocr_parsing_algorithm EXPORTING io_character_parser = lo_char_parser io_characters_seperator = lo_char_sep io_file_access = lo_file_access io_lines_seperator = lo_lines_seperator.
However, the task of manually creating dependent objects in order to hand them over to a course grained object when it is created, can be automated. Think of the facts that are known:
- all low level interfaces like ZIF_OCR_CHARACTER_PARSER have one class that implements each of them
- instances of these classes could be created dynamically, as their constructors currently require no input parameter
- the course grained interface ZIF_OCR_PARSING_ALGORITHM also has a specific class that implements it
- however, its constructor requires some instances of the low level interfaces
- as these required parameters are instances of the low level interfaces, these dependencies could be satisfied automatically
As a brief summary, this is exactly what an IoC Container should do, also refered to as DI containers.
The registration of the interfaces and classes is done programmatically in the following example. However, it may be moved to customizing in order to “free” the code of having hard references to explizit class names
*alternative 2: IoC Container DATA lo_ioc_container TYPE REF TO /leos/if_ioc_container. lo_ioc_container = /leos/cl_ioc_container=>create_empty_instance( ). lo_ioc_container->add_implementer( iv_contract = 'zif_ocr_characters_seperator' iv_implementer = 'zcl_ocr_characters_seperator' ). lo_ioc_container->add_implementer( iv_contract = 'zif_ocr_character_parser' iv_implementer = 'zcl_ocr_character_parser' ). lo_ioc_container->add_implementer( iv_contract = 'zif_ocr_file_access' iv_implementer = 'zcl_ocr_gui_file_access' ). lo_ioc_container->add_implementer( iv_contract = 'zif_ocr_lines_seperator' iv_implementer = 'zcl_ocr_lines_seperator' ). lo_ioc_container->add_implementer( iv_contract = 'zif_ocr_parsing_algorithm' iv_implementer = 'zcl_ocr_parsing_algorithm' ).
No matter, how the classes have been registered, either in the code or by customizing, the caller now only needs to call the container and request an instance from it at startup:
DATA lo_ocr TYPE REF TO zif_ocr_parsing_algorithm. *create the container either by reference to customizing or by the registration coding shown above... *... lo_ocr ?= lo_ioc_container->get_implementer( iv_contract = 'zif_ocr_parsing_algorithm' ).