mercredi 10 février 2010 14:34
tja
[Design Patterns] Partie 2: DIP: Dependency Inversion Principle
C’est le dernier principe des principes du Design Orienté Objet (The Principles of Object Oriented Design) fondés par Robert C. Martin plus connu sous le pseudonyme d’Uncle Bob.
l’image empruntée de LosTechies.
Je ne traite pas les principes dans l’ordre mais peu importe, cela n’a pas une grande importance. Pour rappel, voici les sujets précédents:
- [Design Patterns] Partie 1: SRP: Single Responsibility Principle
- [Design Patterns] SRP encore une fois
Dependency Inversion Principle (DIP): Principe d’Inversion de Dépendance.
“A: Les modules de plus haut niveau ne doivent pas dépendre des modules de plus bas niveau. Les deux doivent dépendre des abstractions”
“B: Les abstractions ne doivent pas dépendre des détails. Les détails doivent dépendre des abstractions”
Le terme d’inversion de dépendance (DIP) est souvent assimilé aux termes d’Injection de dépendance et Inversion of Control (IoC). Pour ma part j’estime que l’injection de dépendance est un sujet très important de connaître et franchement, pour construire des applications robustes et évolutives il est très difficile de passer à côté. Je vais vous épargner le grand débat théorique dont j’ai l’habitude :) et cette fois ci passons directement à l’exemple. Le but de DI (et autre principes S.O.L.I.D) est de palier les problèmes liés au mauvais design des applications. Bien que vous utilisez l’approche OOP, votre code est structuré à l’intérieur des classes mais il est toujours très difficile à le modifier et à le faire évoluer. C’est à cause de ces dépendances que les classes ont entre elles et si vous modifiez une classe il y a un risque qu’une autre “pète” à l’autre bout de la planète :)
Dans l’exemple suivant nous allons construire un pseudo loggueur. Pour une question de simplicité excusez-moi pour certains raccourcis (comme l’instanciation un peu hasardeuse ;))
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 27 28 29 30 31 |
class Program { static void Main(string[] args) { var logger = new Logger();
// mon programme démarre et il // faut le loguer logger.Log("Début du démarrage du programme");
Console.ReadKey(); } }
public class Logger { public void Log(string message) { var txtLog = new TxtLogWriter(); txtLog.LogToFile(message); } }
public class TxtLogWriter { public void LogToFile(string message) { Console.WriteLine(message); Console.WriteLine("--- Ecrit dans un fichier ---"); } }
|
Comme vous pouvez le constater le programme est simplifié au maximum. L’idée est que la classe Logger est utilisée dans le programme principal pour écrire certains événements qui se passent lors de l’exécution de ce programme. Un message de type chaîne de caractères est passé à la classe Logger qui fait appel à son tour à la classe TxtLogWriter qui l’écrit dans un fichier de texte (dans l’exemple nous nous contentons de l’afficher dans la console).
Evolution en vue (comme d’hab) !
Mais, (il y a toujours un mais) on nous a demandé d’avoir la possibilité d’écrire les messages dans le journal d’évènements de Windows. Ah, c’est facile, est bien souvent on se lance dans la programmation pour créer une deuxième classe comme WinLogWriter :
1 2 3 4 5 6 7 8 |
public class WinLogWriter { public void LogToEventLog(string message) { Console.WriteLine(message); Console.WriteLine("--- Ecrit dans un journal Windows ---"); } } |
Le problème est que maintenant pour que cela fonctionne nous devons implémenter au sein de la classe Logger une condition qui nous permettra suivant un paramètre d'utiliser soit la loggueur txt soit le loggueur windows. L’ensemble du programme se présente maintenant comme ceci :
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
class Program
{ static void Main(string[] args) { var logger = new Logger("win");
// mon programme démarre et il // faut le loguer logger.Log("Début du démarrage du programme");
Console.ReadKey(); } }
public class Logger { private string _typeWriter;
public Logger(string typeWriter) { _typeWriter = typeWriter; }
public void Log(string message) { switch (_typeWriter) { case "txt": var txtLog = new TxtLogWriter(); txtLog.LogToFile(message); break; case "win": var winLog = new WinLogWriter(); winLog.LogToEventLog(message); break; } } }
public class TxtLogWriter { public void LogToFile(string message) { Console.WriteLine(message); Console.WriteLine("--- Ecrit dans un fichier ---"); } }
public class WinLogWriter { public void LogToEventLog(string message) { Console.WriteLine(message); Console.WriteLine("--- Ecrit dans un journal Windows ---"); } }
|
Constat, la partie switch est super MOCHE ! Pire encore, notre classe Logger est maintenant entièrement dépendante de 2 classes TxtLogWriter et WinLogWriter alors qu’avant elle ne dépanadait que d’une seule classe. Qu’est que cela implique ? Que potentiellement une modification aura l’impacte sur plus de code, la maintenance sera plus difficile et les tests seront également plus difficile à réaliser.
Introduire une abstraction
En fait quand on y regarde de plus prêt, la classe Logger ne doit pas savoir qu’elle classe concrète logue le message. Tout ce qu’elle doit savoir c’est qu’UNE classe logue le message avec la méthode “WriteLog”. Nous pouvons accomplir cela avec une interface.
1 2 3 4 |
public interface ILogWriter { void WriteLog(string message); } |
Ensuite nos deux classes TxtLogWriter et WinLogWriter doivent implémenter cette interface.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class TxtLogWriter : ILogWriter { public void WriteLog(string message) { Console.WriteLine(message); Console.WriteLine("--- Ecrit dans un fichier ---"); } }
public class WinLogWriter : ILogWriter { public void WriteLog(string message) { Console.WriteLine(message); Console.WriteLine("--- Ecrit dans un journal Windows ---"); } }
|
Maintenant dans la classe Logger nous instancions la classe que nous voulons en tant que ILogWirter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public void Log(string message) { ILogWriter logWriter; switch (_typeWriter) { case "txt": logWriter = new TxtLogWriter(); break; case "win": logWriter = new WinLogWriter(); break; }
logWriter.WriteLog(message); } |
Cela cependant n’a pas amélioré notre design. Notre classe Logger est toujours dépendante des implémentations concrètes de 2 classes. Heureusement nous avons une autre arme à notre disposition pour palier à ça.
Injection de dépendance (Dependency Injection)
Ce que nous ne voulons pas c’est la dépendance en dure dans la classe Logger et les classes TxtLogWriter et WinLogWriter. (Le fait d’utiliser le mot clé new crée une dépendance entre les classes). Nous pouvons dire que la classe Logger contrôle la vie des autres classes. Ce que nous voulons c’est d’injecter la dépendance à l’exécution. Il y a plusieurs manière d’injecter les dépendances mais les plus connues sont celles-ci:
- injection par constructeur
- injection par la méthode
- injection par la propriété
- Factory pattern
- à l’aide d’un outil IoC (Inversion of Control) comme Unity, StructureMap, Ninject, Autofact, Windsor….
Nous réaliserons ici l’injection par le constructeur. La classe Logger ne doit pas savoir qu’elle LogWriter elle va utiliser quand la classe sera utilisée.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class Logger { private ILogWriter _logWriter;
public Logger(ILogWriter logWriter) { _logWriter = logWriter; }
public void Log(string message) { _logWriter.WriteLog(message); } }
|
Vous remarquerez que nous avons “viré” tout le bloc switch très moche. De plus la classe Logger ne sait pas qu’elle instance concrète elle utiliser pour logguer les messages. L’instance actuelle n’est plus créée dans Logger mais est injectée par le constructeur.
Nous pouvons également utiliser l’injection par la méthode ou la propriété mais là dessus je ferai un autre post pour expliquer les différences. Notre appel dans la méthode Main est maintenant comme ceci:
1 |
var logger = new Logger(new WinLogWriter()); |
Ici nous créons l’instance concrète mais bien souvent cette instance est passé par un containeur IoC ou une Factory abstraite.
Conslusion
DIP permet de créer les designs qui sont “propres” car les dépendances sont injectées, donc cette décision ne repose plus sur le client. Pour ma part j’utilise un containeur IoC mais vous pouvez aussi bien d’utiliser une factory abstraite pour les fournir. Le fait que notre desing soit plus propre implique que notre code est plus facile à modifier et à faire évoluer.
J’ai encore plein d’idées sur les sujets similaires mais je manque un peu de temps. Cependant si cela vous intéresse je peux bien me motiver ;)
A bientôt.
Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :