Publié le

Le Principe de Responsabilité Unique

5 min lecture - 992 mots
Auteurs
  • avatar
    Nom
    Cédric RIBALTA
    Twitter

Introduction

Le Principe de Responsabilité Unique (Single Responsibility Principle) est le premier principe SOLID.
Il a été formulé par Robert C. Martin, plus connu sous le nom d'Oncle Bob, dans son livre Clean Code.

Ce principe est souvent considéré comme le plus simple des cinq principes SOLID, mais il est également l'un des plus difficiles à appliquer correctement.

Dans cet article, nous allons explorer ce principe en détail, comprendre pourquoi il est si important et comment l'appliquer dans la pratique.

Qu'est-ce que le Principe de Responsabilité Unique ?

Dans son livre, Oncle Bob définit la responsabilité comme une raison de changer. Selon lui, une classe ou un module ne devrait avoir qu'une seule raison de changer.

Cela va au-delà de la simple idée qu'une classe ne devrait faire qu'une seule chose. Il explique que les changements dans une classe doivent être motivés par un seul facteur ou un seul groupe de personnes.

Différence clé avec le Principe de Séparation des Préoccupations :

Le Principe de Responsabilité Unique est souvent confondu avec le Principe de Séparation des Préoccupations. Bien que ces deux principes soient liés, ils ne sont pas identiques.

Ce que Martin met en avant, c’est l’idée qu’une responsabilité est liée à un axe de changement.

Par exemple, si une classe gère à la fois l'interface utilisateur et la logique métier, elle pourrait être modifiée à cause d'un changement dans l'interface utilisateur, même si la logique métier reste la même.

Cela signifie que cette classe a plusieurs raisons de changer, ce qui viole le principe de responsabilité unique.

Pourquoi est-ce important ?

Si une classe gère à la fois les règles de gestion d’un produit et la façon dont ce produit est affiché dans une interface. Elle devra peut-être être modifiée si l’affichage change, même si les règles de gestion ne changent pas.

Cela crée un risque d’introduire des bugs involontaires dans la logique métier simplement en modifiant l’interface.

Donc, pour Oncle Bob, il ne s'agit pas seulement de séparer les actions (comme générer un rapport et l'envoyer par email), mais aussi de comprendre qui est susceptible de demander un changement et de séparer le code en fonction de ces axes de changement potentiels.

Cette approche rend le code plus modulaire et plus facile à adapter lorsque des changements spécifiques sont nécessaires.

Exemple concret :

Imaginons une classe Employee (Employé) qui aurait les trois méthodes suivantes :

  • calculatePay() : Cette méthode calcule le salaire d'un employé, en fonction des heures travaillées, des bonus, etc. C'est une responsabilité liée aux finances et à la paie de l'entreprise.
  • reportHours() : Cette méthode génère un rapport sur le nombre d'heures travaillées par l'employé. Il s'agit d'une responsabilité relative à la gestion des horaires.
  • save() : Cette méthode enregistre les informations de l'employé dans une base de données. C'est une responsabilité qui relève de la persistance des données.

Problème avec cet exemple :

Cette classe viole le principe SRP, car elle remplit plusieurs rôles distincts, chacun pouvant être modifié pour des raisons différentes :

  • Si l'entreprise change ses politiques de paie, la méthode calculatePay() devra être modifiée.
  • Si la façon dont les heures sont rapportées évolue, la méthode reportHours() devra être ajustée.
  • Si la structure ou le format de la base de données change, la méthode save() devra être modifiée.

Chaque méthode appartient à un domaine différent :

  • calculatePay() : appartient au domaine financier (calcul de la paie).
  • reportHours() : concerne la gestion des ressources humaines ou des rapports d'activité.
  • save() : relève du domaine technique, à savoir la persistance des données dans une base de données.

Cela entraîne plusieurs axes de changement potentiels dans une même classe, ce qui complique sa maintenance et rend le code fragile.

Un changement dans un domaine pourrait involontairement affecter d'autres domaines, car tout est imbriqué dans la même classe.

Solution possible :

Pour respecter le Single Responsibility Principle, il serait préférable de diviser cette classe en plusieurs classes plus petites, chacune responsable d'une tâche spécifique :

  • Une classe pour gérer le calcul de la paie (EmployeePaymentCalculator ou similaire).
  • Une autre classe pour générer les rapports d'heures (EmployeeReportGenerator).
  • Une classe dédiée à la persistance des données (EmployeeRepository ou EmployeeDataSaver).

Cela permettrait à chaque classe d'avoir une seule raison de changer, ce qui simplifie la maintenance et réduit le risque de bugs dans d'autres domaines.

La contrepartie de cette solution est que vous aurez plus de classes à gérer. Une solution intermédiaire pourrait être d'utiliser le pattern "Facade".

Le rôle du pattern Facade :

Le Facade Pattern est un patron de conception structurel qui permet de fournir une interface simplifiée pour interagir avec des sous-systèmes complexes, sans exposer les détails de leur fonctionnement.

En d'autres termes, la façade sert de point d'entrée unique qui masque la complexité de plusieurs classes et permet de simplifier l'accès à ces classes.

Application du Facade Pattern dans cet exemple :

Au lieu d'avoir une classe Employee qui mélange plusieurs responsabilités, tu pourrais :

  1. Séparer les responsabilités dans des classes spécialisées, comme :
  • EmployeePaymentCalculator : pour calculer la paie,
  • EmployeeReportGenerator : pour gérer les rapports d'heures,
  • EmployeeRepository : pour sauvegarder les données.
  1. Créer une classe Facade qui encapsule l'accès à ces différentes classes, tout en offrant une interface simple.

Voici un exemple de ce à quoi cela pourrait ressembler :

class EmployeeFacade {
  private paymentCalculator: EmployeePaymentCalculator
  private reportGenerator: EmployeeReportGenerator
  private repository: EmployeeRepository

  constructor() {
    this.paymentCalculator = new EmployeePaymentCalculator()
    this.reportGenerator = new EmployeeReportGenerator()
    this.repository = new EmployeeRepository()
  }

  calculatePay(employee: Employee): number {
    return this.paymentCalculator.calculatePay(employee)
  }

  generateReport(employee: Employee): string {
    return this.reportGenerator.generateReport(employee)
  }

  saveEmployee(employee: Employee): void {
    this.repository.save(employee)
  }
}

Ainsi, la classe EmployeeFacade agit comme un point d'entrée unique pour toutes les opérations liées à un employé, tout en respectant le principe de responsabilité unique.

Conclusion

Le Principe de Responsabilité Unique est un concept fondamental en programmation orientée objet. Il vise à rendre le code plus modulaire, plus facile à maintenir et moins sujet aux bugs.

En respectant ce principe, tu pourras concevoir des classes et des modules plus cohérents, qui ont une seule raison de changer. Cela te permettra de mieux gérer la complexité de ton code et de le rendre plus robuste face aux évolutions.