Devrais-je utiliser Dependency Injection ou des usines statiques?

81

Lors de la conception d'un système, je suis souvent confronté au problème de l'utilisation d'un ensemble de modules (journalisation, accès à une base de données, etc.) utilisés par les autres modules. La question est de savoir comment puis-je fournir ces composants à d'autres composants. Deux réponses semblent possibles: injection de dépendance ou utilisation du modèle d’usine. Cependant les deux semblent faux

  • Les usines facilitent les tests et ne permettent pas de permuter facilement les implémentations. Ils ne font pas non plus apparaître de dépendances (par exemple, vous examinez une méthode, oubliant qu’elle appelle une méthode qui appelle une méthode qui appelle une méthode qui utilise une base de données).
  • L'injection de dépendance gonfle massivement les listes d'arguments des constructeurs et couvre certains aspects de votre code. La situation typique est celle où les constructeurs de plus de la moitié des classes ressemblent à ceci(....., LoggingProvider l, DbSessionProvider db, ExceptionFactory d, UserSession sess, Descriptions d)

Voici un exemple typique de situation qui me pose problème: j'ai des classes d'exception, qui utilisent des descriptions d'erreur chargées à partir de la base de données, à l'aide d'une requête comportant un paramètre de paramètre de langue utilisateur, qui est un objet de session utilisateur. Donc, pour créer une nouvelle exception, j'ai besoin d'une description, qui nécessite une session de base de données et la session utilisateur. Je suis donc condamné à faire glisser tous ces objets dans toutes mes méthodes, au cas où je devrais lancer une exception.

Comment puis-je aborder un tel problème ??

RokL
la source
2
Si une usine peut résoudre tous vos problèmes, vous pourriez peut-être simplement l'injecter dans vos objets et en obtenir LoggingProvider, DbSessionProvider, ExceptionFactory, UserSession.
Giorgio
1
Trop d’intrants dans une méthode, qu’ils soient transmis ou injectés, sont davantage un problème de la conception même des méthodes. Quel que soit votre choix, vous voudrez peut-être réduire un peu la taille de vos méthodes (ce qui est plus facile à faire une fois que vous avez l'injection en place)
Bill K
La solution ici ne devrait pas réduire les arguments. Construisez plutôt des abstractions qui construisent un objet de niveau supérieur qui effectue tout le travail dans l'objet et vous procure des avantages.
Alex

Réponses:

74

Utilisez l'injection de dépendance, mais chaque fois que vos listes d'arguments de constructeur deviennent trop grandes, refactorisez-les à l'aide d'un service de façade . L'idée est de regrouper certains des arguments du constructeur, en introduisant une nouvelle abstraction.

Par exemple, vous pouvez introduire un nouveau type SessionEnvironmentencapsulant un DBSessionProvider, le UserSessionet le chargé Descriptions. Pour savoir quelles abstractions ont le plus de sens, il faut connaître les détails de votre programme.

Une question similaire a déjà été posée ici sur SO .

Doc Brown
la source
9
+1: Je pense que grouper des arguments de constructeur en classes est une très bonne idée. Cela vous oblige également à organiser ces arguments en plusieurs structures de sens.
Giorgio
5
Mais si le résultat n'est pas une structure significative, vous cachez simplement la complexité qui viole SRP. Dans ce cas, une refactorisation de classe devrait être effectuée.
danidacar
1
@Giorgio Je ne suis pas d'accord avec une affirmation générale selon laquelle "grouper des arguments de constructeur en classes est une très bonne idée". Si vous qualifiez cela avec "dans ce scénario", alors c'est différent.
Tymtam
19

L'injection de dépendance gonfle massivement les listes d'arguments des constructeurs et couvre certains aspects de votre code.

À partir de là, il ne semble pas que vous compreniez bien la DI - l’idée est d’inverser le modèle d’instanciation d’objet dans une usine.

Votre problème spécifique semble être un problème de POO plus général. Pourquoi les objets ne peuvent-ils pas simplement lancer des exceptions normales non lisibles par un humain lors de leur exécution, puis avoir quelque chose avant l'attentat final qui attrape cette exception et, à ce moment-là, utilise les informations de session pour lancer une nouvelle exception plus jolie ?

Une autre approche serait d'avoir une fabrique d'exceptions, qui est transmise aux objets via leurs constructeurs. Au lieu de lancer une nouvelle exception, la classe peut jeter sur une méthode de l'usine (par exemple throw PrettyExceptionFactory.createException(data).

N'oubliez pas que vos objets, à l'exception de vos objets d'usine, ne doivent jamais utiliser l' newopérateur. Les exceptions constituent généralement un cas particulier, mais dans votre cas, elles peuvent constituer une exception!

Jonathan Rich
la source
1
J'ai lu quelque part que lorsque la liste de vos paramètres devient trop longue, ce n'est pas parce que vous utilisez l'injection de dépendance, mais parce que vous avez besoin de plus d'injection de dépendance.
Giorgio
Cela pourrait être l’une des raisons - généralement, en fonction de la langue, votre constructeur ne devrait pas avoir plus de 6 à 8 arguments, et pas plus de 3 à 4 de ceux-ci devraient être des objets eux-mêmes, à moins que le motif spécifique (comme le Buildermotif) le dicte. Si vous transmettez des paramètres à votre constructeur parce que votre objet instancie d’autres objets, c’est un cas évident pour IoC.
Jonathan Rich
12

Vous avez déjà très bien énuméré les inconvénients du modèle d'usine statique, mais je ne suis pas tout à fait d'accord avec les inconvénients du modèle d'injection de dépendance:

Cette injection de dépendance vous oblige à écrire du code car chaque dépendance n’est pas un bogue, mais une fonctionnalité: elle vous oblige à réfléchir pour savoir si vous avez réellement besoin de ces dépendances, favorisant ainsi le couplage lâche. Dans votre exemple:

Voici un exemple typique de situation qui me pose problème: j'ai des classes d'exception, qui utilisent des descriptions d'erreur chargées à partir de la base de données, à l'aide d'une requête comportant un paramètre de paramètre de langue utilisateur, qui est un objet de session utilisateur. Donc, pour créer une nouvelle exception, j'ai besoin d'une description, qui nécessite une session de base de données et la session utilisateur. Je suis donc condamné à faire glisser tous ces objets dans toutes mes méthodes, au cas où je devrais lancer une exception.

Non, vous n'êtes pas condamné. Pourquoi est-il de la responsabilité de la logique applicative de localiser vos messages d'erreur pour une session utilisateur particulière? Et si, ultérieurement, vous vouliez utiliser ce service métier à partir d'un programme batch (sans session utilisateur ...)? Ou que se passe-t-il si le message d'erreur ne doit pas être affiché à l'utilisateur actuellement connecté, mais à son superviseur (qui peut préférer une langue différente)? Ou si vous vouliez réutiliser la logique métier sur le client (qui n'a pas accès à une base de données ...)?

Il est évident que la localisation des messages dépend de ceux qui les consultent, c’est-à-dire que la couche présentation en est responsable. Par conséquent, je jetterais les exceptions ordinaires du service métier, qui portent un identifiant de message qui peut ensuite être consulté dans le gestionnaire d'exceptions de la couche de présentation, quelle que soit la source de message utilisée.

De cette façon, vous pouvez supprimer 3 dépendances inutiles (UserSession, ExceptionFactory et probablement des descriptions), rendant ainsi votre code à la fois plus simple et plus polyvalent.

En règle générale, je n'utiliserais que des usines statiques pour les éléments pour lesquels vous avez besoin d'un accès omniprésent et dont la disponibilité est garantie dans tous les environnements dans lesquels nous pourrions vouloir exécuter le code (comme la journalisation). Pour tout le reste, j'utiliserais une vieille injection de dépendance.

meriton - en grève
la source
Cette. Je ne vois pas la nécessité d'appuyer sur la base de données pour générer une exception.
Caleth
1

Utilisez l'injection de dépendance. L'utilisation d'usines statiques est un emploi de l' Service Locatoranti - modèle. Voir le travail précurseur de Martin Fowler ici - http://martinfowler.com/articles/injection.html

Si vos arguments de constructeur deviennent trop volumineux et que vous n'utilisez pas un conteneur DI par tous les moyens, écrivez vos propres fabriques pour instanciation, ce qui permet de le configurer, soit par XML, soit en liant une implémentation à une interface.

Sam
la source
5
Service Locator n'est pas un antipattern - Fowler le référence lui-même dans l'URL que vous avez postée. Bien que le modèle de localisateur de services puisse être utilisé de manière abusive (de la même manière que les singletons sont exploités - pour faire abstraction de l'état global), il s'agit d'un modèle très utile.
Jonathan Rich
1
Intéressant à savoir. J'ai toujours entendu parler d'un anti-modèle.
Sam
4
Ce n'est qu'un anti-modèle si le localisateur de service est utilisé pour stocker un état global. Le localisateur de service doit être un objet sans état après l'instanciation, et de préférence immuable.
Jonathan Rich
XML n'est pas sûr. Je considérerais cela comme un plan z après l'échec de toute autre approche
Richard Tingle
1

J'irais aussi bien avec Dependency Injection. Rappelez-vous que l’ID n’est pas seulement effectué par le biais de constructeurs, mais également par l’intermédiaire de Property setters. Par exemple, l'enregistreur pourrait être injecté en tant que propriété.

En outre, vous pouvez utiliser un conteneur IoC qui peut vous alléger le fardeau, par exemple en conservant les paramètres de constructeur comme requis par la logique de votre domaine au moment de l’exécution (conserver le constructeur de manière à révéler l’intention de dépendances de la classe et du domaine réel) et peut-être injecter d’autres classes d’aide dans les propriétés.

Vous voudrez peut-être aller plus loin avec Programmnig orienté aspect, qui est implémenté dans de nombreux cadres majeurs. Cela peut vous permettre d'intercepter (ou de "conseiller" d'utiliser la terminologie AspectJ) au constructeur de la classe et d'injecter les propriétés pertinentes, éventuellement avec un attribut spécial.

Tallmaris
la source
4
J'évite DI par les setters car cela introduit une fenêtre temporelle pendant laquelle l'objet n'est pas complètement initialisé (entre le constructeur et l'appel du setter). Ou en d’autres termes, il introduit un ordre d’appel de méthode (doit appeler X avant Y), ce que j’évite dans la mesure du possible.
RokL
1
DI par le biais de paramètres de propriété est idéal pour les dépendances facultatives. La journalisation est un bon exemple. Si vous avez besoin de journalisation, définissez la propriété Logger. Sinon, ne la définissez pas.
Preston
1

Les usines facilitent les tests et ne permettent pas de permuter facilement les implémentations. Ils ne font pas non plus apparaître de dépendances (par exemple, vous examinez une méthode, oubliant qu’elle appelle une méthode qui appelle une méthode qui appelle une méthode qui utilise une base de données).

Je ne suis pas tout à fait d'accord. Du moins pas en général.

Usine simple:

public IFoo GetIFoo()
{
    return new Foo();
}

Injection simple:

myDependencyInjector.Bind<IFoo>().To<Foo>();

Les deux extraits servent le même objectif, ils établissent un lien entre IFooet Foo. Tout le reste n'est que syntaxe.

Le passage Fooà ADifferentFooprend exactement autant d'effort dans l'un ou l'autre échantillon de code.
J'ai entendu des gens dire que l'injection de dépendance permet d'utiliser différentes liaisons, mais que le même argument peut être avancé pour la création d'usines différentes. Choisir la bonne reliure est aussi complexe que choisir la bonne usine.

Les méthodes d'usine vous permettent par exemple d'utiliser Foodans certains endroits et ADifferentFoodans d'autres. Certains peuvent appeler cela bon (utile si vous en avez besoin), d'autres peuvent appeler cela mauvais (vous pouvez faire un travail à moitié assommé pour tout remplacer).
Cependant, il n’est pas si difficile d’éviter cette ambiguïté si vous vous en tenez à une seule méthode qui revient, de IFoosorte que vous ayez toujours une source unique. Si vous ne voulez pas vous tirer une balle dans le pied, ne tenez pas un pistolet chargé ou ne vous visez pas le pied.


L'injection de dépendance gonfle massivement les listes d'arguments des constructeurs et couvre certains aspects de votre code.

C'est pourquoi certaines personnes préfèrent extraire explicitement les dépendances dans le constructeur, comme ceci:

public MyService()
{
    _myFoo = DependencyFramework.Get<IFoo>();
}

J'ai entendu les arguments pro (pas de constructeur gonflant), j'ai entendu les arguments con (l'utilisation du constructeur permet plus d'automatisation de DI).

Personnellement, alors que j'ai cédé à notre senior qui souhaite utiliser des arguments de constructeur, j'ai remarqué un problème avec la liste déroulante dans VS (haut à droite, pour parcourir les méthodes de la classe actuelle): les noms de méthodes disparaissaient de vue quand on des signatures de la méthode est plus longue que mon écran (=> le constructeur gonflé).

Sur le plan technique, je m'en fiche de toute façon. L'une ou l'autre option demande à peu près autant d'effort à taper. Et puisque vous utilisez DI, vous n’appellerez généralement pas un constructeur manuellement de toute façon. Mais le bogue de l'interface utilisateur de Visual Studio me fait préférer ne pas gonfler l'argument du constructeur.


En passant, l' injection de dépendance et les usines ne s'excluent pas mutuellement . J'ai eu des cas où, au lieu d'insérer une dépendance, j'ai inséré une fabrique générant des dépendances (NInject vous permet heureusement de l'utiliser Func<IFoo>, vous n'avez donc pas besoin de créer une classe de fabrique réelle).

Les cas d'utilisation pour cela sont rares mais ils existent.

Flater
la source
OP pose des questions sur les usines statiques . stackoverflow.com/questions/929021/…
Basilevs le
@Basilevs "La question est, comment puis-je fournir ces composants à d'autres composants."
Anthony Rutledge le
1
@Basilevs Tout ce que j'ai dit, mis à part la note latérale, s'applique également aux usines statiques. Je ne suis pas sûr de ce que vous essayez de préciser. Quel est le lien en référence à?
Flater
Un exemple d'utilisation d'une usine est-il un exemple d'utilisation d'une classe de requêtes HTTP abstraite et de stratégies pour cinq autres classes enfants polymorphes, une pour GET, POST, PUT, PATCH et DELETE? Vous ne pouvez pas savoir quelle méthode HTTP sera utilisée chaque fois que vous tenterez d'intégrer ladite hiérarchie de classes à un routeur de type MVC (qui peut dépendre en partie d'une classe de requêtes HTTP. L'interface de message HTTP PSR-7 est laide.
Anthony Rutledge,
@AnthonyRutledge: les usines DI peuvent signifier deux choses (1) Une classe qui expose plusieurs méthodes. Je pense que c'est ce dont vous parlez. Cependant, ce n'est pas vraiment particulier aux usines. Qu'il s'agisse d'une classe de logique métier avec plusieurs méthodes publiques ou d'une fabrique avec plusieurs méthodes publiques, cela relève de la sémantique et ne fait aucune différence technique. (2) Le cas d'utilisation spécifique aux DI pour les usines est qu'une dépendance non-usine est instanciée une fois (pendant l'injection), alors qu'une version usine peut être utilisée pour instancier la dépendance réelle à un stade ultérieur (et éventuellement plusieurs fois).
Flater
0

Dans cet exemple fictif, une classe de fabrique est utilisée au moment de l'exécution pour déterminer le type d'objet de requête HTTP entrant à instancier, en fonction de la méthode de requête HTTP. L'usine elle-même reçoit une instance d'un conteneur d'injection de dépendance. Cela permet à l’usine de déterminer le temps d’exécution et de laisser le conteneur d’injection de dépendance gérer les dépendances. Chaque objet de requête HTTP entrant comporte au moins quatre dépendances (superglobales et autres objets).

<?php
namespace TFWD\Factories;

/**
 * A class responsible for instantiating
 * InboundHttpRequest objects (PHP 7.x)
 * 
 * @author Anthony E. Rutledge
 * @version 2.0
 */
class InboundHttpRequestFactory 
{
    private const GET = 'GET';
    private const POST = 'POST';
    private const PUT = 'PUT';
    private const PATCH = 'PATCH';
    private const DELETE = 'DELETE';

    private static $di;
    private static $method;

    // public function __construct(Injector $di, Validator $httpRequestValidator)
    // {
    //    $this->di = $di;
    //    $this->method = $httpRequestValidator->getMethod();
    // }

    public static function setInjector(Injector $di)
    {
        self::$di = $di;
    }    

    public static setMethod(string $method)
    {
        self::$method = $method;
    }

    public static function getRequest()
    {
        if (self::$method == self::GET) {
            return self::$di->get('InboundGetHttpRequest');
        } elseif ((self::$method == self::POST) && empty($_FILES)) {
            return self::$di->get('InboundPostHttpRequest');
        } elseif (self::$method == self::POST) {
            return self::$di->get('InboundFilePostHttpRequest');
        } elseif (self::$method == self::PUT) {
            return self::$di->get('InboundPutHttpRequest');
        } elseif (self::$method == self::PATCH) {
            return self::$di->get('InboundPatchHttpRequest');
        } elseif (self::$method == self::DELETE) {
            return self::$di->get('InboundDeleteHttpRequest');
        } else {
            throw new \RuntimeException("Unexpected HTTP request. Invalid request.");
        }
    }
}

Le code client pour une configuration de type MVC, dans une configuration centralisée index.php, pourrait ressembler à ce qui suit (validations omises).

InboundHttpRequestFactory::setInjector($di);
InboundHttpRequestFactory::setMethod($httpRequestValidator->getMethod());
$di->set('InboundHttpRequest', InboundHttpRequestFactory::getRequest());
$router = $di->get('Router');  // The Router class depends on InboundHttpRequest objects.
$router->dispatch(); 

Sinon, vous pouvez supprimer la nature statique (et le mot clé) de la fabrique et permettre à un injecteur de dépendance de gérer le tout (d'où le constructeur mis en commentaire). Cependant, vous devrez changer certaines (pas les constantes) des références de membre de classe ( self) en membres d'instance ( $this).

Anthony Rutledge
la source
Les votes négatifs sans commentaires ne sont pas utiles.
Anthony Rutledge