Inversion de contrôle en .net

Préambule

Définitions

L’inversion de contrôle (abrégée IoC) est un patron d’architecture courant en programmation orientée objet. Son principe de base est que, lorsqu’un module effectue un traitement, le contrôle du traitement soit déporté vers l’appelé, et non pas vers l’appelant. En d’autres termes, on cherche à minimiser la connaissance qu’à l’appelant de la mécanique interne de l’appelé.

Au delà de cette définition, on désigne souvent par cet acronyme IoC – par abus de langage – un patron de conception : l’inversion de dépendance (soit la dernière lettre des principes de conception S.O.L.I.D.).

Ainsi pour diminuer le couplage fort entre des classes, on va ajouter des interfaces implémentées par ces classes, de façon à ce qu’au lieu d’appeler une implémentation, l’appelant appelle une interface, ceci permettant d’ajouter un niveau d’abstraction supplémentaire entre l’appelant et l’appelé. Charge à l’appelant de fournir les implémentations de ces interfaces pour faire le traitement (via des propriétés ou le constructeur).

Intérêts

La mise en pratique de ces principes permettent plusieurs choses intéressantes, gravitant autour d’une même notion : la maintenabilité

  • La visualisation rapide des dépendances entre les objets;
  • Une configuration différente en fonction d’un contexte d’exécution;
  • L’isolation des traitements pour les tests automatiques (via des implémentations bouchon des dépendances);
  • L’interception, permettant de fournir comme implémentation des interfaces, des proxys sur les implémentations finales, afin d’effectuer des traitements périphériques sans alourdir le code du traitement final (ex : ajout de métriques d’exécution, de logs, etc.).

Mise en œuvre

Frameworks

Ces principes posés, il existe une bonne trentaine de frameworks en .net permettant de mettre facilement en œuvre ce qu’on appelle de l’injection de dépendance. Certains sont des portages d’autres langages comme le Java, où ces pratiques ont plus d’ancienneté (les premiers frameworks .net datent d’une dizaine d’années).

Leurs principes de base sont une partie déclarative des interfaces existantes, des implémentations associées, et l’appel au framework pour récupérer une implémentation de l’interface souhaitée, pour faire les traitements côté client. Charge au framework d’instancier l’implémentation de l’interface souhaitée ainsi que ses différentes dépendances, et de gérer leur cycle de vie.

Certains passent par une configuration XML – plus ou moins verbeuse – centralisée (permettant de ne pas recompiler pour passer d’une implémentation à l’autre), d’autres par du code, d’autres permettent les deux.

Après consultation de benchmarks et descriptifs des frameworks, les principaux à retenir sont les suivants :

Les syntaxes d’utilisation diffèrent quelque peu, mais sont souvent similaires, et restent une question de goût. Ci-après, le même exemple décliné sur les différents framework cités :

Soit les interfaces et leurs implémentations respectives suivantes :

1
2
3
4
5
6
7
8
9
public interface IService { }
public class SomeService : IService { }
public interface IClient { IService Service { get; } }
public class SomeClient : IClient
{
   public IService Service { get; private set; }
   public SomeClient(IService service) { Service = service; }
}

L’instanciation sans injection de dépendance se ferait ainsi :

1
IClient client = new SomeClient(new SomeService());

Avec injection de dépendance, voici les différentes syntaxes :

DryIoC :

1
2
3
4
// Référencement
Container c = new Container();
c.Register<IClient, SomeClient>();
c.Register<IService, SomeService>();
1
2
// Résolution
IClient client = c.Resolve<IClient>();

Grace :

1
2
3
4
5
6
// Référencement
DependencyInjectionContainer c = new DependencyInjectionContainer();
c.Configure(cfg => {
   cfg.Export<SomeClient>().As<IClient>();
   cfg.Export<SomeService>().As<IService>();
});
1
2
// Résolution
IClient client = c.Locate<IClient>();

SimpleInjector :

1
2
3
4
// Référencement
Container c = new Container();
c.Register<IClient, SomeClient>(Lifestyle.Transient);
c.Register<IService, SomeService>(Lifestyle.Transient);
1
2
// Résolution
IClient client = c.GetInstance<IClient>();

LightInjector :

1
2
3
4
// Référencement
LightInject.ServiceContainer c = new LightInject.ServiceContainer();
c.Register<IClient, SomeClient>(Lifestyle.Transient);
c.Register<IService, SomeService>(Lifestyle.Transient);
1
2
// Résolution
IClient client = c.GetInstance<IClient>();

StructureMap :

1
2
3
4
5
// Référencement
Container c = new Container(cfg => {
   cfg.For<IClient>().Use<SomeClient>();
   cfg.For<IService>().Use<SomeService>();
});
1
2
// Résolution
IClient client = c.GetInstance<IClient>();

Unity :

1
2
3
4
// Référencement
UnityContainer c = new UnityContainer();
c.RegisterType<IClient, SomeClient>();
c.RegisterType<IService, SomeService>();
1
2
// Résolution
IClient client = c.Resolve<IClient>();

AutoFac :

1
2
3
4
5
// Référencement
ContainerBuilder b = new ContainerBuilder();
b.RegisterType<SomeClient>().As<IClient>();
b.RegisterType<SomeService>().As<IService>();
Container c = b.Build();
1
2
3
4
5
// Résolution
using (var scope = c.BeginLifetimeScope())
{
   IClient client = scope.Resolve<IClient>();
}

Castle Windsor :

1
2
3
4
// Référencement
WindsorContainer c = new WindsorContainer();
c.Register(Component.For<IClient>().ImplementedBy<SomeClient>());
c.Register(Component.For<IService>().ImplementedBy<SomeService>());
1
2
// Résolution
IClient client = c.Resolve<IClient>();

Et MEF dans tout ça ?

Managed Extensibility Framework est une composante du framework .net, fournie par Microsoft, permettant de faire de l’injection de dépendance par code, par le biais d’une syntaxe par attribut assez facile à comprendre.

Si ce framework, dans sa première version, ne brille pas par ses performances, il rajoute une fonctionnalité d’exploration, permettant de charger à l’exécution des dll dont il pourra extraire les implémentations d’interfaces connues pour injecter des dépendances. Cela répond facilement à des scénarios de modules / extensions que les autres frameworks ne couvrent pas (du moins nativement).

1
public interface IService { }
1
2
[Export(typeof(IService))]
public class SomeService : IService { }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SomeClient
{
   [Import]
   public IService Service { get; set; }
   public SomeClient()
   {
      Compose()
   }
   private void Compose()
   {
      // Chargement des dll d’un répertoire donné pour y trouver les [Export]
      AggregateCatalog cat = new AggregateCatalog();
      DirectoryCatalog dCat = new DirectoryCatalog(Environment.CurrentDirectory);
      cat.Catalogs.Add(dCat);
      c = new CompositionContainer(cat);
      try
      {
         // Résolution des imports
         c.ComposeParts(this);
      }
      catch (CompositionException compositionException)
      {
         // ...
      }
   }
}

Une deuxième version, MEF2 (ou plus officiellement System.Composition) a vu le jour dans les dernières versions du framework .net. Ses performances et fonctionnalités ont considérablement été améliorées. Il offre également une syntaxe assez simple pour rajouter des conditions à l’enregistrement et à la résolutions des dépendances, ce qui lui permet de répondre à des scénarios encore plus complexes.

Pour aller plus loin

Afin de s’affranchir de la dépendance à un framework, le projet CommonServiceLocator (documentation disponible sur Codeplex) a été créé. Il permet en effet d’utiliser une interface de plus haut niveau, et d’agir comme un pattern Adapter pour rerouter les appels au Service Locator vers le framework d’IoC choisi.

L’interface du CommonServiceLocator est simple, et ne concerne que la récupération des objets, la configuration des résolutions de dépendance restant le travail du framework d’injection.

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace Microsoft.Practices.ServiceLocation
{
   public interface IServiceLocator : IServiceProvider
   {
      object GetInstance(Type serviceType);
      object GetInstance(Type serviceType, string key);
      IEnumerable<object> GetAllInstances(Type serviceType);
      TService GetInstance<TService>();
      TService GetInstance<TService>(string key);
      IEnumerable<TService> GetAllInstances<TService>();
   }
}

Si la communauté a déjà contribué des adaptateurs spécifiques pour un certain nombre de conteneurs, il est tout à fait possible d’écrire des locateurs spécifiques pour un framework donné et de l’utiliser par défaut.

Sources :

Leave a Reply

Your email address will not be published. Required fields are marked *