12
votes

Si un constructeur Perl renvoie un objet UNDEF ou un objet "invalide"?

question :

Qu'est-ce qui est considéré comme "meilleure pratique" - et pourquoi - de manipuler des erreurs dans un constructeur ?.

"Meilleure pratique" peut être une citation de Schwartz, ou 50% des modules CPAN Utilisez-le, etc.;; Mais je suis content d'une opinion bien motivée de quiconque même si cela explique pourquoi la meilleure pratique commune n'est pas vraiment la meilleure approche.

En ce qui concerne la vision du sujet (informé par le développement de logiciels à Perl depuis de nombreuses années), j'ai vu trois approches principales en matière de traitement des erreurs dans un module PERL (répertoriée du mieux à pire à mon avis):

  1. construire un objet, définissez un drapeau non valide (généralement " is_valid " méthode). Souvent couplé au message d'erreur de réglage via la manipulation des erreurs de votre classe.

    pros :

    • Permet la gestion des erreurs (comparée à d'autres appels de méthodes) car elle permet d'utiliser $ obj-> erreurs () appelle les appels après un mauvais constructeur comme après un autre appel à une autre méthode .

    • permet d'adopter des informations supplémentaires (par exemple> 1 erreur, des avertissements, etc ...)

    • permet une fonctionnalité légère "Redo" / "Fixme", en d'autres termes, si l'objet construit est très lourd, avec de nombreux attributs complexes 100% toujours ok et la seule raison pour laquelle ce n'est pas la raison. Valid est parce que quelqu'un est entré une date incorrecte, vous pouvez simplement faire " $ obj-> setdate () " au lieu de la surcharge de ré-exécutant de nouveau constructeur. Ce modèle n'est pas toujours nécessaire, mais peut être énormément utile dans la bonne conception.

      contre : aucun que je suis au courant.

    • retour " undef ".

      contre : ne peut atteindre aucun des avantages de la première solution (messages d'erreur par objet en dehors des variables globales et une capacité de "fixme légère" pour les objets lourds).

    • mourir à l'intérieur du constructeur. En dehors de certains cas de bord très étroits, je considère personnellement cela un choix terrible pour trop de raisons de faire une liste sur les marges de cette question.

    • mise à jour: juste pour être clair, je considère que la solution (sinon digne de conception et une excellente conception) d'avoir un constructeur très simple qui ne peut pas échouer du tout et une méthode d'initialiseur lourde dans laquelle toute vérification des erreurs a lieu à Soyez simplement un sous-ensemble de l'une des deux cas (si l'initialiseur définit les indicateurs d'erreur) ou le cas n ° 3 (si l'initialisateur meurt) aux fins de cette question. Évidemment, choisir un tel design, vous rejetez automatiquement l'option n ° 2.


6 commentaires

Je suis intéressé à entendre vos raisons pour lesquelles le n ° 3 est mauvais. Lancer une exception est un moyen standard de traiter une erreur dans un constructeur dans de nombreuses langues OO.


J'étais (et je suis toujours disponible) d'avoir les "raisons d'utiliser / non utiliser matrire comme une remise des exceptions dans les modules PERL" comme une question distincte. J'ai mis à jour la question avec cette note.


Inconvénients à # 1: avoir à appeler "is_valid" après la création de chaque objet. Je suis à peu près sûr que la plupart des incantations ressembleraient à "$ x-> is_valid ou mourir".


@Mathieu - Cela peut sembler gênant, mais je n'ai jamais saisi comment cela pourrait être plus ennuyeux que le piégeage des matrices en eval après chaque appel? En ce qui concerne "ou mourir", nous utilisons un système de modules assez compliqué pour des raisons variées, y compris le développement Web. Mourir partout sur la place dans une application Web n'est pas tout à fait ce que l'on envisagerait d'être convivial, sans oublier que le module lui-même pourrait envisager de «fatal», l'application globale peut être haussée comme un «avertissement».


Si vous envisagez une évaluation autour de chaque appel, vous le faites mal. Les exceptions à bulles de la pile et doivent être traitées au point le plus élevé, pas le plus bas. Plus de détails dans ma réponse.


C'est une question très intéressante et je pense que cela dépend du contexte. Si le constructeur est appelé dans un contexte scalaire tel que mon $ O = My :: Module-> Nouveau , il devrait renvoyer Undef , mais si le contexte est différent, tel que Enchaînant comme My :: Module-> Nouveau-> do_somethenghant-> plus_stuff , puis renvoyer Undef créera une erreur Perl indésirable. Mieux puis pour renvoyer un objet nul comme module :: générique :: null qui Je reviendra jusqu'à ce que le contexte devienne un scalaire puis renvoie undef .


4 Réponses :


5
votes

Je préfère:

  1. Faites aussi peu d'initialisation que possible dans le constructeur.
  2. croak avec un message informatif lorsque quelque chose ne va pas.
  3. Utilisez des méthodes d'initialisation appropriées pour fournir les messages d'erreur d'objet, etc.

    De plus, le retour Undef (au lieu de la récupération) est bon au cas où les utilisateurs de la classe ne se soucient pas de savoir pourquoi exactement la défaillance n'est survenue que si elles ont un objet valide ou non.

    Je méprise facile à oublier is_valid ou ajout de vérifications supplémentaires pour que les méthodes ne soient pas appelées lorsque l'état interne de l'objet n'est pas bien défini.

    Je le dis d'une perspective très subjective sans faire de déclarations sur les meilleures pratiques.


3 commentaires

SINAN - J'ai débattu de l'approche "Constructeur léger et d'initialisateur lourde" dans cette question, mais la pompeuse n'est pas assez différente de mentionner ... mais je suis d'accord c'est un design très digne que nous utilisons fréquemment.


@DVK Notez que je ne préconise pas un sous-initialisation de Colossal Initialiser . Je pense que décomposer les étapes en plusieurs méthodes est parfaitement acceptable.


Je fortement n'est pas d'accord sur la question de faire la même initialisation que possible dans le constructeur. Le travail d'un constructeur est, par définition, de construire une instance. Un constructeur correctement écrit effectue toute l'initialisation nécessaire et vous donne une instance prête à être utilisée sans avoir à craindre d'appeler d'abord les méthodes init.



8
votes

Cela dépend de la façon dont vous voulez que vos constructeurs se comportent.

Le reste de cette réponse passe dans mes observations personnelles, mais comme avec la plupart des choses Perl, les meilleures pratiques se résument vraiment à "Voici une façon de le faire, que vous pouvez prendre ou partir en fonction de vos besoins." Vos préférences que vous les avez décrites sont totalement valides et cohérentes, et personne ne devrait vous dire autrement.

i réellement préférez mourir si la construction échoue, car nous l'avons configuré pour que les seuls types d'erreurs pouvant survenir lors de la construction d'objets sont de grandes erreurs évidentes qui devraient arrêter l'exécution.

D'autre part, si vous préférez que cela ne se produise pas, je pense que je préférerais 2 sur 1, car il est tout aussi facile de rechercher un objet non défini car il est de vérifier la variable d'indicateur. Ce n'est pas c, donc nous n'avons pas de forte contrainte de frappe qui nous dit que notre constructeur doit renvoyer un objet de ce type. Donc, retourner Undef et vérifier cela pour établir le succès ou l'échec, est un excellent choix.

La «surcharge» de la défaillance de la construction est une contrepartie dans certains cas de bord (où vous ne pouvez pas échouer rapidement avant d'entraîner des frais généraux), donc pour ceux que vous préférez préférer la méthode 1. Encore une fois, cela dépend de la sémantique que vous avez défini pour la construction d'objets. Par exemple, je préfère faire une initialisation des poids lourds en dehors de la construction. En ce qui concerne la normalisation, je pense que vérifier si un constructeur renvoie un objet défini est aussi bon une norme que la vérification d'une variable d'indicateur.

Edit: En réponse à votre édition sur les initialisateurs Rejeter Case n ° 2, je ne vois pas pourquoi un initialiseur ne peut pas simplement retourner une valeur indiquant le succès ou l'échec plutôt que de définir une variable d'indicateur . En fait, vous voudrez peut-être utiliser les deux, en fonction de la quantité de détails que vous souhaitez sur l'erreur survenue. Mais il serait parfaitement valide pour un initialiseur de retourner true sur le succès et Undef sur une défaillance.


5 commentaires

Aurait dû dire C ++, comme nous parlons de la construction d'objets, mais le point était la dactylographie. ;)


Adam - Je prévois d'avoir une autre question sur "mourir" dans les modules Perl, mais il suffit de dire que l'une de mes objections est la bande passante de "matrice" - vous ne pouvez transmettre qu'une chaîne à l'appelant, par opposition à avoir un illimité quantité d'informations disponibles à partir de l'objet que vous avez construite (liste des erreurs, liste des avertissements, etc.). La raison pour laquelle je le mentionne est parce que c'est la même raison pour laquelle je n'aime pas le retour des UNDFD - pas de bande passante (vous ne renvoyez même pas de chaîne d'erreur).


@Dvk: hein? Mourir peut recevoir un objet. C'est une métaphore de manipulation d'exception standard dans Perl: Die Someexception-> Nouveau () . Ensuite, votre contexte d'appel vérifie $ @ pour l'objet et son contenu.


@DVK: En particulier, nous utilisons cette méthode pour nos constructeurs car nous pouvons mourir et analyser l'objet d'exception pour voir ce qui se passait pour provoquer l'échec. Bien que vos méthodes soient parfaitement du son (à l'aide de données / méthodes d'indicateur), évitez Die car vous ne pouvez obtenir qu'une chaîne n'est pas correcte: vous pouvez obtenir plus.


Découvrez Exception :: Classe comme moyen pratique de lancer des objets comme des exceptions. ( Search.cpan.org/perldoc?exception ::class )



2
votes

Premier les observations générales pompeuses:

  1. Le travail d'un constructeur devrait être: donné des paramètres de construction valides, renvoyer un objet valide.
  2. Un constructeur qui ne construit pas un objet valide ne peut pas effectuer son travail et constitue donc un candidat parfait pour la génération d'exceptions.
  3. Assurez-vous que l'objet construit est valide fait partie du travail du constructeur. Cession d'un objet connu à être mauvais et s'appuyant sur le client pour vérifier que l'objet est valide est un moyen sûr de liquider avec des objets non valides qui explosent dans des endroits éloignés pour des raisons non évidentes.
  4. vérifier que tous les arguments corrects sont en place avant que l'appel du constructeur est le travail du client.
  5. Exceptions fournissent une manière graine de propagation de l'erreur particulière survenue sans avoir à avoir un objet cassé à la main.
  6. renvoie UNDF; est toujours mauvais [1]
  7. Bilujdi 'Yichegh () QO'; Yiheugh ()!

    Maintenant à la question réelle, que je vais interpréter à signifier "Que faites-vous, Darch, considérez la meilleure pratique et pourquoi". Premièrement, je remarquerai que le retour d'une fausse valeur sur l'échec a une longue histoire de Perl (la plupart des œuvres de base de cette manière, par exemple), et beaucoup de modules suivent cette convention. Cependant, il s'avère que cette convention produit un code client de qualité inférieure et des modules plus récents s'éloigne. [2]

    [L'argument de support et les échantillons de code pour cela s'avèrent être l'affaire plus générale pour les exceptions qui ont poussé la création de Autodie , et je vais donc résister à la tentation de faire ce cas ici. Au lieu de:]

    avoir à vérifier que la création réussie est en fait plus onéreuse que de vérifier une exception à un niveau de manipulation d'exception approprié. Les autres solutions exigent que le client immédiat fasse plus de travail qu'il ne devait que nécessaire pour obtenir un objet, le travail qui n'est pas requis lorsque le constructeur échoue en lançant une exception. [3] Une exception est considérablement plus expressif que undef et d'expression également comme étant le passage d'un objet cassé à des fins de documentation des erreurs et les annotant à différents niveaux de la pile d'appels.

    Vous pouvez même obtenir l'objet partiellement construit si vous le transmettez à l'exception. Je pense que c'est une mauvaise pratique par ma conviction quant à ce que doit être le contrat d'un constructeur avec ses clients, mais le comportement est pris en charge. Maladroitement.

    Donc: un constructeur qui ne peut pas créer d'objet valide devrait lancer une exception le plus tôt possible. Les exceptions qu'un constructeur peut lancer doit être documentée des parties de son interface. Seuls les niveaux d'appel pouvant agir de manière significative à l'exception devraient même la chercher; Très souvent, le comportement de "si cette construction échoue, ne fais rien" est exactement correct.

    [1]: Par lequel je veux dire, je ne suis au courant d'aucun cas d'utilisation où retour; n'est pas strictement supérieur. Si quelqu'un m'appelle sur ceci, je pourrais avoir à ouvrir une question. Alors s'il vous plaît ne faites pas. ;)
    [2]: Performez mon souvenir extrêmement non scientifique des interfaces de module que j'ai lus au cours des deux dernières années, sous réserve des biais de sélection et de confirmation.
    [3]: Notez que jeter une exception nécessite toujours une gestion des erreurs, de même que les autres solutions proposées. Cela ne signifie pas envelopper chaque instanciation dans un EVAL sauf si vous voulez réellement faire une manipulation d'erreurs complexes autour de chaque construction (et si vous pensez que vous le faites, vous avez probablement tort). Cela signifie envelopper l'appel qui peut agir de manière significative à l'exception d'un eval .


2 commentaires

Je n'ai pas encore analysé cette réponse en détail mais je dois être très saisonnément en désaccord avec: "4. Vérifier que tous les arguments corrects sont en place avant que l'appel du constructeur est le travail du client". Ce 100% viole complètement tous les principes de OO en ce qui me concerne. Le seul code avec la connaissance et la fonctionnalité pour vérifier les arguments à un constructeur devrait être la classe où vit le constructeur.


Le travail du client consiste à s'assurer que cela n'utilise que l'interface de classe de la classe de la manière prise en charge, y compris la transmission de tous les arguments de l'interface requis pour fonctionner. Vérification de la Validité des arguments est un travail approprié pour l'invocataire, mais l'invocataire n'est pas obligée de faire quoi que ce soit si son interface est violée, et surtout pas transmettre un objet qui enfreint ses invariants de classe. parce que l'appelant n'a pas pu être dérangé pour rendre l'appel correctement.



5
votes

Je recommanderais-je contre # 1 simplement parce qu'il conduit à plus de code de traitement des erreurs qui ne sera pas écrit. Par exemple, si vous venez de rentrer simultanément, cela fonctionne bien. XXX PRE>

Mais si vous retournez un objet invalide ... P>

my $obj = eval { Class->new }
  or die "Construction failed: $@ and there were @{[ $@->num_frobnitz ]} frobnitzes";


3 commentaires

Les réponses à ce Q ont convaincu que de réévaluer mes perspectives sur l'utilisation de «matrice» ... Mon principale préoccupation est - y a-t-il des conséquences sur la performance pour utiliser des devales autour de beaucoup de code? (Je posterai probablement cela comme une question distincte, alors veuillez considérer cela un commentaire rhétorique par opposition à la demande d'informations :))


@DVK par "Evalles autour de tellement de code" Vous voulez dire que la quantité de fois que vous écrivez eval ou la quantité de code exécuté à l'intérieur d'un seul eval ? Si c'est le premier, tu le fais mal; Vous ne devriez pas écrire Eval Tant de fois que c'est une préoccupation de performance. Si c'est ce dernier, cela ne devrait pas avoir d'importance. N'oubliez pas que block eval et eval string sont des bêtes complètement différentes.


le dernier. Je ne connais pas les détails Eval Block autre que comment l'utiliser, donc pas certain. Je posterai le Q après quelques recherches.