33 votes

Quelle quantité de travail doit faire le constructeur d'une classe d'analyse HTML ?

Quelle quantité de travail est-il raisonnable pour un constructeur d'objet de faire ? Doit-il se contenter d'initialiser les champs et ne pas effectuer d'opérations sur les données, ou est-il acceptable qu'il effectue certaines analyses ?

Le contexte : J'ai écrit une classe qui est responsable de l'analyse d'une page HTML et du retour de diverses informations basées sur l'information analysée. La conception de la classe est telle que le constructeur de la classe effectue l'analyse syntaxique et lève une exception si une erreur se produit. Une fois l'instance initialisée, les valeurs analysées sont disponibles sans autre traitement via les accesseurs. Quelque chose comme :

public class Parser {

    public Parser(final String html) throws ParsingException {
        /* Parsing logic that sets private fields */
        /* that throws an error if something is erroneous.*/
    }

    public int getNumOfWhatevers() { return private field; }
    public String getOtherValue()  { return other private field; }
}

Après avoir conçu la classe, j'ai commencé à me demander si c'était une pratique OO correcte. Le code d'analyse syntaxique devrait-il être placé dans une classe void parseHtml() et les accesseurs ne renvoient des valeurs valides que lorsque cette méthode est appelée ? J'ai l'impression que mon implémentation est correcte, mais je ne peux m'empêcher de penser que certains puristes de l'OO pourraient la trouver incorrecte pour une raison ou une autre et qu'une implémentation telle que la suivante serait meilleure :

public class Parser {

    public Parser(final String html) {
        /* Remember html for later parsing. */
    }

    public void parseHtml() throws ParsingException { 
        /* Parsing logic that sets private fields */
        /* that throws an error if something is erroneous.*/
    }

    public int getNumOfWhatevers() { return private field; }
    public String getOtherValue()  { return other private field; }
}

Existe-t-il des cas où le code d'initialisation, tel que l'analyse des informations, ne devrait pas se trouver dans le constructeur, ou suis-je simplement stupide et en train de me remettre en question ?

Quels sont les avantages/inconvénients de séparer l'analyse syntaxique du constructeur ?

Réflexions ? Des idées ?

36voto

Stefano Borini Points 36904

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 :

  1. 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é.
  2. 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.

18voto

S.Lott Points 207588

"Le code d'analyse syntaxique doit-il être placé dans une méthode void parseHtml() et les accesseurs ne doivent-ils retourner des valeurs valides qu'une fois cette méthode appelée ?"

Oui.

"La conception de la classe est telle que le constructeur de la classe fait le parsing"

Cela empêche la personnalisation, l'extension et, surtout, l'injection de dépendances.

Il y aura des moments où vous voudrez faire ce qui suit

  1. Construire un analyseur syntaxique.

  2. Ajouter des fonctionnalités à l'analyseur syntaxique : Règles commerciales, filtres, meilleurs algorithmes, stratégies, commandes, etc.

  3. Parse.

En général, il est préférable d'en faire le moins possible dans un constructeur afin d'être libre de l'étendre ou de le modifier.


Modifier

"Les extensions ne pourraient-elles pas simplement analyser les informations supplémentaires dans leurs constructeurs ?"

Seulement s'ils n'ont pas de caractéristiques qui doivent être injectées. Si vous voulez ajouter des fonctionnalités - par exemple une stratégie différente pour construire l'arbre d'analyse - vos sous-classes doivent également gérer l'ajout de cette fonctionnalité avant d'effectuer l'analyse. Cela peut ne pas se résumer à un simple super() parce que la superclasse en fait trop.

"Aussi, le parsing dans le constructeur me permet d'échouer tôt".

En quelque sorte. Échouer pendant la construction est un cas d'utilisation bizarre. Échouer pendant la construction rend difficile la construction d'un analyseur comme celui-ci...

class SomeClient {
    parser p = new Parser();
    void aMethod() {...}
}

En général, un échec de construction signifie que vous n'avez plus de mémoire. Il y a rarement une bonne raison d'attraper les exceptions de construction car vous êtes de toute façon condamné.

Vous êtes obligé de construire l'analyseur syntaxique dans un corps de méthode parce qu'il a des arguments trop complexes.

En bref, vous avez supprimé les options des clients de votre analyseur syntaxique.

"Il est déconseillé d'hériter de cette classe pour remplacer un algorithme."

C'est drôle. Sérieusement. C'est une affirmation scandaleuse. Aucun algorithme n'est optimal pour tous les cas d'utilisation possibles. Souvent, un algorithme très performant utilise beaucoup de mémoire. Un client peut vouloir remplacer l'algorithme par un algorithme plus lent qui utilise moins de mémoire.

On peut prétendre à la perfection, mais c'est rare. Les sous-classes sont la norme, pas une exception. Quelqu'un améliorera toujours votre "perfection". Si vous limitez leur capacité à sous-classer votre analyseur, ils l'abandonneront simplement pour quelque chose de plus flexible.

"Je ne vois pas la nécessité de l'étape 2 telle que décrite dans la réponse."

Une déclaration audacieuse. Les dépendances, les stratégies et les modèles de conception d'injection connexes sont des exigences courantes. En effet, ils sont tellement essentiels pour les tests unitaires qu'une conception qui les rend difficiles ou complexes s'avère souvent être une mauvaise conception.

Limiter la possibilité de sous-classer ou d'étendre votre analyseur est une mauvaise politique.

Ligne de fond .

Ne supposez rien. Ecrivez une classe avec le moins d'hypothèses possible sur ses cas d'utilisation. L'analyse syntaxique au moment de la construction fait trop d'hypothèses sur les cas d'utilisation du client.

5voto

duffymo Points 188155

Un constructeur devrait faire tout ce qui est nécessaire pour mettre cette instance dans un état exécutable, valide, prêt à l'emploi. Si cela signifie une validation ou une analyse, je dirais que cela a sa place ici. Faites juste attention à ce que le constructeur fait.

Il peut y avoir d'autres endroits dans votre conception où la validation s'applique également.

Si les valeurs d'entrée proviennent d'une interface utilisateur, je dirais qu'elle devrait avoir un rôle à jouer pour garantir la validité des entrées.

Si les valeurs d'entrée sont extraites d'un flux XML entrant, je penserais à utiliser des schémas pour les valider.

5voto

Rodrick Chapman Points 2981

Je passerais probablement juste assez pour initialiser l'objet et ensuite avoir une méthode 'parse'. L'idée est que les opérations coûteuses doivent être aussi évidentes que possible.

3voto

Samuel Carrijo Points 9056

Vous devriez essayer d'éviter que le constructeur ne fasse un travail inutile. Au final, tout dépend de ce que la classe doit faire et de la façon dont elle doit être utilisée.

Par exemple, tous les accesseurs seront-ils appelés après la construction de votre objet ? Si ce n'est pas le cas, vous avez traité des données inutilement. De plus, il y a un plus grand risque de lancer une exception "insensée" (oh, en essayant de créer l'analyseur, j'ai eu une erreur parce que le fichier était mal formé, mais je ne lui ai même pas demandé d'analyser quoi que ce soit...).

En y réfléchissant, il se peut que vous ayez besoin d'accéder à ces données rapidement après la construction de l'objet, mais que la construction de l'objet prenne beaucoup de temps. Dans ce cas, cela pourrait être correct.

Quoi qu'il en soit, si le processus de construction est compliqué, je suggérerais d'utiliser une schéma créatif (usine, constructeur).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X