web-dev-qa-db-fra.com

Quand devrions-nous utiliser l'injection de dépendances (C #)

Je voudrais être sûr de bien comprendre le concept d'injection de dépendance (DI). Bon, je comprends bien le concept, DI n'est pas compliqué: on crée une interface puis on passe l'implémentation de mon interface à la classe qui l'utilise. La manière courante de le passer est par constructeur mais vous pouvez également le passer par setter ou une autre méthode.

Ce que je ne suis pas sûr de comprendre, c'est quand utiliser DI.

Utilisation 1: Bien sûr, l'utilisation de DI dans le cas où vous avez plusieurs implémentations de votre interface semble logique. Vous avez un référentiel pour votre SQL Server puis un autre pour votre base de données Oracle. Les deux partagent la même interface et vous "injectez" (c'est le terme utilisé) celle que vous souhaitez à l'exécution. Ce n'est même pas DI, c'est une programmation basique OO ici.

Utilisation 2: Lorsque vous avez une couche métier avec de nombreux services avec toutes leurs méthodes spécifiques, il semble que la bonne pratique consiste à créer une interface pour chaque service et à injecter également l'implémentation même si celle-ci est unique. Parce que c'est mieux pour la maintenance. C'est ce deuxième usage que je ne comprends pas.

J'ai quelque chose comme 50 classes d'affaires. Rien n'est commun entre eux. Certains sont des référentiels qui récupèrent ou enregistrent des données dans 3 bases de données différentes. Certains lisent ou écrivent des fichiers. Certains font de l'action commerciale pure. Il existe également des valideurs et des aides spécifiques. Le défi est la gestion de la mémoire car certaines classes sont instanciées à partir d'emplacements différents. Un validateur peut appeler plusieurs référentiels et d'autres validateurs qui peuvent à nouveau appeler les mêmes référentiels.

Exemple: couche métier

public class SiteService : Service, ICrud<Site>
{
    public Site Read(Item item, Site site)
    {
        return beper4DbContext.Site
            .AsNoTracking()
            .SingleOrDefault(y => y.SiteId == site.Id && y.ItemId == item.Id)
    }

    public Site Read(string itemCode, string siteCode)
    {       
        using (var itemService = new ItemService())
        {
            var item = itemService.Read(itemCode);
            return Read(item, site);
        }
    }
}
public class ItemSiteService : Service, ICrud<Site>
{
    public ItemSite Read(Item item, Site site)
    {
        return beper4DbContext.ItemSite
            .AsNoTracking()
            .SingleOrDefault(y => y.SiteId == site.Id && y.ItemId == item.Id)
    }

    public ItemSite Read(string itemCode, string siteCode)
    {        
        using (var itemService = new ItemService())
        using (var siteService = new SiteService())
        {
            var item = itemService.Read(itemCode);
            var site = siteService.Read(itemCode, siteCode);
            return Read(item, site);
        }
    }
}

Manette

public class ItemSiteController : BaseController
{
    [Route("api/Item/{itemCode}/ItemSite/{siteCode}")]
    public IHttpActionResult Get(string itemCode, string siteCode)
    {
        using (var service = new ItemSiteService())
        {
            var itemSite = service.Read(itemCode, siteCode);
            return Ok(itemSite);
        }
    }
}

Cet exemple est très basique mais vous voyez comment je peux facilement créer 2 instances de itemService pour obtenir un itemSite. Ensuite, chaque service est accompagné de son contexte DB. Cet appel va donc créer 3 DbContext. 3 Connexions.

Ma première idée a été de créer un singleton pour réécrire tout ce code comme ci-dessous. Le code est plus lisible et le plus important, le système singleton ne crée qu'une seule instance de chaque service utilisé et le crée au premier appel. Parfait, sauf que j'ai toujours un contexte différent mais je peux faire le même système pour mes contextes. C'est fait.

Couche métier

public class SiteService : Service, ICrud<Site>
{
    public Site Read(Item item, Site site)
    {
        return beper4DbContext.Site
            .AsNoTracking()
            .SingleOrDefault(y => y.SiteId == site.Id && y.ItemId == item.Id)
    }

    public Site Read(string itemCode, string siteCode)
    {       
            var item = ItemService.Instance.Read(itemCode);
            return Read(item, site);
    }
}
public class ItemSiteService : Service, ICrud<Site>
{
    public ItemSite Read(Item item, Site site)
    {
        return beper4DbContext.ItemSite
            .AsNoTracking()
            .SingleOrDefault(y => y.SiteId == site.Id && y.ItemId == item.Id)
    }

    public ItemSite Read(string itemCode, string siteCode)
    {        
            var item = ItemService.Instance.Read(itemCode);
            var site = SiteService.Instance.Read(itemCode, siteCode);
            return Read(item, site);       
    }
}

Manette

public class ItemSiteController : BaseController
{
    [Route("api/Item/{itemCode}/ItemSite/{siteCode}")]
    public IHttpActionResult Get(string itemCode, string siteCode)
    {
            var itemSite = service.Instance.Read(itemCode, siteCode);
            return Ok(itemSite);
    }
}

Certaines personnes me disent selon les bonnes pratiques que je devrais utiliser DI avec une seule instance et utiliser singleton est une mauvaise pratique. Je devrais créer une interface pour chaque classe affaires et l'instancier à l'aide du conteneur DI. Vraiment? Cette DI simplifie mon code. Difficile à croire.

8
Bastien Vandamme

Le cas d'utilisation le plus "populaire" pour DI (à part l'utilisation du modèle de "stratégie" que vous avez déjà décrit) est probablement le test unitaire.

Même si vous pensez qu'il n'y aura qu'une seule "vraie" implémentation pour une interface injectée, dans le cas où vous effectuez des tests unitaires, il y en a généralement une deuxième: une implémentation "factice" dans le seul but de rendre possible un test isolé. Cela vous donne l'avantage de ne pas avoir à gérer la complexité, les bogues possibles et peut-être l'impact sur les performances du "vrai" composant.

Donc non, DI n'est pas pas pour augmenter la lisibilité, il est utilisé pour augmenter la testabilité (- bien sûr, pas exclusivement).

Ce n'est pas une fin en soi. Si votre classe ItemService est très simple, ce qui ne fait aucun accès à un réseau externe ou à une base de données, elle n'entrave donc pas l'écriture de tests unitaires pour quelque chose comme SiteService, testant cette dernière de manière isolée peut valoir la peine, donc DI ne sera pas nécessaire. Cependant, si ItemService accède à d'autres sites par le réseau, vous voudrez probablement faire un test unitaire SiteService découplé de celui-ci, ce qui peut être accompli en remplaçant le "réel" ItemService par un MockItemService, qui fournit des faux articles codés en dur.

Permettez-moi de souligner une autre chose: dans vos exemples, on pourrait dire que l'on n'aura pas besoin de DI ici pour tester la logique métier de base - les exemples montrent toujours deux variantes des méthodes Read, une avec la vraie entreprise logique impliquée (qui peut être testée à l'unité sans DI), et qui n'est que le code "glue" pour connecter le ItemService à l'ancienne logique. Dans le cas illustré, c'est en effet un argument valable contre DI - en fait, si DI peut être évité sans sacrifier la testabilité de cette façon, alors allez-y. Mais tous les codes du monde réel ne sont pas aussi simples, et souvent DI est la solution la plus simple pour obtenir une testabilité unitaire "suffisante".

14
Doc Brown

En n'utilisant pas l'injection de dépendance, vous vous permettez de créer des connexions permanentes avec d'autres objets. Des connexions que vous pouvez cacher à l'intérieur où elles surprendront les gens. Des connexions qu'ils ne peuvent changer qu'en réécrivant ce que vous créez.

Plutôt que cela, vous pouvez utiliser l'injection de dépendance (ou le passage de référence si vous êtes de la vieille école comme moi) pour rendre explicite ce dont un objet a besoin sans le forcer à définir comment ses besoins doivent être satisfaits.

Cela vous oblige à accepter de nombreux paramètres. Même ceux avec des défauts évidents. En C #, les sods chanceux ont arguments nommés et facultatifs . Cela signifie que vous avez des arguments par défaut. Si cela ne vous dérange pas d'être lié statiquement à vos valeurs par défaut, même lorsque vous ne les utilisez pas, vous pouvez autoriser DI sans être submergé d'options. Cela suit convention sur la configuration .

Les tests ne sont pas une bonne justification pour DI. Le moment où vous pensez que c'est quelqu'un vous vendra un cadre de moquerie wiz bang qui utilise la réflexion ou une autre magie pour vous convaincre que vous pouvez revenir à la façon dont vous avez travaillé auparavant et utiliser la magie pour faire le reste.

Utilisé correctement, le test peut être un bon moyen de montrer si une conception est isolée. Mais ce n'est pas le but. Cela n'empêche pas les vendeurs d'essayer de prouver qu'avec assez de magie tout est isolé. Gardez la magie au minimum.

Le but de cet isolement est de gérer le changement. C'est bien si un changement peut être fait au même endroit. Ce n'est pas agréable d'avoir à le suivre fichier après fichier en espérant que la folie prendra fin.

Mettez-moi dans un magasin qui refuse de faire des tests unitaires et je ferai toujours DI. Je le fais parce que cela me permet de séparer ce qui est nécessaire de la façon dont cela se fait. Test ou pas de test Je veux cet isolement.

4
candied_orange

La vue d'hélicoptère de DI est simplement la possibilité d'échanger une implémentation pour une interface. Bien que cela soit bien sûr une aubaine pour les tests, il existe d'autres avantages potentiels:

Version des implémentations d'un objet

Si vos méthodes acceptent les paramètres d'interface dans les couches intermédiaires, vous êtes libre de passer toutes les implémentations que vous aimez dans la couche supérieure, ce qui réduit la quantité de code qui doit être écrit pour échanger les implémentations. Certes, c'est un avantage des interfaces de toute façon, mais si le code est écrit avec DI à l'esprit, vous obtiendrez cet avantage hors de la boîte.

Réduction du nombre d'objets qui doivent passer à travers les couches

Bien que cela s'applique principalement aux infrastructures DI, si objet A nécessite une instance de objet B, il est possible d'interroger le noyau (ou autre) pour générer objet B à la volée plutôt que de le passer à travers les couches. Cela réduit la quantité de code qui doit être écrit et testé. Il garde également les calques qui ne se soucient pas objet B propres.

2
Robbie Dee

Il n'est pas nécessaire d'utiliser des interfaces pour utiliser DI. Le but principal de DI est de séparer la construction et l'utilisation des objets.

L'utilisation de singletons est à juste titre mal vue dans la plupart des cas. L'une des raisons est qu'il devient très difficile d'obtenir un aperçu des dépendances d'une classe.

Dans votre exemple, ItemSiteController pourrait simplement prendre un ItemSiteService comme argument constructeur. Cela vous permet d'éviter tout coût de création d'objets, mais évitez l'inflexibilité d'un singleton. Il en va de même pour ItemSiteService, s'il a besoin d'un ItemService et d'un SiteService, injectez-les dans le constructeur.

L'avantage est plus important lorsque tous les objets utilisent l'injection de dépendance. Cela vous permet de centraliser la construction dans un module dédié ou de la déléguer à un conteneur DI.

Une hiérarchie de dépendances pourrait ressembler à ceci:

public interface IStorage
{
}

public class DbStorage : IStorage
{
    public DbStorage(string connectionString){}
}

public class FileStorage : IStorage
{
    public FileStorage(FileInfo file){}
}

public class MemoryStorage : IStorage
{
}

public class CachingStorage : IStorage
{
    public CachingStorage(IStorage storage) { }
}

public class MyService
{
    public MyService(IStorage storage){}
}

public class Controller
{
    public Controller(MyService service){}
}

Notez qu'il n'y a qu'une seule classe sans aucun paramètre constructeur et une seule interface. Lors de la configuration du conteneur DI, vous pouvez décider quel stockage utiliser, ou si la mise en cache doit être utilisée, etc. Le test est plus facile car vous pouvez décider quelle base de données utiliser, ou utiliser un autre type de stockage. Vous pouvez également configurer le conteneur DI pour traiter les objets comme des singletons si nécessaire, dans le contexte de l'objet conteneur.

2
JonasH

Vous isolez les systèmes externes.

Utilisation 1: Bien sûr, l'utilisation de DI dans le cas où vous avez plusieurs implémentations de votre interface semble logique. Vous avez un référentiel pour votre SQL Server puis un autre pour votre base de données Oracle. Les deux partagent la même interface et vous "injectez" (c'est le terme utilisé) celle que vous souhaitez à l'exécution. Ce n'est même pas DI, c'est une programmation basique OO ici.


Oui, utilisez DI ici. S'il va sur le réseau, la base de données, le système de fichiers, un autre processus, une entrée utilisateur, etc. Vous voulez l'isoler.

L'utilisation de DI facilitera les tests car vous vous moquerez facilement de ces systèmes externes. Non, je ne dis pas que c'est la première étape vers les tests unitaires. Ni que vous ne pouvez pas faire de test sans cela.

De plus, même si vous n'aviez qu'une seule base de données, l'utilisation de DI vous aiderait le jour où vous souhaitez migrer. Alors, oui, DI.

Utilisation 2: Lorsque vous avez une couche métier avec de nombreux services avec toutes leurs méthodes spécifiques, il semble que la bonne pratique consiste à créer une interface pour chaque service et à injecter également l'implémentation même si celle-ci est unique. Parce que c'est mieux pour la maintenance. C'est ce deuxième usage que je ne comprends pas.

Bien sûr, DI peut vous aider. Je voudrais débattre des conteneurs.

Il convient peut-être de noter que l'injection de dépendances avec des types concrets est toujours une injection de dépendances. L'important est que vous puissiez créer des instances personnalisées. Il ne doit pas nécessairement s'agir d'une injection d'interface (bien que l'injection d'interface soit plus polyvalente, cela ne signifie pas que vous devriez l'utiliser partout).

L'idée de créer une interface explicite pour chaque classe doit mourir. En fait, si vous n'aviez qu'une seule implémentation d'une interface ... YAGNI . L'ajout d'une interface est relativement bon marché, peut être fait lorsque vous en avez besoin. En fait, je suggérerais d'attendre d'avoir deux ou trois implémentations candidates pour avoir une meilleure idée de ce qui est commun entre elles.

Cependant, le revers de la médaille, c'est que vous pouvez créer des interfaces qui correspondent plus étroitement à ce dont le code client a besoin. Si le code client n'a besoin que de quelques membres d'une classe, vous pouvez avoir une interface juste pour cela. Cela conduira à une meilleure ségrégation d'interface .


Des conteneurs?

Vous savez que vous n'en avez pas besoin.

Réduisons-le aux compromis. Il y a des cas où ils n'en valent pas la peine. Vous aurez votre classe prendre les dépendances dont elle a besoin sur le constructeur. Et cela pourrait être suffisant.

Je ne suis vraiment pas fan des attributs d'annotation pour "l'injection de setter", encore moins de tiers, je pense que cela pourrait être nécessaire pour des implémentations indépendantes de votre volonté ... cependant, si vous décidez de changer bibliothèque ceux-ci doivent changer.

Finalement, vous commencerez à créer des routines pour créer ces objets, car pour les créer, vous devez d'abord créer ces autres, et pour ceux dont vous avez besoin de plus ...

Eh bien, lorsque cela se produit, vous voulez mettre toute cette logique en un seul endroit et la réutiliser. Vous voulez une source unique de vérité sur la façon dont vous créez votre objet. Et vous l'obtenez en sans vous répéter . Cela simplifiera votre code. C'est logique, non?

Eh bien, où mettez-vous cette logique? Le premier réflexe sera d'avoir un Service Locator . Une implémentation simple est un singleton avec un dictionnaire en lecture seule de sines . Une implémentation plus complexe pourrait utiliser la réflexion pour créer les usines lorsque vous n'en avez pas fourni une.

Cependant, l'utilisation d'un localisateur de service singleton ou statique signifie que vous ferez quelque chose comme var x = IoC.Resolve<?> partout où vous devez créer une instance. Ce qui ajoute un couplage solide à votre localisateur de service/conteneur/injecteur. Cela peut en fait rendre les tests unitaires plus difficiles.

Vous voulez un injecteur que vous instanciez et ne le gardez que pour être utilisé sur le contrôleur. Vous ne voulez pas qu'il pénètre profondément dans le code. Cela pourrait en fait rendre les tests plus difficiles. Si une partie de votre code en a besoin pour instancier quelque chose, il devrait s'attendre à une instance (ou tout au plus une fabrique) sur son constructeur.

Et si vous avez beaucoup de paramètres sur le constructeur ... voyez si vous avez des paramètres qui voyagent ensemble. Il y a de fortes chances que vous puissiez fusionner les paramètres en types de descripteurs (les types de valeurs idéalement).

2
Theraot