Chapitre 1
Chapitre 01 — Principes SOLID
Chapitre 01 — Principes SOLID
Cours 01 — Principes SOLID
Introduction
SOLID est un acronyme proposé par Robert C. Martin (Uncle Bob) au début des années 2000. Ces 5 principes guident la conception orientée objet pour produire un code maintenable, flexible et facile à tester.
Diagramme en cours de génération...
1. Single Responsibility Principle (SRP)
"Une classe doit avoir une seule raison de changer."
Problème
Une classe qui gère la persistance, l'affichage et les calculs est difficile à maintenir, tester et faire évoluer.
Violation
class Employee {
constructor(
public name: string,
public salary: number
) {}
calculateTax(): number {
return this.salary * 0.2;
}
generateReport(): string {
return `Employee: ${this.name}, Tax: ${this.calculateTax()}`;
}
saveToDatabase(): void {
// SQL logic here
console.log(`Saving ${this.name} to DB`);
}
sendEmail(): void {
// Email logic
console.log(`Sending email to ${this.name}`);
}
}
Cette classe a 4 raisons de changer :
- Les règles de calcul des impôts
- Le format du rapport
- Le schéma de base de données
- Le service d'email
Solution
class Employee {
constructor(
public name: string,
public salary: number
) {}
}
class TaxCalculator {
calculate(employee: Employee): number {
return employee.salary * 0.2;
}
}
class ReportGenerator {
generate(employee: Employee): string {
return `Employee: ${employee.name}`;
}
}
class EmployeeRepository {
save(employee: Employee): void {
console.log(`Saving ${employee.name} to DB`);
}
}
class EmailService {
send(employee: Employee, message: string): void {
console.log(`Email to ${employee.name}: ${message}`);
}
}
Bénéfices
- Testabilité : chaque classe se teste indépendamment
- Réutilisabilité :
TaxCalculatorpeut être réutilisé ailleurs - Maintenabilité : un changement dans les taxes n'affecte que
TaxCalculator
2. Open/Closed Principle (OCP)
"Les entités logicielles doivent être ouvertes à l'extension mais fermées à la modification."
Problème
Quand on ajoute une nouvelle fonctionnalité, on modifie du code existant, ce qui risque d'introduire des régressions.
Violation
class DiscountCalculator {
calculate(price: number, customerType: string): number {
if (customerType === "regular") {
return price * 0.95;
} else if (customerType === "premium") {
return price * 0.90;
} else if (customerType === "vip") {
return price * 0.80;
}
return price;
}
}
Pour ajouter un nouveau type de client, on modifie la classe → violation de l'OCP.
Solution
interface DiscountStrategy {
apply(price: number): number;
}
class RegularDiscount implements DiscountStrategy {
apply(price: number): number {
return price * 0.95;
}
}
class PremiumDiscount implements DiscountStrategy {
apply(price: number): number {
return price * 0.90;
}
}
class VIPDiscount implements DiscountStrategy {
apply(price: number): number {
return price * 0.80;
}
}
class DiscountCalculator {
constructor(private strategy: DiscountStrategy) {}
calculate(price: number): number {
return this.strategy.apply(price);
}
}
Pour ajouter SeasonalDiscount, on crée une nouvelle classe sans modifier l'existant :
class SeasonalDiscount implements DiscountStrategy {
apply(price: number): number {
return price * 0.75;
}
}
3. Liskov Substitution Principle (LSP)
"Les objets d'une classe dérivée doivent pouvoir se substituer aux objets de la classe de base sans altérer le bon fonctionnement du programme."
Problème classique : le carré n'est pas un rectangle
Diagramme en cours de génération...
Violation
class Rectangle {
constructor(
protected width: number,
protected height: number
) {}
setWidth(width: number): void {
this.width = width;
}
setHeight(height: number): void {
this.height = height;
}
getArea(): number {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width: number): void {
this.width = width;
this.height = width; // Maintient le carré
}
setHeight(height: number): void {
this.width = height;
this.height = height;
}
}
// Client code qui suppose que Square est un Rectangle
function resize(rect: Rectangle): void {
rect.setWidth(5);
rect.setHeight(10);
// Attend area = 50, mais si Square => 100
console.log(rect.getArea());
}
resize(new Square(1, 1)); // 100 au lieu de 50
Solution
// Abstraire en Shape
interface Shape {
getArea(): number;
}
class Rectangle implements Shape {
constructor(
private width: number,
private height: number
) {}
getArea(): number {
return this.width * this.height;
}
}
class Square implements Shape {
constructor(private side: number) {}
getArea(): number {
return this.side * this.side;
}
}
// Le client travaille avec Shape, pas avec Rectangle
function printArea(shape: Shape): void {
console.log(`Area: ${shape.getArea()}`);
}
Règles de la substitution
- Préconditions ne peuvent pas être renforcées dans la sous-classe
- Postconditions ne peuvent pas être affaiblies
- Invariants de la classe de base doivent être préservés
- Contrainte d'historique (règle de Helling) : les nouvelles méthodes ne doivent pas modifier l'état d'une manière non prévue par la base
4. Interface Segregation Principle (ISP)
"Les clients ne doivent pas être forcés de dépendre d'interfaces qu'ils n'utilisent pas."
Problème
Une interface "grasse" oblige les implémentations à fournir des méthodes inutiles.
Violation
interface Worker {
work(): void;
eat(): void;
sleep(): void;
}
class HumanWorker implements Worker {
work(): void { console.log("Working"); }
eat(): void { console.log("Eating"); }
sleep(): void { console.log("Sleeping"); }
}
class RobotWorker implements Worker {
work(): void { console.log("Working"); }
eat(): void { throw new Error("Robots don't eat"); }
sleep(): void { throw new Error("Robots don't sleep"); }
}
Solution
Diagramme en cours de génération...
interface Workable {
work(): void;
}
interface Eatable {
eat(): void;
}
interface Sleepable {
sleep(): void;
}
class HumanWorker implements Workable, Eatable, Sleepable {
work(): void { console.log("Working"); }
eat(): void { console.log("Eating"); }
sleep(): void { console.log("Sleeping"); }
}
class RobotWorker implements Workable {
work(): void { console.log("Working"); }
}
5. Dependency Inversion Principle (DIP)
"Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau. Les deux doivent dépendre des abstractions."
Problème
class MySQLDatabase {
query(sql: string): any[] {
console.log(`MySQL query: ${sql}`);
return [];
}
}
class UserService {
private db: MySQLDatabase;
constructor() {
this.db = new MySQLDatabase(); // Couplage fort
}
getUsers(): any[] {
return this.db.query("SELECT * FROM users");
}
}
Solution
Diagramme en cours de génération...
interface Database {
query(sql: string): any[];
}
class MySQLDatabase implements Database {
query(sql: string): any[] {
console.log(`MySQL: ${sql}`);
return [];
}
}
class PostgresDatabase implements Database {
query(sql: string): any[] {
console.log(`Postgres: ${sql}`);
return [];
}
}
class UserService {
constructor(private db: Database) {} // Injection de dépendance
getUsers(): any[] {
return this.db.query("SELECT * FROM users");
}
}
// Usage
const service = new UserService(new MySQLDatabase());
// ou
const service2 = new UserService(new PostgresDatabase());
Injection de dépendance
3 formes courantes :
| Forme | Description |
|---|---|
| Constructor Injection | Dépendances passées au constructeur |
| Setter Injection | Dépendances passées via un setter |
| Interface Injection | Dépendances passées via une méthode d'interface |
Synthèse
Diagramme en cours de génération...
Bénéfices cumulatifs
- SRP → code plus petit, plus ciblé
- OCP → ajouts sans risque de régression
- LSP → polymorphisme fiable
- ISP → implémentations légères
- DIP → découplage, testabilité