juillet
2014
Les constructeurs sont, en dodo, là où le langage devient impératif.
Le constructeur fait habituellement partie du baggage associé à la programmation orientée objet; cependant, ce n’est pas un composant essentiel de celle-ci. Il est tout à fait envisageable de faire de la programmation objet en se contentant de fonctions « fabrique » qui produisent des instances de classe en fonction de paramètres définis.
Ce qui distingue un constructeur de ces dernières, c’est que le constructeur agit sur une instance déjà allouée de la classe et peut (ou parfois, doit) déléguer partie de l’initialisation de l’objet à la superclasse.
Cette dernière propriété est la source de bien des soucis pour le programmeur orienté objet.
Prenons un exemple:
{
make construireA()
{
self.Init()
}
method Init()
{
F()
}
method F()
{
}
}
Dans cet exemple, le constructeur ne fait finalement rien. Mais une sous-classe peut avoir davantage d’attributs:
{
int x
make construireB(int x)
{
Super() // invoquer construireA
.self.x = x
}
method ^F()
{
case if (x = 0):
LancerOgive().
}
}
Dans B, le constructeur de A ne semble plus du tout anodin: puisque x
n’est pas encore initialisé, il prend la valeur par défaut 0 quand le constructeur de A appelle F(), et la fonction au nom inquiétant LancerOgive() est invoquée!
Même si le développeur prend bien soin que self.x
ne soit jamais 0 tant qu’il n’y a pas d’attaque en cours contre la sûreté nationale dans les méthodes de B, il est trop tard. Et le pire est qu’il n’a pas forcément de contrôle sur la classe A; l’appel à la méthode F() pourrait avoir été ajouté après que tous les tests du programme soient passés. C’est un problème général des langages orientés objet, dit « la superclasse fragile ».
Pour résoudre cela on pourrait interdire au constructeur de B d’appeler le constructeur de A sur self
, mais il suffirait de remplacer cet appel avec celui de self.Init() pour retomber sur le même problème. En fin de compte, ce qui est important est que la variable x
soit initialisée avant tout appel aux méthodes de A.
A noter qu’en dodo, il n’est pas toujours possible de provoquer ce problème. Les conditions qui le permettent sont:
- Le type A est polymorphique (venant de
class
) - Le type B réutilise le prototype de A (
def B
) - Le constructeur (ou une méthode) de A est invoqué avant que tous les attributs de B ne soient correctement initialisés.
Si ce n’était pas le cas, la fonction F() appelée dans Init() serait celle de A, puisque Init() n’aurait pas moyen de savoir que self
est en réalité de type B et non A. Alors LancerOgive() ne serait pas invoquée.
Autrement, B ne pourrait pas invoquer la moindre méthode ou fonction de A sur self
, puisque la structure des instances de B et de A n’ont potentiellement rien en commun. Cela est permis en dodo, et dans ce cas A est utilisé comme une interface que B implémente à sa façon.
Cela implique que ces attributs n’ont pas une valeur par défaut bien choisie.
J’ai mentionné le fait que dodo est un langage impératif par le biais des ses constructeurs. De fait, à l’intérieur d’un constructeur il est permis d’appeler d’autres constructeurs, appeler des méthodes avec effets de bord et aussi affecter de nouvelles valeurs aux attributs de l’objet, même si la classe n’est pas Mutable ou Editable (qui est une extension de Mutable).
Mais appeler des méthodes sur self
, alors que les attributs de self
peuvent encore changer, ne semble pas une bonne idée quand l’objet est sensé être constant. Les méthodes en question pourraient être déroutées quand les attributs de l’objet n’ont plus la valeur qu’ils prétendaient avoir. Pour cette raison dodo devrait imposer cette règle: affecter une nouvelle valeur aux attributs de l’objet n’est pas permis après que self
a été passé à une méthode ou un autre constructeur dans le constructeur de la classe. Cela s’applique uniquement aux objets qui ne sont pas Mutable.
Pour que le danger de la superclasse fragile affecte une classe dodo, il faut donc que la superclasse soit Polymorphic, qu’elle soit modifiée sans préavis, que la classe affectée soit Mutable, réutilise le prototype de la superclasse, n’ait pas de valeur par défaut bien choisie pour ses attributs et fasse appel à des méthodes ou constructeurs de la superclasse avant d’être complètement initialisée.
Il n’est pas très difficile de réunir tous ces ingrédients mais il est encore plus facile d’éviter le danger.