ABAP Trapdoors: The Myth of the Instance Constructor
Arguably one of the most misleading field values in the entire ABAP Workbench is this one:
It suggests that the constructor is just another instance method that is just called automatically by the kernel whenever an instance is created – but this is fundamentally wrong. Let me show you a small example for what can go wrong if you happen to trigger this trapdoor.
(Note that I’m using local classes here to cut down on the screenshots, but of course the same applies to global classes as well.)
Let’s create a class that performs some fancy stuff during its initialization. Because we’re good citizens and want to keep things reusable, we’ll encapsulate the fancy stuff in its own method and call it from the constructor.
CLASS lcl_super DEFINITION. PUBLIC SECTION. METHODS constructor. PROTECTED SECTION. METHODS initialize_me. ENDCLASS. CLASS lcl_super IMPLEMENTATION. METHOD constructor. WRITE: / 'entering LCL_SUPER CONSTRUCTOR'. initialize_me( ). WRITE: / 'leaving LCL_SUPER CONSTRUCTOR'. ENDMETHOD. METHOD initialize_me. WRITE: / 'executing fancy stuff in LCL_SUPER INITIALIZE_ME'. ENDMETHOD. ENDCLASS.
Creating an instance of lcl_super yields a rather unspectacular output:
entering LCL_SUPER CONSTRUCTOR executing fancy stuff in LCL_SUPER INITIALIZE_ME leaving LCL_SUPER CONSTRUCTOR
Now let’s add a subclass with its own constructor:
CLASS lcl_sub DEFINITION INHERITING FROM lcl_super. PUBLIC SECTION. METHODS constructor. ENDCLASS. CLASS lcl_sub IMPLEMENTATION. METHOD constructor. WRITE: / 'entering LCL_SUB CONSTRUCTOR'. super->constructor( ). WRITE: / 'leaving LCL_SUB CONSTRUCTOR'. ENDMETHOD. ENDCLASS.
The results aren’t very surprising either:
entering LCL_SUB CONSTRUCTOR entering LCL_SUPER CONSTRUCTOR executing fancy stuff in LCL_SUPER INITIALIZE_ME leaving LCL_SUPER CONSTRUCTOR leaving LCL_SUB CONSTRUCTOR
Now someone might think that the fancy stuff in initialize_me isn’t fancy enough or needs a different flavor of fancyness – whatever, since it’s a protected method, we can just go ahead and redefine it. Still being the good citizens that we started off as, we ensure that the inherited implementation is called from the redefinition:
CLASS lcl_sub DEFINITION INHERITING FROM lcl_super. PUBLIC SECTION. METHODS constructor. PROTECTED SECTION. METHODS initialize_me REDEFINITION. ENDCLASS. CLASS lcl_sub IMPLEMENTATION. METHOD constructor. WRITE: / 'entering LCL_SUB CONSTRUCTOR'. super->constructor( ). WRITE: / 'leaving LCL_SUB CONSTRUCTOR'. ENDMETHOD. METHOD initialize_me. WRITE: / 'entering LCL_SUB INITIALIZE_ME'. WRITE: / 'performing more fancy stuff'. super->initialize_me( ). WRITE: / 'setting fancyness level to 11'. WRITE: / 'leaving LCL_SUB INITIALIZE_ME'. ENDMETHOD. ENDCLASS.
Looking good? Okay then, let’s give it a try:
entering LCL_SUB CONSTRUCTOR entering LCL_SUPER CONSTRUCTOR executing fancy stuff in LCL_SUPER INITIALIZE_ME leaving LCL_SUPER CONSTRUCTOR leaving LCL_SUB CONSTRUCTOR
What the …?
Okay, back to the drawing board. What went wrong here? A quick look into the documentation reveals this nugget:
In a constructor method, the methods of the subclasses of the class are not visible. If an instance constructor calls an instance method of the same class using the implicit self-reference me->, the method is called as it is implemented in the class of the instance constructor, and not in any redefined form that may occur in the subclass you want to instantiate. This is an exception to the rule that states that when you call instance methods, the system always calls the method as it is implemented in the class to whose instance the reference is pointing.
The rationale behind this is that the constructor is there to ensure that an instance of a class is initialized correctly and completely before any other operation is attempted. Calling a redefined method would circumvent this principle – the system would execute a method of the subclass although the construction of that class was not yet completed.
The irony of this problem is that the compiler will even tell you exactly this if you try to call an abstract protected method from within a constructor:
However, it won’t be able to prevent the issue we’ve encountered above. It’s a single-pass compiler that doesn’t know about the subclasses that redefine the protected methods when compiling the superclass, and it doesn’t know about the superclass calling redefined methods from within the constructor when compiling the subclasses. The compiler would also have to follow the entire call graph to ensure that no redefined method is called by a non-redefined method that is called by the constructor, and since the exact static call graph is undecidable, that just won’t happen.
So what are our options? The most basic to prevent this from happening is to make the method private. If the subclasses still need to be able to call the method, make it final – this will at least prevent subclasses from overriding it. And think about whether you really need to call this method from the constructor – in most cases, there’s a different way of structuring the code that will avoid this problem altogether.
I have always found it challenging to explain these eccentricities to developers coming from a "pure"(if there ever was one) Object Oriented background, but what's ABAP or ABAP Objects without the idiosyncrasies?
Well done and keep up the good work!
I especially appreciate the fact that this blog is concise and includes an example that can be easily copied/tested - nice work.
I especially appreciate the fact that this blog is concise and includes an example that can be easily copied/tested - nice work.
it's almost embarassing to say, but I've already run into this thing twice. I hope that publishing an article about it will make it finally stick in my memory...
Volker
Thanks for this interesting blog.
One of my colleague also showed me something strange and I don’t understand why it’s possible.
You can use the attribute of an object, which is still initial as importing parameter during object creation. Normally you would get a short dump, that the object is initial, but it seems during object creation it’s possible.
Example:
Class CL_GUI_ALV_GRID CONSTRUCTOR
*... (6)create variant object
if m_cl_variant is initial.
create object m_cl_variant
exporting
it_outtab = mt_outtab
it_fieldcatalog = m_cl_variant->mt_fieldcatalog
it_sort = m_cl_variant->mt_sort
it_filter = m_cl_variant->mt_filter
it_grouplevels_filter = m_cl_variant->mt_grouplevels_filter
is_variant = m_cl_variant->ms_variant
i_variant_save = m_cl_variant->m_variant_save
i_variant_default = m_cl_variant->m_variant_default
is_total_options = m_cl_variant->ms_total_options
is_layout = m_cl_variant->ms_layout
is_print = m_cl_variant->ms_print
i_www_active = m_www
i_cl_alv_grid = me.
Any idea about this why it's possible?
Thanks,
Peter
thank you for your comment. I will take a look at this and probably mention this in one of the next articles.
Volker
I'm looking forward to your future blogs.
Peter
The best way to achieve something similar is to use some kind of factory method and setting the class as CREATE PRIVATE. So, instead of putting the call to the (redefined) initialize_me method in the constructor itself, you have this method:
METHOD factory.
CREATE OBJECT rr_instance.
rr_instance->initialize_me( ).
ENDMETHOD.
As a client of the class, you do not perform a direct CREATE OBJECT, but get a new instance via the FACTORY method.
(if you know i.s.h.med, you already know this concept with the CL_ISHMED* classes and their CREATE / LOAD and COMPLETE_CONSTRUCTION methods)
Regards
you're right about the factory method, but it has its own drawbacks because you can't redefine static methods. Since you always have to call the factory method of a specific class, redefining the initialize_me class alone won't help you, you also have to copy the factory method.
In IS-H / i.s.h.med, factory methods and even factory classes are used for another reason - depending on which license is installed, different classes are instantiated...
Volker
CLASS lcl_super DEFINITION CREATE PRIVATE.
PUBLIC SECTION.
CLASS-METHODS:
factory
IMPORTING i_type TYPE i
RETURNING value(rr_instance) TYPE REF TO lcl_super.
PROTECTED SECTION.
METHODS:
initialize_me.
ENDCLASS. "lcl_super DEFINITION
CLASS lcl_super IMPLEMENTATION.
METHOD factory.
DATA: l_class TYPE seoclsname.
CASE i_type.
WHEN 1.
l_class = 'LCL_SUB1'.
WHEN OTHERS.
l_class = 'LCL_SUPER'.
ENDCASE.
CREATE OBJECT rr_instance TYPE (l_class).
rr_instance->initialize_me( ).
ENDMETHOD. "factory
METHOD initialize_me.
WRITE: /'','Executing in SUPER...'.
ENDMETHOD. "initialize_me
ENDCLASS. "lcl_super IMPLEMENTATION
CLASS lcl_sub1 DEFINITION INHERITING FROM lcl_super.
PROTECTED SECTION.
METHODS:
initialize_me REDEFINITION.
ENDCLASS. "lcl_sub1 DEFINITION
CLASS lcl_sub1 IMPLEMENTATION.
METHOD initialize_me.
super->initialize_me( ).
WRITE: /'','...and in SUB1'.
ENDMETHOD. "initialize_me
ENDCLASS. "lcl_sub1 IMPLEMENTATION
DATA: gr_instance TYPE REF TO lcl_super.
START-OF-SELECTION.
gr_instance = lcl_super=>factory( 0 ).
ULINE.
gr_instance = lcl_super=>factory( 1 ).
ULINE.
(hope that was readable, unfortunately blog comments do not support the {code} tag I think, so indentation is lost)
However, I'd like to know what alternative you meant in your blog by "different way of structuring the code".
Looking forward also for your next trapdoor post!
Regards
yes, that's a great way to bypass this issue, but it only works if your factory statically knows about all of the subtypes it has to create. In a classical framework scenario where the factory developer does not know about any subclasses that the users of the framework might develop, this is not the case.
The next obvious alternative would be to pass the class name as a parameter. This looks intriguing, but it has (at least) the drawback that it's up to the factory to ensure that the class name passed is a valid class name and the class is a subclass of the superclass. That's definitely possible, but not trivial and rather time-consuming. Either this or you accept the short dumps that will occur if someone tells your factory to create an instance of CL_GUI_FOOLBAR (typo intended ;-)).
If you really need this kind of flexibility, I usually use the Enhancement Framework to decouple the framework classes from the implementations. I usually supply a BAdI definition that can have multiple active implementations and whose implementations act as factories.
Volker
Even though there seems to be no definitive solution, I was just trying to think of reasonable alternatives (which have their own drawbacks so far). If anyone comes up with others, keep them coming!
All the subclasses will include said interface, and the factory reads the list of the subclasses from the database.
I like better the idea of a virtual method and an empty constructor, plus an internal table with a dummy instance of each class only good for said method (no need for the regular initialization) and a second method that returns a priority.But that's even more complicated.
This is an effect we see in almost all OOP languages -- C++ and Java have the same behaviour as ABAP. (I've never seen the Java compiler or an IDE warn you about it either, even though it has a multipass compiler and the most advanced IDEs around.) C# _does_ call the overridden virtual function of the subtype, even though the instance is not fully constructed (which has its own caveats and drawbacks).
I guess the philosophical debate as to which path to choose is only a non-issue in languages like SmallTalk that eschew constructors altogether. ("Real" OOP languages, as Alan Kay would tell you.) The "different way of structuring the code" that you mention is the only way in those languages.
Constructor methods are just for assignment purposes to the attributes that the class holds
First, there's nothing controversial about calling static methods in constructors (though, admittedly, that's probably not what you meant). More pertinently, if two or more of the constructors and methods of your class share common functionality (imagine a "reset()" method, that might contain constructor-like functionality), it can be extremely useful to define that in properly encapsulated instance methods. (ABAP doesn't support multiple instance constructors in a single class, but your comment referred to OOP in general.)
More generally, it's not true that constructor methods are just for assignment purposes -- while it's convenient, there are actually (maybe surprisingly) people who would argue against using them like that. Constructors are for setting up/initializing objects, which can often be more complex than member variable-setting. In light of that, it's also conceivable that the behaviour of a constructor might be sufficiently complex to benefit from decomposition into several methods.
I'd assume that these "basic books" would be about "Real" OOP languages designed by Alan Kay, as JAmes Geddes has already pointed out. There are many examples where a lot more than just simple attribute initialization has to happen in order to initialize an instance, and I can see absolutely no harm in splitting the constructor into several methods. It might even be advisable to do this to re-use code. Let's say, for example, you want to ensure that an attribute foo always has a valid value, and that checking the value is a non-trivial task. You could encapsulate the checking into a method that throws an exception in case of an error and call this method both from the constructor as well as the setter method. You just have to ensure that this method is either private or final to prevent the kind of issue I described in the article.
Volker
CLASS lcl_super DEFINITION.
PUBLIC SECTION.
METHODS constructor.
PROTECTED SECTION.
METHODS initialize_me.
ENDCLASS.
CLASS lcl_super IMPLEMENTATION.
METHOD constructor.
WRITE: / 'entering LCL_SUPER CONSTRUCTOR'.
initialize_me( ).
me->initialize_me( ).
*the two lines above do the same job
*this me-> is explained by SAP help:
*http://help.sap.com/saphelp_nw70/helpdata/en/dd
*/4049c40f4611d3b9380000e8353423/frameset.htm
*In a constructor method, the methods of the *subclasses of the class are not visible.
*If an instance constructor calls an instance *method of the same class using the
*implicit self-reference me->, the method is *called as it is implemented in the class
*of the instance constructor, and not in any *redefined form that may occur in the
*subclass you want to instantiate. This is an *exception to the rule that states
*that when you call instance methods, the system *always calls the method as it is
*implemented in the class to whose instance the *reference is pointing.
WRITE: / 'leaving LCL_SUPER CONSTRUCTOR'.
ENDMETHOD.
METHOD initialize_me.
WRITE: / 'executing fancy stuff in LCL_SUPER INITIALIZE_ME'.
ENDMETHOD.
ENDCLASS.
CLASS lcl_sub DEFINITION INHERITING FROM lcl_super.
PUBLIC SECTION.
METHODS constructor.
PROTECTED SECTION.
METHODS initialize_me REDEFINITION.
ENDCLASS.
CLASS lcl_sub IMPLEMENTATION.
METHOD constructor.
WRITE: / 'entering LCL_SUB CONSTRUCTOR'.
* initialize_me( ).
* me->initialize_me( ).
*if you remove * from the two lines above
*system sill give an error and say
*in the constructor method, you can only access *instance attributes,instance methods, or "ME"
*after calling the constructor of the
*superclass (SUPER->CONSTRUCTOR).
super->constructor( ).
me->initialize_me( ).
*thie me-> points to the instance of class *lcl_sub
WRITE: / 'leaving LCL_SUB CONSTRUCTOR'.
ENDMETHOD.
METHOD initialize_me.
WRITE: / 'entering LCL_SUB INITIALIZE_ME'.
WRITE: / 'performing more fancy stuff'.
super->initialize_me( ).
WRITE: / 'setting fancyness level to 11'.
WRITE: / 'leaving LCL_SUB INITIALIZE_ME'.
ENDMETHOD.
ENDCLASS.
DATA instance TYPE REF TO LCL_SUB.
START-OF-SELECTION.
CREATE OBJECT instance.
I'm not sure I get your point. From what I can see, the comments are partially misleading - for example, the error message you designated as "silly" is simply the consequence of having to initialize the superclass completely before performing any action on the subclass. Did I miss something essential here?
Volker
I have seen similair quirky behaviors over time and most of these have been addressed, let's hope this is addressed as ABAP moves closer and closer to becoming a fully compliant OO language.
Christiaan
I don't see this as a quirk - it's simply a design decision whether to resolve redefinitions during construction. I don't think that this will be changed - in fact, I sincerely hope it won't because that would cause a different kind of trouble altogether...
Volker
Apologies for the literary fopa, more like a quirky design decision then. My point is that a framework or language should not limit bad practice, a developer should. In this case the language should allow you to call overridden methods from the constructor that is how inheritance works
To prevent a subclass calling or overriding these methods, the developer should make them final or private. But you should have the choice....
Anyway still good to know it is there and one should avoid any design decision where a feature such as this is required.
Christiaan
Thanks for sharing details about intricate mistake that can happen in coding. The solution and suggestion approach is good.
Regards,
Prathap
I just read your blog and I found it very worthwhile. It is something to keep me in my memory on future assignments.
Kind Regards,
Rae Ellen Woytowiez
Hello Volker,
Another option could be not use the CONSTRUCTOR when you need type of fancy design. Instead of that create a separate method which you need to call as soon as you instantiate the object. Here the execution doesn't have to understand the path of progress as its very well established by Object instantiation.
IMHO, it shouldn't be categorized as Trapdoor. Its basic design flaw as you are using constructor for different purpose.
Regards,
Naimesh Patel
Naimesh,
this is no fancy design. A constructor is used to initialize an object. If that initialization is more complex than the average "g_foo = i_foo" - for whatever reason - then there's no reason not to call methods from within the controller, especially if these methods already exist for other purposes. If they do, then there's a good chance that they are protected or even public and thus can be overriden - with the results I described above. This is no "different purpose", it's just a possible trap that you need to be aware of.
Volker