Le constructeur ne doit généralement pas appeler de méthodes

12

J'ai expliqué à un collègue pourquoi un constructeur appelant une méthode peut être un contre-modèle.

exemple (dans mon C ++ rouillé)

class C {
public :
    C(int foo);
    void setFoo(int foo);
private:
    int foo;
}

C::C(int foo) {
    setFoo(foo);
}

void C::setFoo(int foo) {
    this->foo = foo
}

Je voudrais mieux motiver ce fait grâce à votre contribution supplémentaire. Si vous avez des exemples, des références de livres, des pages de blog ou des noms de principes, ils seraient les bienvenus.

Edit: je parle en général, mais nous codons en python.

Stefano Borini
la source
Est-ce une règle générale ou spécifique à des langues particulières?
ChrisF
Quelle langue? En C ++, c'est plus qu'un anti-pattern: parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.5
LennyProgrammers
@ Lenny222, l'OP parle de "méthodes de classe", ce qui - pour moi du moins - signifie des méthodes sans instance . Ce qui ne peut donc pas être virtuel.
Péter Török
3
@Alb En Java, tout va bien. Ce que vous ne devez pas faire, c'est passer explicitement thisà l'une des méthodes que vous appelez à partir du constructeur.
biziclop
3
@Stefano Borini: Si vous codez en Python, pourquoi ne pas montrer l'exemple en Python au lieu de C ++ rouillé? Veuillez également expliquer pourquoi c'est une mauvaise chose. Nous le faisons tout le temps.
S.Lott

Réponses:

26

Vous n'avez pas spécifié de langue.

En C ++, un constructeur doit se méfier lors de l'appel d'une fonction virtuelle, dans la mesure où la fonction réelle qu'il appelle est l'implémentation de classe. S'il s'agit d'une méthode virtuelle pure sans implémentation, ce sera une violation d'accès.

Un constructeur peut appeler des fonctions non virtuelles.

Si votre langage est Java où les fonctions sont généralement virtuelles par défaut, il est logique que vous deviez faire très attention.

C # semble gérer la situation comme vous vous y attendez: vous pouvez appeler des méthodes virtuelles dans les constructeurs et il appelle la version la plus finale. Donc en C # pas un anti-pattern.

Une raison courante pour appeler des méthodes à partir de constructeurs est que plusieurs constructeurs souhaitent appeler une méthode "init" commune.

Notez que les destructeurs auront le même problème avec les méthodes virtuelles, donc vous ne pouvez pas avoir une méthode de "nettoyage" virtuelle qui se trouve à l'extérieur de votre destructeur et vous attendre à ce qu'elle soit appelée par le destructeur de classe de base.

Java et C # n'ont pas de destructeurs, ils ont des finaliseurs. Je ne connais pas le comportement avec Java.

C # semble gérer correctement le nettoyage à cet égard.

(Notez que bien que Java et C # aient une récupération de place, cela ne gère que l'allocation de mémoire. Il y a un autre nettoyage dont votre destructeur a besoin pour faire qui ne libère pas de mémoire).

Vache à lait
la source
13
Il y a quelques petites erreurs ici. Les méthodes en C # ne sont pas virtuelles par défaut. C # a une sémantique différente de C ++ lors de l'appel d'une méthode virtuelle dans un constructeur; la méthode virtuelle sur le type le plus dérivé sera appelée, pas la méthode virtuelle sur la partie du type en cours de construction. C # appelle ses méthodes de finalisation des "destructeurs" mais vous avez raison, ils ont la sémantique des finaliseurs. Les méthodes virtuelles appelées dans les destructeurs C # fonctionnent de la même manière que dans les constructeurs; la méthode la plus dérivée est appelée.
Eric Lippert
@ Péter: Je voulais des méthodes d'instance. Désolé pour la confusion.
Stefano Borini
1
@Eric Lippert. Merci pour votre expertise sur C #, j'ai édité ma réponse en conséquence. Je ne connais pas ce langage, je connais très bien C ++ et Java moins bien.
CashCow
5
Je vous en prie. Notez que l'appel d'une méthode virtuelle dans un constructeur de classe de base en C # est toujours une très mauvaise idée.
Eric Lippert
Si vous appelez une méthode (virtuelle) en Java à partir d'un constructeur, elle invoquera toujours le remplacement le plus dérivé. Cependant, ce que vous appelez «comme vous vous y attendriez», c'est ce que j'appellerais déroutant. Parce que même si Java appelle le remplacement le plus dérivé, cette méthode ne verra que les initialiseurs classés traités, mais pas le constructeur de sa propre classe. Invoquer une méthode sur une classe qui n'a pas encore son invariant établi peut être dangereux. Je pense donc que C ++ a fait le meilleur choix ici.
5gon12eder
18

OK, maintenant que la confusion concernant les méthodes de classe vs les méthodes d' instance est éliminée, je peux donner une réponse :-)

Le problème n'est pas avec l'appel des méthodes d'instance en général à partir d'un constructeur; c'est avec l'appel de méthodes virtuelles (directement ou indirectement). Et la raison principale est que tandis qu'à l'intérieur du constructeur, l'objet n'est pas encore entièrement construit . Et surtout ses parties de sous-classe ne sont pas du tout construites pendant que le constructeur de la classe de base s'exécute. Son état interne est donc incohérent d'une manière dépendante de la langue, ce qui peut provoquer différents bogues subtils dans différentes langues.

C ++ et C # ont déjà été discutés par d'autres. En Java, la méthode virtuelle du type le plus dérivé sera appelée, mais ce type n'est pas encore initialisé. Donc, si cette méthode utilise des champs du type dérivé, ces champs peuvent ne pas encore être initialisés correctement à ce moment. Ce problème est abordé en détail dans Effecive Java 2nd Edition , Item 17: Design and document for inheritance or else prohibit it .

Notez qu'il s'agit d'un cas particulier du problème général de publication prématurée des références d'objets . Les méthodes d'instance ont un thisparamètre implicite , mais le passage thisexplicite à une méthode peut provoquer des problèmes similaires. Surtout dans les programmes concurrents où si la référence d'objet est publiée prématurément sur un autre thread, ce thread peut déjà appeler des méthodes dessus avant que le constructeur du premier thread ne se termine.

Péter Török
la source
3
(+1) "tandis qu'à l'intérieur du constructeur, l'objet n'est pas encore entièrement construit." Identique à "méthodes de classe vs instance". Certains langages de programmation le considèrent comme construit en entrant dans le constructeur, comme si le programmeur assignait des valeurs au constructeur.
umlcat
7

Je ne considérerais pas les appels de méthode ici comme un contre-motif en soi, plutôt comme une odeur de code. Si une classe fournit une resetméthode, qui renvoie un objet à son état d'origine, alors appeler reset()le constructeur est DRY. (Je ne fais aucune déclaration sur les méthodes de réinitialisation).

Voici un article qui pourrait aider à satisfaire votre appel à l'autorité: http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/

Il ne s'agit pas vraiment d'appeler des méthodes, mais de constructeurs qui en font trop. À mon humble avis, appeler des méthodes dans un constructeur est une odeur qui pourrait indiquer qu'un constructeur est trop lourd.

Cela est lié à la facilité avec laquelle il est possible de tester votre code. Les raisons incluent:

  1. Les tests unitaires impliquent beaucoup de création et de destruction - la construction doit donc être rapide.

  2. Selon ce que font ces méthodes, il peut être difficile de tester des unités de code discrètes sans s'appuyer sur une condition préalable (potentiellement non testable) configurée dans le constructeur (par exemple, obtenir des informations d'un réseau).

Paul Butcher
la source
3

Philosophiquement, le but du constructeur est de transformer un morceau brut de mémoire en une instance. Pendant l'exécution du constructeur, l'objet n'existe pas encore, donc appeler ses méthodes est une mauvaise idée. Vous ne savez peut-être pas ce qu'ils font en interne après tout, et ils peuvent à juste titre considérer que l'objet existe au moins (duh!) Lorsqu'ils sont appelés.

Techniquement, il n'y a peut-être rien de mal à cela, en C ++ et surtout en Python, c'est à vous de faire attention.

En pratique, vous devez limiter les appels aux seules méthodes qui initialisent les membres de la classe.


la source
2

Ce n'est pas une question d'ordre général. C'est un problème en C ++, en particulier lors de l'utilisation de l'héritage et des méthodes virtuelles, car la construction d'objet se fait à l'envers, et les pointeurs vtable sont réinitialisés avec chaque couche constructeur dans la hiérarchie d'héritage, donc si vous appelez une méthode virtuelle, vous pourriez ne pas finissent par obtenir celui qui correspond réellement à la classe que vous essayez de créer, ce qui va à l'encontre du but de l'utilisation de méthodes virtuelles.

Dans les langues avec une prise en charge OOP saine, qui définissent correctement le pointeur vtable dès le début, ce problème n'existe pas.

Mason Wheeler
la source
2

Il y a deux problèmes avec l'appel d'une méthode:

  • appeler une méthode virtuelle, qui peut soit faire quelque chose d'inattendu (C ++), soit utiliser des parties des objets qui n'ont pas encore été initialisées
  • appeler une méthode publique (qui devrait appliquer les invariants de classe), car l'objet n'est pas encore nécessairement complet (et donc son invariant peut ne pas tenir)

Il n'y a rien de mal à appeler une fonction d'assistance, tant qu'elle ne tombe pas dans les deux cas précédents.

Matthieu M.
la source
1

Je n'achète pas ça. Dans un système orienté objet, appeler une méthode est à peu près la seule chose que vous pouvez faire. En fait, c'est plus ou moins la définition de "orienté objet". Donc, si un constructeur ne peut appeler aucune méthode, que peut- il faire?

Jörg W Mittag
la source
Initialisez l'objet.
Stefano Borini
@Stefano Borini: Comment? Dans un système orienté objet, la seule chose que vous pouvez faire est d'appeler des méthodes. Ou de le regarder sous l'angle opposé: tout se fait en appelant des méthodes. Et "n'importe quoi" inclut évidemment l'initialisation de l'objet. Donc, si, pour initialiser l'objet, vous devez appeler des méthodes, mais que les constructeurs ne peuvent pas appeler de méthodes, comment un constructeur peut-il initialiser l'objet?
Jörg W Mittag
ce n'est absolument pas vrai que la seule chose que vous puissiez faire est d'appeler des méthodes. Vous pouvez simplement initialiser l'état sans aucun appel, directement aux internes de votre objet ... Le but du constructeur est de rendre un objet dans un état cohérent. Si vous appelez d'autres méthodes, celles-ci peuvent avoir des problèmes pour gérer un objet dans un état partiel, à moins qu'il ne s'agisse de méthodes spécialement conçues pour être appelées à partir du constructeur (généralement en tant que méthodes d'assistance)
Stefano Borini
@Stefano Borini: "Vous pouvez simplement initialiser l'état sans aucun appel, directement aux internes de votre objet." Malheureusement, lorsque cela implique une méthode, que faites-vous? Copiez et collez le code?
S.Lott
1
@ S.Lott: non, je l'appelle, mais j'essaie de le garder une fonction de module au lieu d'une méthode d'objet, et lui faire fournir des données de retour que je peux mettre dans l'état d'objet dans le constructeur. Si je dois vraiment avoir une méthode objet, je la rendrai privée et clarifierai que c'est pour l'initialisation, comme lui donner un nom propre. Cependant, je n'appellerais jamais une méthode publique pour définir l'état d'un objet à partir du constructeur.
Stefano Borini
0

Dans la théorie de la POO, cela ne devrait pas avoir d'importance, mais dans la pratique, chaque langage de programmation POO gère des constructeurs différents . Je n'utilise pas très souvent des méthodes statiques.

En C ++ et Delphi, si je devais donner des valeurs initiales à certaines propriétés ("membres de champ"), et que le code est très étendu, j'ajoute quelques méthodes secondaires comme extension des constructeurs.

Et n'appelez pas d'autres méthodes qui font des choses plus complexes.

En ce qui concerne les méthodes "getters" et "setters" des propriétés, j'utilise généralement des variables privées / protégées pour stocker leur état, ainsi que les méthodes "getters" & "setters".

Dans le constructeur, j'attribue des valeurs "par défaut" aux champs d'état des propriétés, SANS appeler les "accesseurs".

umlcat
la source