Do not call overridable methods from constructor. This is a general risk-mitigation rule for most of today's object-oriented languages, among them Java and TypeScript. Not so widely known, but quite subtle and hard to understand.
In TypeScript, overridable methods are public
or protected
.
In Java you could avoid the pitfall by making these methods final
,
but there is nothing like that in TypeScript.
Now, what exactly is the pitfall?
Example
The problem occurs not always when you call an overridable method from constructor, just when that overridable uses instance fields of its own class that are expected to have been initialized to a certain value.
Following example classes should make this clear.
They represent the idea of encapsulating a Name
string.
Let's assume that FirstName
and LastName
are both names,
but with different semantics, one being a different default value.
(I left out all other semantics, so the classes may not look really useful.)
The abstract super-class Name
leaves it up to sub-classes
to define a default-value for the encapsulated value
string.
It does so by declaring a protected abstract getDefault()
method.
Mind that TypeScript, like Java, requires a class to be abstract when it contains an abstract method,
and sub-classes are forced to implement it, or be abstract again.
FirstName
extends Name
.
The programmer decided to define the default in an instance field constant.
Looks like nothing can break that code, right?
Test
Finally here is a test for these quite simple looking classes. You can execute it using the HTML page that I introduced in a recent Blog. Script references are on bottom of the page.
This first imports the classes to test.
Then it declares two external functions that are provided by
test.html
(thus it depends on its test-executor).
It outputs a title, and then constructs FirstName
and LastName
objects,
executing the same assertion on both: check that getValue()
returns the correct default value.
Mind that, due to the imports, all TS files must be in the same directory. You can compile them by:
tsc -t ES6 *.ts
Would you expect that the test succeeds?
When you load the test.html
page into your browser, you will see this result:
Don't Call Overridables From Constructor
firstName.getValue() is expected to be '(No Firstname)': 'undefined'lastName.getValue() is expected to be '(No Lastname)': 'undefined'
Both tests failed because the real value was undefined
instead of the expected default name!
Executing an override before its owning object was initialized
The explanation of this pitfall is the object-initialization control flow.
-
A sub-class calls the constructor of its super-class before its own instance fields have been initialized.
In other words,
firstName.defaultValue
is still undefined when theName
constructor starts to work. -
The
Name
constructor calls thegetDefault()
method, which is overridden and thus control goes back to theFirstName
object. -
The
getDefault()
method inFirstName
returns the value of the not-yet-initialized instance fieldthis.defaultValue
, which isundefined
. -
The
value
field inName
now has been set toundefined
by theName
constructor. That was the pitfall. -
As soon as the
Name
constructor has terminated, the object-initialization ofFirstName
gets started, before constructor execution. Now "(No Firstname)" is assigned to the instance fieldfirstName.defaultValue
, but too late for getting intosuper.value
.
Conclusion
There are several ways to fix this.
One is to provide the default value as static
field.
Another one is to hardcode the default inside the getDefault()
override implementation.
The basic problem is the initialization order of object instances.
Constructors are not really object-oriented, they are a legacy from structured languages.
Whatever is inside a constructor is hardly overridable.
In my next Blog I will show how this can be fixed without static fields or hardcoding values inside methods.
Keine Kommentare:
Kommentar veröffentlichen