Cette article est la suite d’une série de plusieurs articles sur les principes SOLID (je vous invite à relire l’article Conception d’applications SOLID pour en savoir plus). L’article précédent était consacré à ISP (Interface Seggregation Principle).
Principe de l’Inversion des dépendances (DIP: Dependency Inversion Principle)
Présentation
Le principe d’inversion des dépendances est celui que je préfère. En effet, il permet de mettre en place des applications plus souples.
DIP s’exprime de la manière suivante :
Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau. Les deux doivent dépendre d’abstractions.
Les abstractions ne doivent pas dépendre des détails. Les détails doivent dépendre des abstractions.
Les modules de haut niveau sont ceux qui contiennent votre code métier et le fonctionnel de l’application. Les modules de bas niveau sont ceux qui contiennent les implémentations dépendantes de la machine, du stockage, de la communication ou des serveurs externes (exemple: base de données, serveur de logs).
Ce principe indique qu’il ne faut donc pas dépendre (directement) de l’implémentation bas niveau que vous allez utiliser.
Au quotidien, je vois régulièrement des développeurs qui mettent en place du code qui va utiliser SQL Server. SQL Server est un élément externe à l’application et peut donc être vu comme un module bas niveau. Pour respecter DIP, il faut donc utiliser une classe abstraite qui va s’occuper des requêtes SQL. En général, par configuration, nous allons faire le lien entre la classe abstraite et l’implémentation réelle.
DIP apporte les avantages suivants :
- Une diminution du couplage dans votre application (Le nom de la classe n’est plus codé en dur)
- L’implémentation concrète peut être choisie à la compilation ou à l’exécution
Comment mettre en pratique DIP ?
La mise en pratique est assez simple :
- Il faut tout d’abord analyser et lister les modules de bas niveau
- Pour chaque élément, il faut mettre en place une abstraction (soit par une interface soit par une classe du type factory)
- Ensuite, il faut écrire les implémentations qui respectent ces interfaces
- Enfin, votre application doit définir quelle implémentation utiliser pour chaque interface.
L’idée est que chaque point de contact entre deux modules soit matérialisé par une abstraction.
L’avantage évident de DIP est qu’il permet de changer d’implémentation en modifiant simplement une ligne de code dans votre application.
Dans les langages modernes, comme C# avec .NET, vous pouvez même configurer l’application grâce à un fichier (le plus souvent app.config) qui contient l’association entre chaque interface et chaque implémentation. Ceci permet donc de modifier le comportement simplement en modifiant un fichier et en redémarrant votre application.
Un exemple concret
Pour illustrer DIP, je vous propose un exemple concernant le traitement XML (je ne vais pas entrer dans les détails de XML, ne vous inquiétez pas).
Imagions donc que nous avons une classe qui s’occupe d’analyser un contenu au format XML. Dans notre première version, nous utilisons un fichier comme source de données.
Voici le squelette de la classe :
[csharp] public class XmlProcessor{
public XmlProcessor(string filename)
{
// TODO: charger le XML depuis le fichier
}
public void Process()
{
// TODO: Traiter le XML
}
}
[/csharp]
Au fur et à mesure, vous remplissez les zones « TODO ». Après quelques heures de travail, votre chef de projet arrive et vous demande de gérer le chargement des données depuis une base de données.
Là, vous vous dites, Ok, je vais ajouter un paramètre pour indiquer si je charge d’un fichier ou d’une base de données. Voilà, ce que ça pourrait donner :
[csharp] public enum XmlSource{
Undefined = 0,
File = 1,
Database = 2
}
public class XmlProcessor
{
/// <summary>
/// Constructeur de la classe
/// </summary>
/// <param name="source">Indique la source de données</param>
/// <param name="filename">Nom du fichier (si la source est File)</param>
/// <param name="connString">Chaine de connexion (si la source est Database)</param>
public XmlProcessor(XmlSource source, string filename, string connString)
{
if (source == XmlSource.File)
{
// TODO: charger le XML depuis le fichier
}
else
if (source == XmlSource.Database)
{
// TODO: charger le XML depuis la base de données
}
}
public void Process()
{
// Traiter le XML
}
}
[/csharp]
La journée se termine. Le lendemain, vous relisez votre code et vous vous dites : mince, si mon chef arrive et me demande une troisième source de données, comment vais-je faire ? Je ne vais pas ajouter encore des paramètres ?
Comme vous venez de voir le principe DIP, vous remarquez que le traitement XML (code de haut niveau) dépend du stockage des données (code de bas niveau). Pour mettre en œuvre le principe, il suffit donc de mettre en place une abstraction entre les deux niveaux.
En C#, nous pouvons utiliser les interfaces. Je vous propose donc de créer une interface dont le rôle est de charger les données à partir d’une source de données, donc voici le code :
[csharp] public interface IDataLoader{
public string LoadData();
}
[/csharp]
Il reste donc à modifier notre classe XmlProcessor pour prendre en compte cette interface :
[csharp] public class XmlProcessor{
private string xml;
/// <summary>
/// Constructeur de la classe
/// </summary>
/// <param name="source">Indique la source de données</param>
/// <param name="filename">Nom du fichier (si la source est File)</param>
/// <param name="connString">Chaine de connexion (si la source est Database)</param>
public XmlProcessor(IDataLoader dataLoader)
{
xml = dataLoader.LoadData();
}
public void Process()
{
// Traiter le XML
}
}
[/csharp]
Voici un exemple d’implémentation pour le chargement depuis un fichier :
[csharp]public class FileLoader: IDataLoader
{
private string path;
public FileLoader(string filename)
{
path = filename;
}
public string LoadData()
{
return File.ReadAllText(path);
}
}
[/csharp]
Pour appeler XmlProcessor, il suffit alors d’écrire ce code :
[csharp] static void Main(string[] args){
IDataLoader loader = new FileLoader("test.xml");
XmlProcessor processor = new XmlProcessor(loader);
processor.Process();
}
[/csharp]
A partir de ce code, vous remarquez que pour changer d’implémentation, il suffira de modifier une seule ligne (celle qui contient new FileLoader…).
Conclusion
J’arrive à la fin de mes explications concernant DIP. Pour aller plus loin, je vous invite aussi à consulter l’article sur Wikipedia (en) qui est très complet sur DIP.
J’espère que ce principe vous aura permis de comprendre comment réduire le couplage dans vos applications. Parmi les principes SOLID, DIP est celui que je préfère car il est très puissant mais DIP est aussi celui que j’utilise le plus souvent dans mes applications.
Si cela vous intéresse, je parlerais prochainement des frameworks d’injection qui permettent d’aller encore plus loin et d’automatiser certaines taches.
A vous désormais de vous exprimer : laissez-moi un commentaire pour m’indiquer ce que vous pensez de ce principe.
Bonus complémentaire
L’ensemble du projet est disponible en bonus à cet article. Pour le télécharger, il suffit de devenir membre.
[ninja-inline id=3699]
Bonjour,
J’avoue avoir été surpris de tomber sur vos articles car j’ai procédé à la même analyse. Je suis donc bien content de voir que je ne sort pas du droit chemin et que je ne délire pas. C’est simple et bien présenté pour un accès à certain novice.
Chapeau.
Super tuto,
rapide, clair et précis !
Je me rends compte que j’appliquais à peu près tous les principes sans vraiment le savoir.
merci !