Je suis normalement un principe simple :
Tout ce qui est obligatoire pour l'existence et le comportement corrects de l'instance de la classe doit être transmis et réalisé dans le constructeur.
Toutes les autres activités sont réalisées par d'autres méthodes.
Le constructeur ne devrait jamais :
- utiliser d'autres méthodes de la classe dans le but d'utiliser le comportement de surcharge.
- agir sur ses attributs privés via des méthodes
Parce que j'ai appris à la dure que lorsque vous êtes dans le constructeur, l'objet est dans un état intermédiaire incohérent qui est trop dangereux à manipuler. Certains de ces comportements inattendus peuvent être attendus de votre code, d'autres peuvent provenir de l'architecture du langage et des décisions du compilateur. Ne devinez jamais, soyez prudent, soyez minimal.
Dans votre cas, j'utiliserais une méthode Parser::parseHtml(file). L'instanciation de l'analyseur et l'analyse sont deux opérations différentes. Lorsque vous instanciez un analyseur, le constructeur le met en condition pour effectuer son travail (analyse). Ensuite, vous utilisez sa méthode pour effectuer l'analyse syntaxique. Vous avez alors deux choix :
- Soit vous autorisez l'analyseur à contenir les résultats de l'analyse, et vous donnez aux clients une interface pour récupérer les informations analysées (par exemple, Parser::getFooValue()). Les méthodes renverront Null si vous n'avez pas encore effectué l'analyse syntaxique, ou si l'analyse syntaxique a échoué.
- ou votre Parser::parseHtml() renvoie une instance ParsingResult, contenant ce que le Parser a trouvé.
La deuxième stratégie vous offre une meilleure granularité, car l'analyseur est maintenant sans état, et le client doit interagir avec les méthodes de l'interface ParsingResult. L'interface de l'analyseur reste élégante et simple. Les éléments internes de la classe de l'analyseur auront tendance à suivre le modèle de l'interface de l'analyseur. Motif de construction .
Vous commentez : "J'ai l'impression que renvoyer une instance d'un parseur qui n'a rien analysé (comme vous le suggérez) est un constructeur qui a perdu son but. Il n'y a aucune utilité à initialiser un analyseur sans avoir l'intention d'analyser l'information. Donc, si l'analyse syntaxique doit se produire à coup sûr, devrions-nous analyser le plus tôt possible et signaler les erreurs dès le début, par exemple pendant la construction de l'analyseur syntaxique ? J'ai l'impression que l'initialisation d'un analyseur syntaxique avec des données invalides devrait entraîner l'émission d'une erreur."
Pas vraiment. Si vous renvoyez une instance d'un analyseur, bien sûr qu'il va analyser. Dans Qt, lorsque vous instanciez un bouton, il est évident qu'il va être affiché. Cependant, vous avez la méthode QWidget::show() à appeler manuellement avant que quelque chose ne soit visible pour l'utilisateur.
Tout objet en POO a deux préoccupations : l'initialisation, et l'opération (ignorez la finalisation, elle n'est pas en discussion pour le moment). Si vous gardez ces deux opérations ensemble, vous risquez à la fois des problèmes (avoir un objet incomplet qui fonctionne) et vous perdez en flexibilité. Il y a de nombreuses raisons pour lesquelles vous devriez effectuer une configuration intermédiaire de votre objet avant d'appeler parseHtml(). Exemple : supposons que vous voulez configurer votre Parser pour qu'il soit strict (donc échouer si une colonne donnée dans une table contient une chaîne au lieu d'un entier) ou permissif. Ou enregistrer un objet écouteur qui sera averti à chaque fois qu'un nouveau parsing est effectué ou terminé (pensez à la barre de progression de l'interface graphique). Il s'agit d'informations facultatives, et si votre architecture fait du constructeur la überméthode qui fait tout, vous vous retrouvez avec une énorme liste de paramètres et de conditions facultatives à gérer dans une méthode qui est par nature un champ de mines.
"La mise en cache ne devrait pas être la responsabilité d'un analyseur syntaxique. Si les données doivent être mises en cache, une classe de cache séparée doit être créée pour fournir cette fonctionnalité."
Au contraire. Si vous savez que vous allez utiliser la fonctionnalité d'analyse sur un grand nombre de fichiers, et qu'il y a une forte probabilité que les fichiers soient accédés et analysés à nouveau plus tard, il est de la responsabilité interne de l'analyseur d'effectuer une mise en cache intelligente de ce qu'il a déjà vu. Du point de vue du client, il est totalement inconscient si cette mise en cache est effectuée ou non. Il appelle toujours l'analyseur et obtient toujours un objet résultat, mais il obtient la réponse beaucoup plus rapidement. Je pense qu'il n'y a pas de meilleure démonstration de la séparation des préoccupations que celle-ci. Vous améliorez les performances sans aucun changement dans l'interface du contrat ou dans toute l'architecture du logiciel.
Cependant, notez que je ne préconise pas que vous devriez nunca utiliser un appel au constructeur pour effectuer le parsing. Je dis simplement que c'est potentiellement dangereux et que vous perdez en flexibilité. Il y a beaucoup d'exemples où le constructeur est au centre de l'activité réelle de l'objet, mais il y a aussi beaucoup d'exemples du contraire. Exemple (bien que biaisé, il découle du style C) : en python, je considérerais comme très bizarre quelque chose comme ceci
f = file()
f.setReadOnly()
f.open(filename)
au lieu de l'actuel
f = file(filename,"r")
Mais je suis sûr qu'il y a des bibliothèques d'accès IO qui utilisent la première approche (avec la seconde comme approche de syntaxe de sucre).
Modifier enfin, rappelez-vous que s'il est facile et compatible d'ajouter à l'avenir un constructeur "raccourci", il n'est pas possible de supprimer cette fonctionnalité si vous la trouvez dangereuse ou problématique. Les ajouts à l'interface sont beaucoup plus faciles que les suppressions, pour des raisons évidentes. Un comportement sucré doit être pondéré par rapport au support futur que vous devez fournir à ce comportement.