Combien d'injections est acceptable dans une classe lors de l'utilisation de l'injection de dépendance

9

J'utilise Unity en C # pour l'injection de dépendance, mais la question devrait être applicable à tout langage et framework qui utilise l'injection de dépendance.

J'essaie de suivre le principe SOLID et j'ai donc eu beaucoup d'abstractions. Mais maintenant, je me demande s'il existe une meilleure pratique pour combien d'injections une classe devrait injecter?

Par exemple, j'ai un référentiel avec 9 injections. Sera-ce difficile à lire pour un autre développeur?

Les injections ont les responsabilités suivantes:

  • IDbContextFactory - création du contexte pour la base de données.
  • IMapper - Mappage des entités aux modèles de domaine.
  • IClock - Abstracts DateTime.Now pour aider avec les tests unitaires.
  • IPerformanceFactory - mesure le temps d'exécution pour des méthodes spécifiques.
  • ILog - Log4net pour la journalisation.
  • ICollectionWrapperFactory - Crée des collections (qui étend IEnumerable).
  • IQueryFilterFactory - Génère des requêtes basées sur l'entrée qui interrogera db.
  • IIdentityHelper - Récupère l'utilisateur connecté.
  • IFaultFactory - Créer différentes FaultExceptions (j'utilise WCF).

Je ne suis pas vraiment déçu de la façon dont j'ai délégué les responsabilités, mais je commence à m'inquiéter pour la lisibilité.

Alors, mes questions:

Y a-t-il une limite au nombre d'injections qu'une classe devrait avoir? Et si oui, comment l'éviter?

De nombreuses injections limitent-elles la lisibilité ou l’améliorent-elles réellement?

Smoksnes
la source
2
Mesurer la qualité par des chiffres est généralement aussi mauvais que d'être payé par des lignes de code que vous écrivez par mois. Cependant, vous avez inclus l'exemple réel des dépendances, ce qui est une excellente idée. Si j'étais vous, je reformulerais votre question pour supprimer le concept de comptage et de limite stricte, et me concentrer au lieu de la qualité en soi. Lors de sa reformulation, veillez à ne pas rendre votre question trop précise.
Arseni Mourzenko
3
Combien d'arguments constructeurs sont acceptables? L'IoC ne change rien à cela .
Telastyn
Pourquoi les gens veulent-ils toujours des limites absolues?
Marjan Venema
1
@Telastyn: pas nécessairement. Ce que l'IoC change, c'est qu'au lieu de s'appuyer sur des classes statiques / singletons / variables globales, toutes les dépendances sont plus centralisées et plus «visibles».
Arseni Mourzenko
@MarjanVenema: car cela rend la vie beaucoup plus facile. Si vous connaissez exactement le LOC maximum par méthode ou le nombre maximum de méthodes dans une classe ou un nombre maximum de variables sur une méthode, et c'est la seule chose qui compte, il devient facile de qualifier le code comme bon ou mauvais, comme ainsi que «corriger» le mauvais code. Il est regrettable que la vie réelle soit beaucoup plus complexe que cela et que de nombreux paramètres ne soient pour la plupart pas pertinents.
Arseni Mourzenko

Réponses:

11

Trop de dépendances peuvent indiquer que la classe elle-même en fait trop. Afin de déterminer si cela ne fait pas trop:

  • Regardez la classe elle-même. Serait-il sensé de le diviser en deux, trois, quatre? Cela a-t-il un sens dans son ensemble?

  • Regardez les types de dépendances. Lesquels sont spécifiques à un domaine et ceux qui sont «mondiaux»? Par exemple, je ne considérerais pas ILogau même niveau que IQueryFilterFactory: le premier serait de toute façon disponible dans la plupart des classes commerciales s'ils utilisent la journalisation. D'un autre côté, si vous trouvez beaucoup de dépendances spécifiques au domaine, cela peut indiquer que la classe en fait trop.

  • Regardez les dépendances qui peuvent être remplacées par des valeurs.

    IClock - Abstracts DateTime.Now pour aider avec les tests unitaires.

    Cela pourrait facilement être remplacé en DateTime.Nowétant transmis directement aux méthodes qui nécessitent de connaître l'heure actuelle.

En regardant les dépendances réelles, je ne vois rien qui pourrait indiquer que de mauvaises choses se produisent:

  • IDbContextFactory - création du contexte pour la base de données.

    OK, nous sommes probablement dans une couche métier où les classes interagissent avec la couche d'accès aux données. Semble bien.

  • IMapper - Mappage des entités aux modèles de domaine.

    Il est difficile de dire quoi que ce soit sans l'image globale. Il se peut que l'architecture soit incorrecte et que le mappage soit effectué directement par la couche d'accès aux données, ou il se peut que l'architecture soit parfaitement correcte. Dans tous les cas, il est logique d'avoir cette dépendance ici.

    Un autre choix serait de diviser la classe en deux: l'un traitant de la cartographie, l'autre de la logique métier actuelle. Cela créerait une couche de facto qui séparera davantage le BL du DAL. Si les mappages sont complexes, ce pourrait être une bonne idée. Dans la plupart des cas, cependant, cela ne ferait qu'ajouter une complexité inutile.

  • IClock - Abstracts DateTime.Now pour aider avec les tests unitaires.

    Il n'est probablement pas très utile d'avoir une interface (et une classe) séparée juste pour obtenir l'heure actuelle. Je passerais simplement le DateTime.Nowaux méthodes qui nécessitent l'heure actuelle.

    Une classe distincte peut avoir du sens s'il existe d'autres informations, comme les fuseaux horaires ou les plages de dates, etc.

  • IPerformanceFactory - mesure le temps d'exécution pour des méthodes spécifiques.

    Voir le point suivant.

  • ILog - Log4net pour la journalisation.

    Ces fonctionnalités transcendantes doivent appartenir au cadre et les bibliothèques réelles doivent être interchangeables et configurables au moment de l'exécution (par exemple via app.config dans .NET).

    Malheureusement, ce n'est pas (encore) le cas, ce qui vous permet soit de choisir une bibliothèque et de vous y tenir, soit de créer une couche d'abstraction pour pouvoir échanger les bibliothèques plus tard si nécessaire. Si votre intention est précisément d'être indépendant du choix de la bibliothèque, allez-y. Si vous êtes sûr que vous continuerez à utiliser la bibliothèque pendant des années, n'ajoutez pas d'abstraction.

    Si la bibliothèque est trop complexe à utiliser, un motif de façade est logique.

  • ICollectionWrapperFactory - Crée des collections (qui étend IEnumerable).

    Je suppose que cela crée des structures de données très spécifiques qui sont utilisées par la logique du domaine. Cela ressemble à une classe utilitaire. Utilisez plutôt une classe par structure de données avec les constructeurs appropriés. Si la logique d'initialisation est légèrement compliquée à intégrer dans un constructeur, utilisez des méthodes d'usine statiques. Si la logique est encore plus complexe, utilisez un modèle d'usine ou un modèle de générateur.

  • IQueryFilterFactory - Génère des requêtes basées sur l'entrée qui interrogera db.

    Pourquoi n'est-ce pas dans la couche d'accès aux données? Pourquoi y a-t-il un Filterdans le nom?

  • IIdentityHelper - Récupère l'utilisateur connecté.

    Je ne sais pas pourquoi y a-t-il un Helpersuffixe. Dans tous les cas, les autres suffixes ne seront pas particulièrement explicites non plus ( IIdentityManager?)

    Quoi qu'il en soit, il est parfaitement logique d'avoir cette dépendance ici.

  • IFaultFactory - Créer différentes FaultExceptions (j'utilise WCF).

    La logique est-elle si complexe qu'elle nécessite d'utiliser un modèle d'usine? Pourquoi l'injection de dépendance est-elle utilisée pour cela? Souhaitez-vous échanger la création d'exceptions entre le code de production et les tests? Pourquoi?

    J'essaierais de refaçonner cela en simple throw new FaultException(...). Si des informations globales doivent être ajoutées à toutes les exceptions avant de les propager au client, WCF dispose probablement d'un mécanisme permettant d'attraper une exception non gérée et de la modifier et de la renvoyer au client.

Y a-t-il une limite au nombre d'injections qu'une classe devrait avoir? Et si oui, comment l'éviter?

Mesurer la qualité par des chiffres est généralement aussi mauvais que d'être payé par des lignes de code que vous écrivez par mois. Vous pouvez avoir un grand nombre de dépendances dans une classe bien conçue, car vous pouvez avoir une classe de merde utilisant peu de dépendances.

De nombreuses injections limitent-elles la lisibilité ou l’améliorent-elles réellement?

De nombreuses dépendances rendent la logique plus difficile à suivre. Si la logique est difficile à suivre, la classe en fait probablement trop et devrait être divisée.

Arseni Mourzenko
la source
Merci pour vos commentaires et votre temps. Principalement sur FaultFactory qui sera déplacé à la place vers WCF-logic. Je garderai IClock car c'est une bouée de sauvetage lors du TDD d'une application. Il y a plusieurs fois où vous voulez vous assurer qu'une valeur spécifique a été définie avec un temps donné. Alors DateTime.Now ne suffira pas toujours car il n'est pas moquable.
smoksnes
7

Ceci est un exemple classique de DI vous disant que votre classe devient probablement trop grande pour être une seule classe. Ceci est souvent interprété comme "wow, DI rend mes constructeurs plus grands que jupiter, cette technique est horrible", mais ce qu'elle vous dit en fait, c'est que "votre classe a un bateau plein de dépendances". Sachant cela, nous pouvons soit

  • Balayez le problème sous le tapis en commençant par de nouvelles dépendances
  • Repensez notre conception. Peut-être que certaines dépendances se rejoignent toujours et devraient être cachées derrière une autre abstraction. Peut-être que votre classe devrait être divisée en 2. Peut-être qu'elle devrait être composée de plusieurs classes qui nécessitent chacune un petit sous-ensemble des dépendances.

Il existe d'innombrables façons de gérer les dépendances, et il est impossible de dire ce qui fonctionne le mieux dans votre cas sans connaître votre code et votre application.

Pour répondre à vos dernières questions:

  • Y a-t-il une limite supérieure au nombre de dépendances qu'une classe devrait avoir?

Oui, la limite supérieure est "trop". Combien est «trop»? «Trop» est lorsque la cohésion de la classe devient «trop faible». Tout dépend. Normalement, si votre réaction à une classe est "wow, cette chose a beaucoup de dépendances", c'est trop.

  • L'injection de dépendances améliore-t-elle ou nuit-elle à la lisibilité?

Je pense que cette question est trompeuse. La réponse peut être oui ou non. Mais ce n'est pas la partie la plus intéressante. Le point d'injecter des dépendances est de les rendre VISIBLES. Il s'agit de créer une API qui ne ment pas. Il s'agit d'empêcher l'état mondial. Il s'agit de rendre le code testable. Il s'agit de réduire le couplage.

Les classes bien conçues avec des méthodes raisonnablement conçues et nommées sont plus lisibles que celles mal conçues. DI n'améliore pas vraiment ni ne nuit à la lisibilité en soi, il fait juste ressortir vos choix de conception, et s'ils sont mauvais, cela vous piquera les yeux. Cela ne signifie pas que DI a rendu votre code moins lisible, cela vous a juste montré que votre code était déjà un gâchis, vous veniez de le cacher.

Sara
la source
1
Bonne rétroaction. Je vous remercie. Oui, la limite supérieure est "trop". - Fantastique et tellement vrai.
smoksnes
3

Selon Steve McConnel, auteur de l'omniprésent "Code Complete", la règle générale est que plus de 7 est une odeur de code qui nuit à la maintenabilité. Personnellement, je pense que le nombre est inférieur dans la plupart des cas, mais faire correctement DI entraînera de nombreuses dépendances à injecter lorsque vous êtes très proche de la racine de composition. Ceci est normal et attendu. C'est l'une des raisons pour lesquelles les conteneurs IoC sont utiles et valent la complexité qu'ils ajoutent à un projet.

Donc, si vous êtes très proche du point d'entrée de votre programme, c'est normal et acceptable. Si vous êtes plus profondément dans la logique du programme, c'est probablement une odeur qui devrait être traitée.

Canard en caoutchouc
la source