web-dev-qa-db-fra.com

la complexité d'un constructeur

J'ai une discussion avec mon collègue sur la quantité de travail qu'un constructeur peut faire. J'ai une classe, B qui nécessite en interne un autre objet A. L'objet A est l'un des quelques membres dont la classe B a besoin pour faire son travail. Toutes ses méthodes publiques dépendent de l'objet interne A. Les informations sur l'objet A sont stockées dans la base de données, j'essaie donc de les valider et de les obtenir en les recherchant sur la base de données dans le constructeur. Mon collègue a souligné que le constructeur ne devrait pas faire beaucoup de travail autre que la capture des paramètres du constructeur. Étant donné que toutes les méthodes publiques échoueraient de toute façon si l'objet A n'est pas trouvé en utilisant les entrées du constructeur, j'ai soutenu qu'au lieu de permettre la création d'une instance et l'échec ultérieur, il est en fait préférable de lancer tôt le constructeur. Toutes les classes ne le font pas, mais j'ai trouvé que le constructeur StreamWriter lève également s'il a un problème pour ouvrir un fichier et ne retarde pas la validation jusqu'au premier appel à Write.

Que pensent les autres? J'utilise C # si cela fait une différence.

Lecture Y a-t-il jamais une raison de faire tout le travail d'un objet dans un constructeur? je me demande si aller chercher l'objet A en allant dans DB fait partie de "toute autre initialisation nécessaire pour rendre l'objet prêt à l'emploi" car si l'utilisateur a transmis des valeurs erronées au constructeur, je ne serais pas en mesure d'utiliser l'une de ses méthodes publiques.

Les constructeurs doivent instancier les champs d'un objet et effectuer toute autre initialisation nécessaire pour rendre l'objet prêt à l'emploi. Cela signifie généralement que les constructeurs sont petits, mais il existe des scénarios où cela représenterait une quantité de travail considérable.

20
Taein Kim

Votre question est composée de deux parties complètement distinctes:

Dois-je lever l'exception du constructeur ou dois-je faire échouer la méthode?

Il s'agit clairement de l'application du principe Fail-fast . Faire échouer un constructeur est beaucoup plus facile à déboguer que d'avoir à trouver pourquoi la méthode échoue. Par exemple, vous pouvez obtenir l'instance déjà créée à partir d'une autre partie du code et vous obtenez des erreurs lors de l'appel de méthodes. Est-il évident que l'objet est mal créé? Non.

Quant au problème "envelopper l'appel dans try/catch". Les exceptions sont censées être exceptionnelles. Si vous savez qu'un code lèvera une exception, vous n'encapsulez pas le code dans try/catch, mais vous validez les paramètres de ce code avant d'exécuter le code qui peut lever une exception. Les exceptions sont uniquement destinées à garantir que le système ne passe pas dans un état non valide. Si vous savez que certains paramètres d'entrée peuvent conduire à un état non valide, assurez-vous que ces paramètres ne se produisent jamais. De cette façon, vous n'avez qu'à essayer/attraper dans des endroits, où vous pouvez logiquement gérer l'exception, qui est généralement à la limite du système.

Puis-je accéder à "d'autres parties du système, comme DB" à partir du constructeur.

Je pense que cela va à l'encontre principe du moindre étonnement . Peu de gens s'attendraient à ce que le constructeur accède à une base de données. Alors non, tu ne devrais pas faire ça.

22
Euphoric

Pouah.

Un constructeur doit faire le moins possible - le problème étant que le comportement d'exception dans les constructeurs est maladroit. Très peu de programmeurs savent quel est le bon comportement (y compris les scénarios d'héritage), et forcer vos utilisateurs à essayer/attraper chaque instanciation est ... au mieux douloureux.

Il y a deux parties à cela, in le constructeur et sing le constructeur. C'est gênant dans le constructeur car vous ne pouvez pas faire grand-chose quand une exception se produit. Vous ne pouvez pas retourner un type différent. Vous ne pouvez pas retourner null. Vous pouvez essentiellement renvoyer l'exception, renvoyer un objet cassé (mauvais constructeur) ou (dans le meilleur des cas) remplacer une partie interne par une valeur par défaut saine. tilisation le constructeur est maladroit car alors vos instanciations (et les instanciations de tous les types dérivés!) Peuvent se lancer. Ensuite, vous ne pouvez pas les utiliser dans les initialiseurs de membres. Vous finissez par utiliser des exceptions pour la logique pour voir "ma création a-t-elle réussi?", Au-delà de la lisibilité (et de la fragilité) provoquée par try/catch partout.

Si votre constructeur fait des choses non triviales, ou des choses dont on peut raisonnablement penser qu'elles échoueront, envisagez plutôt une méthode statique Create (avec un constructeur non public). Il est beaucoup plus utilisable, plus facile à déboguer et vous offre toujours une instance entièrement construite en cas de succès.

18
Telastyn

L'équilibre de complexité constructeur-méthode est en effet un sujet de discussion sur le bon montant. Très souvent, un constructeur vide Class() {} est utilisé, avec une méthode Init(..) {..} pour les choses plus compliquées. Parmi les arguments à prendre en compte:

  • Possibilité de sérialisation de classe
  • Avec la méthode de séparation Init du constructeur, vous devez souvent répéter la même séquence Class x = new Class(); x.Init(..); plusieurs fois, en partie dans les tests unitaires. Pas si bon, cependant, limpide pour la lecture. Parfois, je les mets simplement dans une ligne de code.
  • Lorsque l'objectif du test unitaire est simplement un test d'initialisation de classe, mieux vaut avoir son propre Init, pour effectuer des actions plus compliquées accomplies une par une, avec les Asserts intermédiaires.
0
Pavel Khrapkin