Je n'aime pas les conteneurs d'injection de dépendances

Je n'ai rien contre les DI containers en tant que tels. Je pratique l'injection de dépendances aussi souvent que nécessaire mais j'ai un problème avec les librairies et frameworks en tout genre qui me donnent plus de complications à gérer que d'écrire le code correspondant aux 10% de fonctionnalités dont j'ai réellement besoin.

C'est pour cela que je lis avec grand plaisir des phrases telles que "Dependency Injection doesn’t require a framework; it just requires that you invert your dependencies and then construct and pass your arguments to deeper layers" venant de quelqu'un dont la crédibilité n'est plus à démontrer.

L'injection de dépendances, ce n'est jamais qu'un cas particulier de l'inversion de dépendances, c'est à dire d'une conception logicielle où les détails d'implémentation ne dépendent pas l'un de l'autre mais dépendent tous d'abstractions.
La spécificité de l'injection de dépendances, c'est, étant donnés un contexte et une abstraction, d'être capable de fournir automatiquement le détail d'implémentation de l'abstraction pour le contexte considéré. Bref, si on est capable d'implémenter l'appel suivant (écrit dans un pseudo langage objet), on est sur la bonne voie :

instance = context.GetDetailsFor(abstraction)

Une abstraction, c'est typiquement ce que, dans les langages orientés objets les plus courants, on appelle une "interface", Mais un contexte ? J'ai un théorème pour répondre à ça.

Théorème de oaz sur l'injection de dépendance

Le contexte d'une injection de dépendance peut se limiter à un choix de package

Démonstration

Une abstraction étant fixée, une injection de dépendance est le choix d'une classe implémentant cette abstraction et des arguments de construction d'une instance de cette classe.
Les arguments de constructions peuvent être rendus optionnels : il suffit pour cela d'utiliser une classe dérivée ad hoc où les arguments en question sont fixés.
Reste à choisir la classe à instancier.
Si une injection de dépendance nécessitait plus qu'un choix de package, cela signifierait que, en plus du package choisi, il faut d'autres informations qui permettent de choisir telle ou telle classe du package implémentant l'abstraction considérée. Cela implique que, parmi les classes du package choisi, au moins deux d'entre elles implémentent la même abstraction injectable et et ces deux ne vont donc pas être utilisées simultanément.
L'existence de ces deux classes dans le même package est une violation du Common Reuse Principle qui dit que "The classes in a package a reused together. If you reuse one of the classes in a package, you reuse them all"


Ce résultat théorique n'est pas toujours pratique à mettre en oeuvre (du fait de la nécessité d'une classe dérivée dans certains cas) mais pour un grand nombre de cas d'injection de dépendances, il suffit amplement.

Et, ce qui n'est pas pour me déplaire, dans un langage pas trop mal fichu, il me donne l'occasion d'écrire un "framework" basique d'injection de dépendances en 5 ou 6 lignes de code :

public static class MyDIContainer
{
  public static ABSTRACTION GetDetailsFor<ABSTRACTION>(this Assembly a)
  {
      var candidates = a.GetTypes().Where(t => typeof(ABSTRACTION).IsAssignableFrom(t));
      if(candidates.Count() != 1)
        throw new ApplicationException(string.Format("Cannot find unique implementation of {0} in {1}", typeof(ABSTRACTION), a));
      return (ABSTRACTION) Activator.CreateInstance(candidates.First());
  }
}

Ce "framework", qui se contente de retrouver dans un package donné l'unique classe qui implémente une interface donnée, s'utilise de la façon suivante :

var context = Assembly.LoadFile("DependenciesToInject.dll");
var instance = context.GetDetailsFor<IAmAnAbstraction>();

Une manière simple d'étendre ce "framework" pour prendre la main sur la phase de construction des instances, c'est de rajouter une interface "IProvideCustomDetails" et de l'implémenter dans les packages qui ont besoin de cette spécificité.

public interface IProvideCustomDetails
{
  ABSTRACTION GetDetailsFor<ABSTRACTION>() where ABSTRACTION : class;
}

public static class MyDIContainer
{
  public static ABSTRACTION GetDetailsFor<ABSTRACTION>(this Assembly a) where ABSTRACTION : class
  {
    return a.GetCustomDetailsFor<ABSTRACTION>() ?? a.GetUniqueImplementationFor<ABSTRACTION>();
  }
	
  public static ABSTRACTION GetUniqueImplementationFor<ABSTRACTION>(this Assembly a) where ABSTRACTION : class
  {
    var candidates = a.GetTypes().Where(t => typeof(ABSTRACTION).IsAssignableFrom(t));
    if(candidates.Count() != 1)
      throw new ApplicationException(string.Format("Cannot find unique implementation of {0} in {1}", typeof(ABSTRACTION), a));
    return (ABSTRACTION) Activator.CreateInstance(candidates.First());
  }
	
  public static ABSTRACTION GetCustomDetailsFor<ABSTRACTION>(this Assembly a) where ABSTRACTION : class
  {
    var provider = a.GetUniqueImplementationFor<IProvideCustomDetails>();
    return ( provider == null ) ? null : provider.GetDetailsFor<ABSTRACTION>();
  }
}

Bien évidemment, ce genre de code ne va pas rendre l'intégralité des services que rendrait un conteneur de DI "prêt à l'emploi" mais, en ce qui me concerne, je n'ai pas besoin de beaucoup plus que ça (typiquement j'aurais tendance à rajouter un passage de données de configuration à l'appel GetDetailsFor). Je n'ai donc aucune incitation pour utiliser des choses bien plus compliquées et qui nécessitent un suivi ne serait-ce que pour intégrer les mises à jour qui ne manquent pas de fleurir.

Peut être que ce choix n'est finalement qu'une question de contexte de développement. Un développeur qui vogue de projet en projet aura tout intérêt à débarquer avec une panoplie de connaissances directement utilisables. La maîtrise d'un ou plusieurs DI container du marché en fait partie. Cela peut permettre de gagner du temps lors d'un démarrage de projet.
Mais un développeur qui passe des années à faire évoluer les mêmes produits, a-t-il le même intérêt ?
Gardons cela pour un autre débat...

Jean-Baptiste

Je suis bien d'accord.

Si un jour tu viens du côté de java, je pense que tu vas adorer Guice, il peut se comporter par défaut plus ou moins exactement comme ce que tu viens de décrire.

Oaz

Ah bon ?
Si c'est le cas, c'est bien dommage de ne pas trouver cela facilement dans la doc de Guice. Tous ces "bind(machin).to(truc)" ça me file des boutons.

Oaz 15 février 2012 - 11:01
Jean-Baptiste

par défaut, si tu ne lui dis rien, quand tu lui demandes une instance, il prend la première implémentation qu'il trouve.

La syntax du bind je la trouve très explicite, que lui reproches-tu ? 

Oaz

Je n'ai pas de problème avec la syntaxe. C'est plutôt avec les concepts que je pense avoir du mal.
(Je n'ai jamais utilisé Guice donc ce que peux peux écrire doit être pris avec des pincettes)

Il me semble que c'est la notion de "module" qui me pose plus de problèmes.

Si je prends la notion de package telle que définie par Uncle Bob dans Principles of OOP,c'est à dire un .jar ou un .dll, alors toute notion supplémentaire servant à grouper des classes me parait superflue, voire dangereuse si elle permet de grouper les choses différemment.
Si en pratique le développeur s'en tient strictement à avoir 1 .jar = 1 AbstractModule alors pas de problème : l'AbstractModule représente ce que j'appelle IProvideCustomDetails dans mon exemple.
(et du coup le "bind" se limite à définir des arguments de construction puisque dans un package on ne trouve qu'une et une seule implémentation d'une interface donnée)

Je crois fermement que l'injection de dépendances et les notions de packages cohesion / packages coupling doivent être fortement liées.
Le risque d'un framework, c'est de pouvoir faire trop facilement n'importe quoi...

Oaz 15 février 2012 - 14:30
sfui

Faire facilement n'importe quoi c'est déjà possible simplement avec le langage. C'est pour cela que l'on se fixe des principes. Libre à toi de les appliquer ou non…
Avec Guice on peut faire des trucs très sympas (couplé par exemple avec le ServiceLoader) et des trucs… vraiment crades ;)
Bien qu'élégante, j'ai peur que ta solution soit légèrement limité dans certains cas.
Donc, comme tu l'as dit, connaître des frameworks est une bonne chose et, bien sûr, connaître les principes de la POO est vitale.

sfui 15 février 2012 - 16:58
Oaz

Bon... Je crois que je n'ai pas complètement réussi à faire passer l'idée que je voulais faire passer.
Ce n'est qu'un embryon de solution et d'ailleurs ce n'est même pas une solution, c'est juste une façon de voir les choses.

L'injection de dépendances, je préfère la bâtir en utilisant des concepts qui sont propres à mon logiciel et que l'on peut agrémenter en fonction des besoins.
Cela pourrait même aller jusqu'à l'utilisation d'un DI externe mais qui resterait caché derrière ces concepts. Donc la solution proposée n'est pas limitée, bien au contraire.

Ceci étant dit, je n'ai jamais eu besoin d'aller jusque là comme quoi les besoins dépendent du contexte et sont peut être moins étendus qu'on peut le penser.

Mais je pense qu'il y a là un point important : les frameworks ou outils externes (peu importe comment on les nomme) doivent rester des détails d'implémentation amovibles.
Ils doivent être complètement disjoints des concepts architecturaux du logiciel et ils ne doivent intervenir que lorsque leur nécessité est avérée.

Oaz 15 février 2012 - 18:00
Denis

Dans le code que tu proposes, je vois plus un Service Locator qu'un DI container.

Mais je suis entièrement d'accord sur le principe : l'important est d'inverser les dépendances (le D de SOLID), et l'utilisation de DI n'est qu'une option, qu'un moyen, pas une fin.

C'est pourquoi certains proposent le "poor man's DI" : http://blog.robbowley.net/2010/01/18/not-so-poor-mans-dependency-injection/ ce qui est le sujet de débats enragés : http://lostechies.com/chadmyers/200...

Denis 16 février 2012 - 00:12
Oaz

Pas tout à fait d'accord sur la similarité avec un Service Locator. Sur la forme, oui, parce qu'on va chercher le truc que l'on veut "s'auto-injecter" mais sur le fond c'est très différent (un package ne référence que les bindings qui le concernent alors qu'un Service Locator référence tous les bindings possibles)

Et oui, ce qui compte en premier, c'est le DIP, peu importe comment on le réalise.
Mais dès que le DIP est acquis (ce que je crois pas grand monde ne conteste), ce qui m'importe le plus c'est de ne pas mettre un quelconque outil implémentant ce DIP au coeur de mon système. Il faut inverser toutes les dépendances, y compris celles sur un outil d'inversion de dépendances...

Oaz 16 février 2012 - 12:54

Fil des commentaires de ce billet

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.