la complexité d'un constructeur

18

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.

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 chez un constructeur? je me demande si récupérer l'objet A en allant dans DB fait partie de "toute autre initialisation nécessaire pour rendre l'objet prêt à l'emploi" parce que si l'utilisateur transmettait 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.

Taein Kim
la source
8
Je ne suis pas d'accord. Jetez un œil au principe d'inversion de dépendance, il serait préférable que l'objet A soit remis à l'objet B dans un état valide dans son constructeur.
Jimmy Hoffa
5
Demandez-vous combien peut être fait? Combien faut- il faire? Ou quelle partie de votre situation spécifique devrait être réalisée? Ce sont trois questions très différentes.
Bobson
1
Je suis d'accord avec la suggestion de Jimmy Hoffa de considérer l'injection de dépendance (ou "inversion de dépendance"). Ce fut ma première pensée en lisant la description, car il semble que vous êtes déjà à mi-chemin; vous avez déjà une fonctionnalité séparée en classe A, que la classe B utilise. Je ne peux pas parler de votre cas, mais je trouve généralement que le refactoring de code pour utiliser le modèle d'injection de dépendance rend les classes plus simples / moins entrelacées.
Dr.Wily's Apprentice
1
Bobson, j'ai changé le titre en «devrait». Je demande ici les meilleures pratiques.
Taein Kim
1
@JimmyHoffa: vous devriez peut-être étendre votre commentaire en réponse.
kevin cline

Réponses:

22

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 d'une application du principe Fail-fast . Faire échouer le 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 du 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.

Euphorique
la source
2
La solution la plus appropriée à cela consiste généralement à ajouter une méthode de type "ouvert" où la construction est gratuite, mais aucune utilisation ne peut être effectuée avant que vous "ouvriez" / "connectez" / etc, où se produit toute l'initialisation réelle. Les constructeurs qui échouent peuvent provoquer toutes sortes de problèmes lorsque vous souhaitez à l'avenir faire une construction partielle d'objets, ce qui est une capacité utile.
Jimmy Hoffa
@JimmyHoffa Je ne vois aucune différence entre la méthode constructeur et la méthode spécifique de construction.
Euphoric
4
Vous ne pouvez pas, mais beaucoup le font. Réfléchissez lorsque vous créez un objet de connexion, le constructeur le connecte-t-il? L'objet est-il utilisable s'il n'est pas connecté? Dans les deux questions, la réponse est non, car il est utile d'avoir des objets de connexion partiellement construits afin de pouvoir modifier les paramètres et les choses avant d'ouvrir la connexion. Il s'agit d'une approche courante de ce scénario. Je pense toujours que l'injection de constructeur est la bonne approche pour les scénarios où l'objet A a une dépendance aux ressources mais une méthode ouverte est toujours meilleure que de laisser le constructeur faire le travail dangereux.
Jimmy Hoffa
@JimmyHoffa Mais c'est une exigence spécifique de classe de connexion, pas une règle générale de conception OOP. Je dirais en fait que c'est un cas exceptionnel, plutôt qu'une règle. Vous parlez essentiellement de modèle de constructeur.
Euphoric
2
Ce n'est pas une exigence, c'était une décision de conception. Ils auraient pu obliger le constructeur à prendre une chaîne de connexion et à se connecter immédiatement lors de la construction. Ils ont décidé pas trop car il est utile de pouvoir créer l'objet, puis de modifier la chaîne de connexion ou d'autres bits et bobbles et de le connecter plus tard ,
Jimmy Hoffa
19

Pouah.

Un constructeur devrait 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, dans le constructeur et en utilisant 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. Utiliser le constructeur est gênant car alors vos instanciations (et les instanciations de tous les types dérivés!) Peuvent se lancer. Vous ne pouvez alors 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 Createplutôt une méthode statique (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.

Telastyn
la source
2
Les scénarios de construction complexes expliquent exactement pourquoi les modèles de création comme vous le mentionnez ici existent. C'est une bonne approche de ces constructions problématiques. Je dois réfléchir à la façon dont un Connectionobjet .NET peut CreateCommandvous être utile afin que vous sachiez que la commande est correctement connectée.
Jimmy Hoffa
2
À cela, j'ajouterais qu'une base de données est une dépendance externe lourde, supposons que vous souhaitiez changer de base de données à un moment donné (ou vous moquer d'un objet pour le tester), déplacer ce type de code vers une classe Factory (ou quelque chose comme un IService fournisseur) semble être une approche plus propre
Jason Sperske
4
pouvez-vous élaborer sur "le comportement d'exception dans les constructeurs est maladroit"? Si vous deviez utiliser Create, n'auriez-vous pas besoin de l'envelopper dans try catch comme vous le feriez pour un appel au constructeur? J'ai l'impression que Create n'est qu'un nom différent pour le constructeur. Peut-être que je n'obtiens pas votre bonne réponse?
Taein Kim
1
Ayant dû essayer d'attraper des exceptions provenant des constructeurs, +1 aux arguments contre cela. Ayant travaillé avec des gens qui disent "Pas de travail dans le constructeur", cela peut également créer des motifs étranges. J'ai vu du code où chaque objet a non seulement un constructeur, mais aussi une forme de Initialize () où, devinez quoi? L'objet n'est pas vraiment valide tant qu'on ne l'appelle pas non plus. Et puis vous avez deux choses à appeler: le ctor et Initialize (). Le problème du travail de configuration d'un objet ne disparaît pas - C # peut fournir des solutions différentes de celles de C ++. Les usines sont bonnes!
J Trana
1
@jtrana - ouais, initialiser n'est pas bon. Le constructeur initialise une valeur par défaut saine ou une fabrique ou une méthode create le construit et retourne une instance utilisable.
Telastyn
1

L'équilibre de complexité constructeur-méthode est en effet un sujet de discussion sur le bon montant. Un constructeur assez souvent 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 Class x = new Class(); x.Init(..);plusieurs fois la même séquence , 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 le test d'unité n'est qu'un test d'initialisation de classe, mieux vaut avoir son propre Init, pour effectuer des actions plus compliquées exécutées une par une, avec les Asserts intermédiaires.
Pavel Khrapkin
la source
l'avantage, à mon avis, de la initméthode est que la gestion des échecs est beaucoup plus facile. En particulier, la valeur de retour de la initméthode peut indiquer si l'initialisation a réussi et la méthode peut garantir que l'objet est toujours en bon état.
pqnet