Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

Windows Phone, Android et iOS & Injection de dépendances et conteneurs IoC

[new:30/04/2015]L’injection de dépendances et les conteneurs IoC (inversion de contrôle) qu’est-ce que cela a à voir avec les trois OS cités ? Xamarin. Et au-delà les Xamarin.Forms. Mais encore plus les Xamarin.Forms.Labs ! Comment tout cela est-il lié ? Lisez la suite !

 

L’injection de dépendances

C’est un sujet dont je vous ai déjà parlé plusieurs fois et dans différentes occasions, dont le développement cross-plateforme. Mais avant d’entrer dans le vif du sujet de cet article faisons un point rapide : Qu’est-ce que l’injection de dépendances ?

Selon Wikipédia on nous dit :

L'injection de dépendances (Dependency Injection) est un mécanisme qui permet d'implémenter le principe de l'inversion de contrôle. Il consiste à créer dynamiquement (injecter) les dépendances entre les différentes classes en s'appuyant sur une description (fichier de configuration ou métadonnées) ou de manière programmatique. Ainsi les dépendances entre composants logiciels ne sont plus exprimées dans le code de manière statique mais déterminées dynamiquement à l'exécution.

Je ne sais pas qui a écrit cela mais j’imagine quelqu’un qui ne sait pas ce qu’est la DI (Dependency Injection) et je me demande si une telle définition peut l’éclairer vraiment…

Je préfère personnellement dire :

L’injection de dépendance est un ensemble de principes de conception et de patterns qui permettent de développer un code à faible couplage.

Certes cela impose d’expliquer la notion de faible couplage, mais ce n’est pas dur non plus :

La notion de couplage faible fait référence à la façon dont les classes et les instances interagissent entre elles. Si ces classes ou instances se “connaissent” et s’appellent directement le couplage est dit fort. Si les classes et instances peuvent communiquer, s’utiliser, s’appeler sans se connaitre (par ex. sans faire un “using” de l’assemblage de ces classes)  on parle de couplage faible.

On peut schématiquement représenter le concept d’injection de dépendances comme suit :

DependencyInjection_Solution

L’âme sur laquelle repose les implémentations de la DI est la notion d’interface. Une interface définit un contrat qui peut être implémenter par plusieurs classes. Les classes clientes peuvent utiliser le contrat au travers de l’interface sans avoir besoin de connaitre la définition des classes d’implémentation.

Le schéma ci-dessus n’est pas absolu, ce n’est qu’une façon de pratiquer l’injection de dépendances. Ici un Builder va créer une instance de “Client Class” en lui passant l’instance de Service1. Toutefois “Client Class” ne verra cette dernière que sous la forme de l’interface IService1 qu’elle supporte.

Où se trouve le couplage faible ? Entre “Client Class” et “Service1”. Comment ? grâce à “IService1” et au Builder. De fait “Client Class” utilisera Service1 sans connaitre la définition de cette classe, juste en se reposant sur la connaissance de IService1.

Intérêts ?

Le couplage faible apporte d’abord plus de clarté au code. Ensuite le découplage entre clients et services permet à tout moment de proposer aux clients des services d’un nouveau type sans que leur code ne soit impacté. Si je définis ILogger une interface pour faire des Logs, et si toutes les classes de mon application ne connaissent que ILogger il me sera très facile d’écrire des classes comme LogToXML, LogToDb, LogToCloud, etc qui chacune respecteront ILogger mais qui stockeront ou présenteront les Logs de façon différente. Et cela sans que jamais les classes clientes ne puissent voir ni connaitre la différence.

C’est puissant et pratique. On pense par exemple à l’extensibilité d’une application via des plugins. Les plugins supportent une ou plusieurs interfaces et le code client peut utiliser ces nouveaux services sans même le savoir.

Il existe donc quelque part une sorte d’aiguillage qui lui connait, par force, les véritables instances. C’est le Builder dans le schéma plus haut. Mais il y a d’autres façons de coder un tel couplage faible. Notamment par l’intermédiaire de conteneurs IoC (inversion de contrôle). A proprement parler ce ne sont que de simples dictionnaires qui associent un type (celui d’une interface) à un type de service (ou une instance existante d’un tel service). Avec les classes du schéma plus haut un conteneur IoC permettrait d’enregistrer le couple instance (ou type) de Service1 / IService1. Au lieu du Builder qui contrôle toutes les instanciations, c’est directement “Client Class” qui demanderait alors au conteneur IoC (un singleton) de lui fournit l’instance implémentant IService1. Le conteneur retournerait donc l’instance Service1.

Le conteneur IoC n’est pas indispensable à la mise en œuvre de l’injection de dépendances mais dans la pratique c’est un complément incontournable pour gagner en souplesse et éviter d’écrire un code trop fastidieux.

On peut ensuite inventer des tas de façons différentes de gérer chaque étape. Par exemple on peut avoir un fichier XML de configuration qui précise les couples interface / classe d’implémentation. La configuration est lue en début d’exécution de l’application et elle construit le dictionnaire. On peut aussi supposer à l’inverse un code qui automatiquement va balayer tous les assemblages en mémoire et qui va collecter toute les classes implémentant une interface et ayant par exemple un nom se terminant par “Service”… Cela ne vous dit rien ? C’est comme cela que MvvmCross pratique pour gérer les services de façon automatique.

De la même façon l’utilisation des services peut se pratiquer très simplement, la classe cliente réclame au conteneur l’instance de l’interface, ou bien de façon plus sophistiquée. Par exemple avec des attributs. C’est comme cela que MEF fonctionne. On peut aussi avoir des classes clientes qui exposent un constructeur recevant en paramètre tous les services nécessaires à leur fonctionnement. Il existe alors bien quelque part un “builder” qui permet d’instancier les clients et qui va analyser le constructeur pour passer directement en paramètres les services (en regardant dans le dictionnaire du conteneur IoC et par réflexion pour analyser le constructeur et ses paramètres).

Bref le principe est simple mais on peut le complexifier, le sophistiquer, l’automatiser à son gréé ce qui donne des tas de solutions possibles, des tas de frameworks plus ou moins difficiles d’ailleurs à prendre en main.

Mais au bout du compte on retrouvera toujours un conteneur IoC qui n’est qu’un dictionnaire de couples interface / classe d’implémentation (ou instances existantes).

Et forcément selon comment ce dictionnaire est géré impacte à la fois sur les possibilités plus ou moins larges de gérer l’injection de dépendances elle-même ainsi que les performances globales de l’application.

C’est pourquoi on trouve de nombreux conteneurs IoC !

Choisir l’un plutôt que l’autre dépendra donc directement des priorités qu’on se donne en retrouvant souvent les balances entre richesse fonctionnelle et temps de prise en main, complexité du code et rapidité d’exécution.

Un dernier mot sur ce que n’est pas l’injection de dépendance.

Il faut savoir désapprendre pour apprendre, savoir faire le vide des idées reçues pour en accepter d’autres plus proches de la vérité. Dit comme ça on est en plein dans les leçons de sagesse un peu bidon d’un film d’Arts Martiaux à la sauce américaine. Ok. Mais ce n’est pas faux pour autant !

Il y a certaines choses sur l’injection de dépendances qui sont dites et redites et qui égarent celui qui veut comprendre ce mécanisme.

Par exemple certains présentent la DI comme une simple façon plus “branchée” d’appeler le late binding (ligature différée). Si la DI peut servir à faire du late binding ce n’est pas son but premier. Ce n’est qu’une utilisation possible.

D’autres encore parlent de la DI comme une technique uniquement utile pour les tests unitaires. Comme pour le late binding ce n’est pas totalement faux, mais c’est trop réducteur. En effet la DI peut simplifier les tests unitaires (en fournissant des mocks au lieu de services réels par exemple), mais là encore ce n’est pas son but et encore moins son but unique.

On peut aussi entendre parler de la DI comme une sorte de copie dopée du design pattern “abstract factory”. Souvent même on présente la DI comme un service permettant de localiser d’autres services. Mais cela porte un autre nom, c’est le Service Locator… Mais il reste possible d’utiliser un conteneur IoC pour mettre en œuvre une abstract factory ou un service locator, c’est là que les apparences deviennent trompeuses. Prenez par exemple le ViewModelLocator de Mvvm Light, il utilise désormais un conteneur IoC pour mémoriser les ViewModels. C’est un locator, c’est aussi une sorte de factory, il utilise un conteur IoC donc quelque part de la DI mais cela n’a rien à voir avec cette dernière en réalité… C’est parfois un peu confusant je l’accorde et l’erreur est fréquente même chez des informaticiens confirmés.

Enfin, il semble admis comme une vérité première qu’il ne peut y avoir de DI sans un conteneur. C’est tout aussi faux. Au même titre que j’ai démontré dans plusieurs articles comment mettre en œuvre MVVM en se passant de toute librairie externe, on peut parfaitement implémenter une DI sans utiliser un conteneur IoC. Cela peut réclamer plus de code et l’intérêt n’est pas forcément flagrant mais en tout cas il n’y a pas de lien obligatoire entre DI et conteneur IoC.

Bref, la DI sert à concevoir un code à couplage faible et c’est tout. Après on peut tirer avantage de ce couplage faible pour simplifier des tests unitaires, pour créer une factory, un locator, etc…

Le Hello World de l’injection de dépendances

On comprend toujours mieux avec un exemple. En voici un dont le schéma UML reprend exactement le schéma présenté plus haut :

image

 

Il s’agit d’une application Console. La méthode Main joue le rôle du Builder. C’est ici que sont créées d’une part l’instance de la classe cliente (Client Class dans le premier schéma, “Salutation” ici) et d’autre part l’instance du service (Service1 dans le 1er schéma, “ConsoleMessageWrite” ici). La classe cliente utilisera ici aussi le service au travers d’une interface (IService1 dans le 1er schéma, “IMessageWriter” ici).

Voici le code de la classe Salutation :

public class Salutation
{
    private readonly IMessageWriter writer;
    public Salutation(IMessageWriter writer)         
    {
        if (writer == null)
        {
            throw new ArgumentNullException("writer");
        }
        this.writer = writer;
    }
    public void SayHello()
    {
        this.writer.Write("Hello World!");              
    }
}

 

Il s’agit de la classe cliente, celle qui utilise le service. On voit qu’elle possède un constructeur acceptant en paramètre l’instance d’une interface, IMessageWriter. C’est grâce à ce service que sa méthode “SayHello” peut afficher le message.

SayHello() sait quoi écrire mais elle ne sait pas comment le faire et doit utiliser un service qui lui ne sait pas quoi écrire mais qui sait comment le faire.

L’interface est donc définie comme cela :

public interface IMessageWriter
{
    void Write(string message);
}

 

Toute classe supportant IMessageWriter devra exposer une méthode Write(string message).

Le service sera donc implémenté dans notre exemple par une classe supportant IMessageWriter :

public class ConsoleMessageWriter : IMessageWriter
{
    public void Write(string message)
    {
        Console.WriteLine(message);
    }
}

 

La classe ConsoleMessageWriter définit la méthode Write de l’interface. Elle prend le message à afficher et se sert de la Console pour l’écrire.

On pourrait maintenant penser à une autre classe LogMessageWriter qui n’afficherait pas le message mais l’enregistrerait dans un fichier de Log. La classe cliente “Salutation” présentée plus haut n’en saurait rien, c’est le builder qui fournirait l’une ou l’autre version du service… Il y a donc un couplage faible entre “Salutation” et le service réel ConsoleMessageWriter. Et ce grâce à l’interface IMessageWriter et la présence d’un builder (le Main() ici) qui va instancier le service et le passer en paramètre à l’instance de la classe cliente…

Le Main() de l’exemple, c’est à dire le Builder du premier schéma, ressemble donc à cela :

private static void Main()
{
    IMessageWriter writer = new ConsoleMessageWriter();
    var salutation = new Salutation(writer);
    salutation.SayHello();
}

 

Moralité : nous venons de faire de l’injection de dépendances totale et parfaite sans l’ombre d’un framework ou d’un conteneur IoC …

Bien entendu dans la réalité les choses sont un poil plus complexe que dans ce “Hello World” de la DI… Mais les principes restent rigoureusement les mêmes. C’est cette complexification qui peut amener à utiliser des frameworks ou des conteneurs IoC. Mais rien de tout cela n’est obligatoire pour implémenter proprement de l’injection de dépendances.

Xamarin.Forms et les Xamarin.Forms.Labs

Et c’est ici que toutes les pièces du puzzle viennent s’emboiter ! Car maintenant que savons ce qu’est l’injection de dépendances et les conteneurs IoC, en ajoutant les Xamarin.Forms nous ajoutons la glu qui colle ensemble : Windows Phone, iOS, Android et même WinRT (encore expérimental quand j’écris ces lignes mais bientôt releasé). Avec les Xamarin.Forms on est dans le monde du cross-plateforme et comme j’ai pu le montrer dans des articles ou des vidéos, la variabilité entre les plateformes imposent la technique de l’injection de dépendances pour qui veut écrire un code unique et portable.

Par exemple comme écrire un ViewModel totalement cross-plateforme qui utilise le niveau de charge de la batterie alors que ce genre d’information est par nature native ? Tout simplement en créant une interface qui retourne la valeur de la charge au sein du projet cross-plateforme et en mettant des classes d’implémentation natives dans chaque projet spécifique à une plateforme. Classes implémentant l’interface et enregistrant dans un conteneur IoC le couple type interface / type classe d’implémentation. Dès lors le code unique cross-plateforme peut utiliser le service “charge de la batterie” de façon totalement naturelle, au runtime c’est le mécanisme de DI qui fournira qui à Windows Phone, qui à Android la bonne instance de la classe native sachant retourner l’information… On retrouve ici tous les ingrédients que j’avais traité sur dans une vidéo YouTube et portant sur l’injection de code natif dans une application cross-plateforme (utilisant MvvmCross mais la problématique est générique).

Mais ce n’est qu’un exemple.

L’inversion de contrôle (IoC) est partie intégrante des Xamarin.Forms et des Xamarin.Forms.Labs. Utiliser l’injection de dépendance dans ce type d’application est naturel. Savoir utiliser pour votre propre code la puissance de ces outils vous permettra de concevoir un code de meilleur qualité.

Quand on fait appel à DependencyService dans une application Xamarin.Forms on utilise en fait un service locator intégré au framework. Ce service locator utilise l’inversion de contrôle. En réalité l’injection de dépendances est devenue au fil du temps l’un des patterns préférés pour mettre en œuvre l’inversion de contrôle car la DI demande à ce que tout le code respecte les mêmes règles de façon claire. Et comme la clarté est l’un des buts recherchés…

Les Xamarin.Forms.Labs, projet open source qui est au Xamarin.Forms un peu l’équivalent de ce qu’est le toolkit à WPF, apportent de nombreux compléments aux Xamarin.Forms. J’en ai présenté l’essentiel dans un article plus ancien. Parmi les ajouts marquant on trouve un mini framework MVVM très suffisant pour des applications mobiles. Son intégration aux Xamarin.Forms le rend à mon sens plus adapté que Mvvm Light même si j’aime beaucoup ce dernier et son adaptation assez récente à Xamarin.

Xamarin.Forms.Labs et l’injection de dépendance

La première chose à bien comprendre ici est que les Labs fournissent un niveau d’abstraction au-dessus du conteneur DI utilisé ce qui permet d’utiliser différents conteneurs de la même façon.

Cette généralisation est matérialisée par la classe statique Resolver :

var resolverContainer = new SimpleContainer();
Resolver.SetResolver(resolverContainer.GetResolver());

 

La première ligne créée un conteneur, ici celui fourni par défaut (SimpleContainer); la seconde ligne enregistrant ce conteneur comme étant celui que le Resolver devra utiliser. A partir de cet instant le Resolver est capable d’enregistrer des couples interface / type ou instance de services et bien entendu de les restituer selon les besoins des classes clientes.

L’intérêt des Labs ici est que nulle part dans votre code vous n’aurez à référencer SimpleContainer pour utiliser la DI. C’est un niveau d’abstraction supplémentaire, un autre niveau découplage qui participe à la conception d’un code plus maintenable.

On notera que le code ci-dessus se répète quasi à l’identique dans les projets natifs car ce sont eux qui fournissent et instancient les services natifs. Le code central et portable ne faisant usage que de la couche d’abstraction. Il s’avère que le code de SimpleContener est le même pour toutes les plateformes mais il pourrait en être autrement. C’est le cas par exemple pour les informations sur l’unité mobile.

Les Labs proposent notamment un objet Device bien pratique qui retourne des informations natives. Si l’interface du service est unique, chaque projet spécifique instancie une classe qui là est bien différente car les moyens d’accéder aux informations sont différents selon la plateforme. On retrouve ainsi un AppleDevice.CurrentDevice, un AndroidDevice.CurrentDevice and un WindowsPhone.CurrentDevice. Chacune de ces classes implémentant l’interface IDevice. Par exemple cette dernière est déclarée dans XLabs.Platform qui est partagée par tous les projets mais AndroidDevice.CurrentDevice se trouve dans l’assemblage XLab.Current.Droid qui est codé spécifiquement pour Android. Et c’est comme cela que le conteneur IoC et la DI deviennent un socle pour créer des applications cross-plateforme !

On notera que les Xamarin.Forms et les XLabs reprennent en réalité des mécanismes qu’on trouvait déjà dans MvvmCross que j’ai largement présenté et sur lequel je m’appuie dans la série de 12 vidéos YouTube présentant le développement cross-plateforme.

L’enregistrement de IDevice en utilisant le SimpleContainer utilisera la méthode SetIoc() de chaque projet natif :

private void SetIoc()
{
	var resolverContainer = new SimpleContainer();
		
	resolverContainer.Register<IDevice>(r => AndroidDevice.CurrentDevice);
		
	Resolver.SetResolver(resolverContainer.GetResolver());
}

 

L’exemple ci-dessus concerne l’application native Android. Pour Windows Phone c’est la seconde ligne de code qui sera modifiée pour utiliser WindowsPhone.CurrentDevice tout simplement (et de façon similaire donc pour iOS).

Maintenant que nous possédons un conteneur IoC et que nous y avons enregistré l’instance du service, notre code cross-plateforme peut utiliser sans problème des informations qui lui était pourtant à jamais inaccessibles !

Imaginons un ViewModel de notre application qui a besoin de prendre connaissance des informations de la Device, il suffit maintenant d’ajouter un paramètre de type IDevice à son constructeur pour faire de l’injection de dépendance :

public class MainViewModel : XLabs.Forms.Mvvm.ViewModel
{
	private readonly IDevice _device;
	private string _message;

	public MainViewModel(IDevice device)
	{
		_device = device;
		Message = String.Format("Hello XLabs MVVM! your device is a {0}",
		device.Manufacturer);
	}
 
	public string Message
	{
		get { return _message; }
		set { SetProperty(ref _message, value); }
	}
}

 

Ce ViewModel très simple possède ainsi un constructeur prêt pour l’injection de dépendances, ici une instance du service couvert par l’interface IDevice. Ce service est stocké par le ViewModel pour son utilisation ultérieur, c’est une bonne pratique même si ici cela n’est absolument nécessaire puisque le service est utilisé immédiatement pour créer un message de bienvenue indiquant le nom du fabricant de l’unité mobile. Mais les exemples sont très réducteurs par obligation, dans la réalité conserver le service est souvent obligatoire.

La question qui se pose maintenant est de savoir comment l’instance de notre ViewModel va-t-elle pouvoir “recevoir” le fameux paramètre que nous avons ajouté à son constructeur ?

Beaucoup de conteneurs IoC savent analyser par réflexion les différents constructeurs d’une classe pour lui fournir automatiquement les instances des services qu’elle utilise. Mais SimpleContainer est… comment dire … Simple ! De fait avec ce dernier il faudra explicitement indiquer au conteneur comment créer l’instance du ViewModel.

Le moyen le plus simple pour y arriver est d’utiliser la méthode SetIoc() de chaque plateforme. Directement après l’enregistrement du conteneur nous pouvons ajouter une ligne telle que la 3ème de ce code :

private void SetIoc()
{
    var resolverContainer = new SimpleContainer();
 
    resolverContainer.Register<IDevice>(r => AndroidDevice.CurrentDevice);
    resolverContainer.Register<MainViewModel>(r => new MainViewModel(r.Resolve<IDevice>()));
 
    Resolver.SetResolver(resolverContainer.GetResolver());
}

 

La 3ème ligne enregistre pour le type MainViewxModel une expression Lambda d’intialisation, celle-ci ne sera invoquée que si on demande au conteneur de retourner une instance de MainViewModel (c’est l’avantage d’utiliser une expression d’initialisation plutôt que de passer un “new servicemachin()” qui lui instancierait immédiatement un service peut-être jamais utilisé au runtime).

L’expression d’initialisation créée l’instance de MainViewModel en passant en paramètre un Resolve<IDevice> … et le tour est joué !

Bon, soyons honnêtes l’un des grands bénéfices d’un conteneur IoC est justement de pouvoir gérer les constructeurs et de faire l’injection de dépendances tout seul. Par défaut le SimpleContainer est une classe qui n’est qu’un dictionnaire de couples interface / type (ou instance). C’est bien pratique mais on peut faire mieux.

C’est pourquoi les XLabs fournissent un système d’abstraction pour SImpleContaineur… car justement ils permettent d’utiliser d’autre conteneurs plus sophistiqués mais avec la même interface.

XLabs et les conteneurs IoC

Comme je viens de le dire SimpleContaineur n’est que le conteneur par défaut. Il est simple, ne pèse presque rien et fonctionne rapidement. Mais il est simple.

Et comme je le disais plus haut, si les principes de l’injection de dépendances ne nécessitent pas obligatoirement la présence d’un conteneur IoC ce dernier permet une mise en œuvre plus souple. Toutefois comme il est possible de privilégier tel ou tel aspect il existe plusieurs “visions” de la chose d’où l’existence de multiples conteneurs.

Les XLabs proposent une notion de plugins qui permet d’en étendre le fonctionnement en mode cross-plateforme exactement comme le fait depuis longtemps MvvmCross. On dispose d’une interface partagée et utilisée par le noyau et d’autant d’implémentations spécifiques qu’on a de cibles natives. Les XLabs sont conçus de cette façon, les Xamarin.Forms aussi, et les plugins pour XLabs ne font que reproduire d’une façon un peu fractale le même principe…

Les XLabs proposent de base certains plugins notamment pour les conteneurs IoC. On dispose ainsi de plusieurs “visions” et on choisi celle qui correspond le mieux au projet en cours. Pour l’instant, et ce n’est déjà pas mal du tout, les XLabs proposent des plugins pour Autofac, Ninject, TinyIoC et Unity. Si on y ajoute SimpleContainer ce sont 5 conteneurs IoC différents parmi lesquels on peut faire son choix.

Mais comment faire ce choix ?

Disons le tout de suite ce n’est pas parce qu’il y a du choix que cela justifie de se prendre la tête et qu’il faille utiliser à tout prix des conteneurs différents dans chaque projet !

Bien entendu s’il y a le choix c’est bien parce qu’il y a des nuances dans les services rendus et que c’est par l’analyse de ces derniers que votre choix devra se faire.

L’un des services rendus dont nous avons parlé plus haut et qui peut justifier de choisir un conteneur plus qu’un autre réside par exemple dans la capacité à rendre l’injection de dépendances automatique ou non.

On se rappelle l’exemple où je montrais comment enregistrer une expression lambda d’initialisation dans SimpleContaineur au niveau de chaque projet natif pour prendre en charge l’injection de dépendances du service IDevice dans le constructeur de MainViewModel.

Voici un excellent point de divergence entre un SimpleContainer, simple, et d’autres conteneurs IoC qui peuvent apporter une réponse plus automatisée à cette problématique.

Mais il existe d’autres nuances qui peuvent influer le choix d’un conteneur plus que d’un autre. La rapidité d’exécution par exemple. A ce sujet on lira avec intérêt le post de Daniel Palme qui a testé des dizaines de conteneurs. On retiendra outre les performances brutes, la richesse des fonctionnalités qu’il résume dans le tableau suivant :

  PerformanceConfigurationFeaturesEnvironment
Container   CodeXMLAutoAutowiringCustom lifetimesInterceptionAuto diagnostics.NETSLWP7WP8WinRT
AutoFac Average Yes Yes Yes Yes No Yes No Yes Yes Yes Yes Yes
Caliburn.Micro Average Yes No No Yes No No No Yes Yes Yes Yes Yes
Catel Average Yes Yes Yes Yes Yes Yes No Yes Yes Yes Yes Yes
DryIoc Fast Yes No No Yes Yes No No Yes No No No No
Dynamo Fast Yes No Yes Yes Yes No No Yes No No No No
fFastInjector Fast Yes No No Yes No No No Yes Yes Yes No Yes
Funq Fast Yes No No No No No No Yes Yes Yes No No
Grace Fast Yes Yes Yes Yes Yes Yes Yes Yes No No Yes Yes
Griffin Fast Yes No No Yes No Yes No Yes No No No No
HaveBox Fast Yes No No Yes No Yes No Yes Yes Yes Yes No
Hiro Fast Yes No No Yes No No No Yes No No No No
IfInjector Fast Yes No No Yes No No No Yes Yes Yes No Yes
LightCore Average Yes Yes Yes Yes Yes No No Yes Yes No No No
LightInject Fast Yes No Yes Yes Yes Yes No Yes Yes No Yes Yes
LinFu Slow Yes No No Yes No Yes No Yes No No No No
Maestro Average Yes No No Yes Yes Yes No Yes Yes Yes Yes Yes
MEF Slow Yes No No Yes No No No Yes Yes No No Yes
MEF2 Fast Yes No No Yes No No No Yes No Yes Yes Yes
MicroSliver Average Yes No No Yes No No No Yes Yes No No Yes
Mugen Average Yes No No Yes Yes Yes No Yes Yes Yes Yes Yes
Munq Fast Yes No No Yes Yes No No Yes No Yes No No
Ninject Slow Yes Yes Yes Yes Yes No No Yes Yes Yes No Yes
Petite Fast Yes No No No No No No Yes No No No No
QuickInject Fast Yes No Yes Yes Yes No Yes Yes No No No No
SimpleInjector Fast Yes No Yes Yes Yes Yes Yes Yes Yes No Yes Yes
Speedioc Fast Yes No No No No No No Yes No No No No
Spring.NET Very slow No Yes No Yes No Yes No Yes No No No No
Stiletto Average Yes No No Yes No No No Yes Yes Yes Yes No
StructureMap Average Yes Yes Yes Yes Yes Yes Yes Yes No Yes No Yes
StyleMVVM Fast Yes No Yes Yes Yes No No Yes No No Yes No
TinyIoc Average Yes No No Yes Yes No No Yes Yes Yes No No
Unity Average Yes Yes Yes Yes Yes Yes No Yes Yes No Yes Yes
Windsor Average Yes Yes Yes Yes Yes Yes Yes Yes Yes No No No

 

Tous ces conteneurs ne sont pas forcément utilisables sur toutes les plateformes, je vous donne ce tableau uniquement pour que vous puissiez prendre conscience du nombreux incroyable d’implémentations proposées et donc des façons différentes de voir le concept. Pour le reste je vous renvoie à l’article cité en référence qui propose notamment des benchs entre ces différents conteneurs.

Cela permet aussi de remarquer les principales possibilités qu’on prend généralement en compte pour choisir un conteneur. Outre les performances on note par exemple l’autowiring dont je parlais, à savoir la capacité à injecter les dépendances automatiquement ou non. Et cette même possibilité peut être implémentée de façons très différentes (attributs, fichier de configuration XML, règle de nommage avec balayage automatique des assemblages, etc).

Autre critère important, la gestion personnalisée du cycle de vie des services. Lorsqu’on enregistre un service certains conteneurs permettent en effet de préciser s’il s’agira ou non d’un singleton par exemple, ou d’autres mode de gestion du cycle de vie plus complexes. Pour de petites applications simples c’est certainement une considération non primordiale, mais dans certains cas ou dans des applications plus sophistiquées cela peut devenir absolument essentiel…

Les XLabs ne fournissent pas tous les conteneur du tableau ci-dessus, juste 4 en plus de SimpleContainer. Mais il s’agit juste de plugins XLabs c’est à dire d’enveloppes autour de conteneurs existants. Si on désire utiliser autre chose que le conteneur par défaut, outre l’utilisation du plugin XLabs correspondant il faudra bien entendu installer le package Nuget officiel. Ici aussi on conserve un niveau de découplage intéressant puisque les XLabs restent indépendants des versions et des évolutions des conteneurs supportés qui restent sous le contrôle de leurs créateurs respectifs.

Ce principe de couplage faible qu’on retrouve à tous les niveaux montre bien à la fois sa simplicité et la paradoxale richesse qui en découle !

Autofac

Le plugin s’appelle XLabs.IoC.Autofac, ce qui n’a rien de surprenant… et il faut l’installer sur toutes les plateformes, ce qui n’a rien d’étonnant non plus puisque nous savons qu’il y a toujours dans ce mécanisme une interface portable accompagnée d’une implémentation spécifique par plateforme.

Vous trouverez toutes les informations sur ce conteneur sur son site officiel autofac.org

Autofac permet d’enregistrer automatiquement par balayage du code source tous les services selon un principe qu’on retrouvait dans MvvmCross. Il permet aussi de faire automatiquement de l’injection de propriétés ou de méthodes.

Bien que ces performances ne soient pas exemplaires (ce qui n’a que fort peu d’impact en général pour des applications mobiles relativement simples il faut l’avouer) c’est un conteneur très complet et très utilisé.

A vous de voir si vous avez besoin de toutes ces sophistications ou si SimpleContainer est finalement suffisant…

Ninject

Ninject a été construit pour rester simple et compréhensible mais en offrant des possibilités plus étendues que SimpleContainer, notamment l’injection des dépendances automatisées ou en tout cas plus automatisées. Autofac est certainement plus complet sur ce point.

Le site officiel de Ninject vous en apprendra beaucoup plus que quelques lignes ici alors n’hésitez pas à le consulter !

TinyIoC

Tiny est un synonyme de Simple … On reste donc dans le principe d’un conteneur simple n’offrant pas forcément toutes les possibilités de Autofac. Même son site officiel est “tiny” puisqu’il faut se contenter d’un mini Wiki sur Github.

Toutefois l’appellation est trompeuse car TinyIoC propose des choses évoluées comme l’auto-enregistrement des services par exemple ou bien une gestion embryonnaire du cycle de vie en ayant la possibilité d’enregistrer des services multi-instances ou sous forme de singletons.

Là encore chacun fera en fonction de ses besoins, encore faut-il bien connaitre chaque conteneur pour faire un choix éclairé !

Unity

Unity est le conteneur proposé par le groupe Patterns & Practices de Microsoft. Ce n’est pas pour cela qu’il est le meilleur, comme on l’a vu le meilleur est surtout celui qui est le mieux adapté à un projet donné et que vous saurez maitriser sans problème ! Mais forcément c’est un conteneur très utilisé, bien écrit et bien débogué.

Le site officiel de Unity est riche en documentations de tout genre et en faire la lecture ici n’aurait pas d’intérêt, je vous laisse le faire vous-mêmes !

Conclusion

Je m’aperçois qu’il est temps de conclure ce très long article… Le sujet est si vaste qu’on pourrait écrire des pages et des pages… d’ailleurs il existe des ouvrages uniquement dédiés à l’injection de dépendances (en anglais pour l’essentiel, si vous en connaissez en français qui vaille le détour n’hésitez pas à le dire en commentaire !).

Mais la DI et les conteneurs IoC ont déjà été abordés dans d’autres articles ce qui m’intéressait ici était de vous présenter ces notions au service du développement cross-plateforme avec Xamarin, les Xamarin.Forms et les Xamarin.Forms.Labs (ou XLabs).

Si on comprend généralement bien ce que Xamarin peut être, si on comprend bien le principe de conteneurs IoC, il reste souvent le plus difficile : comprendre comment tout cela marche ensemble dans un tout cohérent et maitrisable !

C’est souvent le but de mes articles, celui-ci comme tant d’autres (MvvmCross, Mvvm Light, …), montrer qu’en s’appuyant sur des choses simples on peut construire des logiciels puissants pour peu qu’on sache comment mélanger les ingrédients, ce qui est finalement la partie la plus difficile…

Stay Tuned !