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)
Principe de responsabilité unique (SRP: The Single Responsibility Principle)
Présentation de SRP
Ce principe indique qu’une classe ne doit avoir qu’une seule raison d’être modifiée.
Si une classe comporte plusieurs responsabilités alors ces responsabilités sont couplées. Le fait de changer une responsabilité va influencer les autres responsabilités de la classe.
Les classes comportant des responsabilités couplées entraîneront des difficultés de maintenance ou des dysfonctionnement inattendus.
La modification d’une responsabilité de la classe aura un impact sur les autres responsabilités.
Qu’est-ce qu’une responsabilité ?
Selon Robert C. Martin, une responsabilité de la classe est une « raison de changer ». S’il y a plusieurs raisons de modifier une classe, c’est qu’elle possède plusieurs responsabilités.
Comment mettre en pratique SRP ?
Voici les étapes à suivre pour mettre en pratique le principe de responsabilité unique (SRP) :
- Lister les responsabilités d’une classe
- Choisir une responsabilité et extraire les méthodes pour en créer une nouvelle classe
- Continuer tant que la liste des responsabilité contient plus d’un élément
Exemple concret
Pour mieux comprendre ce principe, je vous propose un exemple réel.
Imaginons que nous souhaitions mettre en place un système de vente en ligne qui charge des produits depuis une base de données.
Pour la simplicité de l’exemple, la base de données est simulée avec une liste d’objet en mémoire.
Le projet d’exemple contient deux classes importantes :
- La classe Product qui contient les informations d’un produit
- La classe Database qui s’occupe de charger les données
Voici le code de la classe Product :
[csharp]
namespace SrpDemo
{
public class Product
{
public string Name { get; private set ; }
public double Price { get; private set ; }
public Product(string name, double price)
{
Name = name;
Price = price;
}
}
}
[/csharp]
Voici le code de la classe Database :
[csharp] using System.Collections.Generic;using System.Linq;
namespace SrpDemo
{
public class Database
{
List<Product > productsInMemory;
public void Connect()
{
// simulate database connection
}
public void LoadData()
{
productsInMemory = new List <Product>();
productsInMemory.Add( new Product ("Product1", 100));
productsInMemory.Add( new Product ("Product2", 255));
}
public IEnumerable <Product> GetProducts( bool applyVAT)
{
Connect();
LoadData();
// simulate database query (using linq)
var products = (from p in productsInMemory select p).AsEnumerable();
double vat = applyVAT ? 1.196 : 1;
// apply VAT
return from p in products
select new Product(p.Name, p.Price * vat);
}
}
}
[/csharp]
Dans cet exemple, j’ai crée une application du type console (ce qui est le projet le plus simple possible). Voici le code de la classe Program :
[csharp] using System;namespace SrpDemo
{
class Program
{
static void Main(string[] args)
{
Database db = new Database();
var products = db.GetProducts(true );
foreach (var p in products)
Console.WriteLine("{0} = {1} Euros TTC" , p.Name, p.Price);
// To make a pause
Console.Read();
}
}
}
[/csharp]
En analysant la classe Database, on remarque qu’elle possède les responsabilités suivantes :
- Gestion des connexions à la base de données
- Chargement des données (requêtes Linq)
- Calcule de la TVA
Pour appliquer le principe SRP, il suffit donc de séparer les reponsabilités de la classe.
Refactoring 1 :
Responsabilité : Gestion des connexions
Dans mon exemple, j’utilise une liste en mémoire. Pour séparer cette responsabilité, je peux utiliser une classe DbContext provenant d’Entity Framework. Je vous propose donc de créer une classe qui hérite de DbContext et qui contient les éléments d’accès à la base de données.
[csharp] using System.Data.Entity;namespace CodeFirst.Models
{
public class DatabaseContext : DbContext
{
public DatabaseContext()
: base("name=DatabaseContext" )
{
}
public DbSet <Product> Products { get; set ; }
}
}
[/csharp]
Je modifie Database pour l’adapter. Je renomme également la classe en ProductRepository qui me semble plus adapté.
Refactoring 2 :
Responsabilité : Chargement des données
Je modifie la classe ProductRepository pour qu’elle s’occupe uniquement de charger les données.
Voici le code après modification :
[csharp] using System.Collections.Generic;using System.Linq;
namespace SrpDemo
{
public class ProductRepository
{
public IEnumerable <Product> GetProducts(bool applyVAT)
{
using (var context = new DatabaseContext())
{
var products = (from p in context.Products select p).AsEnumerable();
return products;
}
}
}
}
[/csharp]
Refactoring 3 :
Responsabilité : Calcule du prix TTC à partir du prix HT.
Je choisi de placer les règles de calcul de tarif dans l’objet métier. C’est la classe Product qui contiendra les règles métier d’un produit.
Une fois la classe modifiée, le code ressemblera à ceci :
[csharp] namespace SrpDemo{
public class Product
{
public const double VATRate = 0.196;
public string Name { get; private set ; }
public double Price { get; private set ; }
public Product(string name, double price)
{
Name = name;
Price = price;
}
public double GetPriceWithVAT()
{
return Price * (1 + VATRate);
}
}
}
[/csharp]
Il reste à adapter la classe Program pour prendre en compte la nouvelle méthode GetPriceWithVAT, voici le code :
[csharp] using System;namespace SrpDemo
{
class Program
{
static void Main(string[] args)
{
ProductRepository db = new ProductRepository();
var products = db.GetProducts();
foreach (var p in products)
Console.WriteLine("{0} = {1} Euros TTC" , p.Name, p.GetPriceWithVAT());
// To make a pause
Console.Read();
}
}
}
[/csharp]
Conclusion
Au travers de cet exemple très simple, je vous ai montré comment mettre en œuvre le principe SRP.
J’espère simplement que cela vous aura sensibilisé à ce principe et je vous invite à l’essayer sur certaines de vos classes. Grâce à SRP, vos classes seront plus simples à comprendre et plus simples à faire évoluer !
Le code du projet est disponible sur l’archive suivante : SrpDemo
Au premier lancement, l’application va créer une base de données (SQLExpress). Il faudra donc ajouter des produits dans la table pour que l’exemple affiche des données.
Pour continuer, je vous invite à lire l’article sur le principe d’ouvert-fermé (3/6).