|
| 1 | +--- |
| 2 | +title: "Les principes SOLID : Comprendre le principe de responsabilité unique" |
| 3 | +date: 2024-03-25 |
| 4 | +image_credit: designedbyflores |
| 5 | +url: solid-single-responsibility-principle |
| 6 | +description: "Le principe de responsabilité unique (SRP) est une règle d'ingénierie logicielle qui peut aider les développeurs à écrire du code plus maintenable et testable. En suivant ce principe, les développeurs peuvent décomposer des problèmes complexes en unités de code plus petites et plus gérables, ce qui rend le code plus facile à comprendre et à maintenir." |
| 7 | +keywords: "Principe de Responsabilité Unique, Single Responsibility Principle,SRP,SOLID Principles,SOLID,Code modulaire,Software Development logiciel,Object-Oriented Programming, Programmation Orienté Objet,POO,OOP" |
| 8 | +tags: [OOP, SOLID] |
| 9 | +--- |
| 10 | + |
| 11 | +Le principe de responsabilité unique (Single Responsibility Principle) est le premier des cinq principes SOLID. Il peut être le principe SOLID le plus simple à comprendre, mais il n'est pas toujours facile à appliquer, surtout si vous êtes un développeur junior. Que dit ce principe ? |
| 12 | + |
| 13 | +>There should never be more than one reason for a class to change. In other words, every class should have only one responsibility |
| 14 | +> |
| 15 | +>[wikipedia](https://en.wikipedia.org/wiki/SOLID) |
| 16 | +
|
| 17 | +SRP peut être difficile à appliquer car il nécessite des développeurs de décomposer des problèmes complexes en unités de code plus petites et plus gérables. Identifier et isoler les responsabilités peut être difficile, et si cela est fait de manière incorrecte, cela peut conduire à de mauvaises décisions de conception. |
| 18 | + |
| 19 | +Prenons un exemple. La classe suivante est chargée d'importer des produits dans une application en tant que PIM ou ERP. |
| 20 | + |
| 21 | +```ts |
| 22 | +type Product = { |
| 23 | + name: string |
| 24 | + description: string |
| 25 | +}; |
| 26 | + |
| 27 | +class ProductImport { |
| 28 | + constructor(private connection: Connection) {} |
| 29 | + |
| 30 | + |
| 31 | + async import(filePath: string): Promise<void> { |
| 32 | + await this.loadProductFromCsvFile(filePath); |
| 33 | + } |
| 34 | + |
| 35 | + private async loadProductFromCsvFile(file: string): Promise<void> { |
| 36 | + const csvData: Product[] = []; |
| 37 | + createReadStream(file) |
| 38 | + .pipe(csvParser()) |
| 39 | + .on('data', (product: Product) => csvData.push(product)) |
| 40 | + .on('end', async () => { |
| 41 | + for (const data of csvData) { |
| 42 | + await this.saveProducts(data); |
| 43 | + } |
| 44 | + }); |
| 45 | + } |
| 46 | + |
| 47 | + private async saveProducts(product: Product): Promise<void> { |
| 48 | + await this.connection.execute( |
| 49 | + 'INSERT INTO products (name, description) VALUES (?, ?)', |
| 50 | + [product.name, product.description], |
| 51 | + ); |
| 52 | + } |
| 53 | +} |
| 54 | +``` |
| 55 | + |
| 56 | +Cette classe `ProductImport` fait plusieurs choses : elle récupère les données des produits à partir d'un fichier CSV et les importe dans une base de données. Cela signifie qu'elle a plusieurs responsabilités, ce qui viole le principe de responsabilité unique (SRP). |
| 57 | + |
| 58 | +{{< image src="product-import-responsibilities.svg" alt="Product import responsibilities" >}} |
| 59 | + |
| 60 | +Nous devons diviser cette grosse classe en plusieurs petites classes pour isoler les responsabilités et la rendre conforme au principe de responsabilité unique. Nous allons créer une nouvelle classe appelée `CsvProductLoader` qui chargera les données des produits à partir du fichier CSV, et nous créerons une seconde classe appelée `MysqlProducts` qui sera responsable de sauvegarder les données des produits dans la base de données. |
| 61 | + |
| 62 | +```ts |
| 63 | +class CsvProductLoader { |
| 64 | + async loadProduct(file: string): Promise<Product[]> { |
| 65 | + const products: Product[] = []; |
| 66 | + createReadStream(file) |
| 67 | + .pipe(csvParser()) |
| 68 | + .on('data', (product: Product) => products.push(product)); |
| 69 | + |
| 70 | + return products; |
| 71 | + } |
| 72 | +} |
| 73 | + |
| 74 | +class MysqlProducts { |
| 75 | + constructor(private connection: Connection) {} |
| 76 | + |
| 77 | + async save(product: Product): Promise<void> { |
| 78 | + await this.connection.execute( |
| 79 | + 'INSERT INTO products (name, description) VALUES (?, ?)', |
| 80 | + [product.name, product.description], |
| 81 | + ); |
| 82 | + } |
| 83 | +} |
| 84 | +``` |
| 85 | + |
| 86 | +Nous avons toujours besoin de la classe `ProductImport`. Elle agit comme un contrôleur et est responsable de l'orchestration des interactions entre les classes `CsvProductLoader` et `MysqlProducts`. La classe `ProductImport` n'a pas besoin de gérer des traitements de données de bas niveau ou des opérations de base de données. Sa responsabilité principale est de déléguer les tâches de lecture et de sauvegarde des données aux classes spécialisées. Cette séparation des responsabilités favorise la modularité et rend le code plus maintenable. |
| 87 | + |
| 88 | +```ts |
| 89 | +class ProductImport { |
| 90 | + constructor( |
| 91 | + private productLoader: CsvProductLoader, |
| 92 | + private products: MysqlProducts, |
| 93 | + ) {} |
| 94 | + |
| 95 | + async import(filePath: string): Promise<void> { |
| 96 | + const products = await this.productLoader.loadProduct(filePath); |
| 97 | + products.forEach((product: Product) => this.products.save(product)); |
| 98 | + } |
| 99 | +} |
| 100 | +``` |
| 101 | + |
| 102 | +Il reste une dernière chose à améliorer dans cet exemple de code. La classe `ProductImport` dépend actuellement de classes concrètes, ce qui ne respecte pas le principe d'Inversion de Dépendance, car les modules de haut niveau ne devraient pas dépendre directement des modules de bas niveau. Pour remédier à cela, nous devons introduire des interfaces pour abstraire les dépendances dans la classe `ProductImport`. |
| 103 | + |
| 104 | +```ts |
| 105 | +interface ProductLoader { |
| 106 | + loadProduct(file: string): Promise<Product[]> |
| 107 | +} |
| 108 | + |
| 109 | +interface ProductLoader { |
| 110 | + save(product: Product): Promise<void> |
| 111 | +} |
| 112 | + |
| 113 | +class ProductImport { |
| 114 | + constructor( |
| 115 | + private productLoader: ProductLoader, |
| 116 | + private products: ProductLoader, |
| 117 | + ) {} |
| 118 | +} |
| 119 | +``` |
| 120 | +J'ai écrit un article sur le Principe d'Inversion de Dépendance (DIP) qui explique le principe et comment il facilite les tests : |
| 121 | + |
| 122 | +{{< page-link page="solid-dependency-inversion-principle" >}} |
| 123 | + |
| 124 | +{{< training-link >}} |
| 125 | + |
| 126 | +Le plus grand avantage de travailler avec de petites classes est qu'il facilite les tests. La classe `ProductImport` originale nécessitait une base de données fonctionnelle et la capacité de lire des fichiers du système de fichiers. Cela ne facilite pas l'obtention d'une boucle de feedback courte. Tester du code impliquant des opérations d'entrée/sortie (Input/Output) est plus compliqué parce que le code ne peut pas être exécuté sans les outils requis par l'application. Diviser de grosses classes en plus petites aide à isoler les opérations d'entrée/sortie (Input/Output) et rend votre code plus testable. |
| 127 | + |
| 128 | +J'ai écrit un article sur la manière dont le Principe de Responsabilité Unique (SRP) facilite les tests, surtout lorsque vos classes sont énormes et que vous souhaitez tester leurs méthodes privées : |
| 129 | + |
| 130 | +{{< page-link page="do-not-test-private-methods" >}} |
| 131 | + |
| 132 | +Créer du bon code est comme jouer avec des briques Lego. Cela implique de travailler sur des classes petites et facilement testables et de les assembler en utilisant la composition pour construire des fonctionnalités plus complexes. |
0 commit comments