5 principes que vous appliquez déjà sans le savoir
Bootcode IWA-S04 â Semaine 15, Jour 4
1. ConnaĂźtre les 5 principes SOLID
S, O, L, I, D et leur signification
2. ReconnaĂźtre chaque principe
Dans le code qu'on a déjà écrit
3. SOLID n'est pas nouveau
C'est nommer des bonnes pratiques
4. Identifier les violations
Dans du code existant
SURPRISE : vous appliquez déjà SOLID
On va juste donner des noms Ă ce que vous faites
S = Single Responsibility
Une classe = un rĂŽle (depuis S8)
O = Open/Closed
Ajouter PostgreSQL sans modifier le service (S14)
L = Liskov Substitution
InMemory et Postgres sont interchangeables
I = Interface Segregation
Des interfaces ciblées (depuis S10)
D = Dependency Inversion
Dépendre de l'interface, pas de l'implémentation (S11)
Module 1
Vous appliquez déjà SOLID depuis des semaines
C'est le nom de ce que vous faites déjà dans le codebase Bootcode
// S8 â ResponsabilitĂ© unique
Une classe UserRepository ne fait que gérer les users
// S10 â Interfaces fines
Des interfaces ciblées plutÎt qu'un gros god-object
// S11 â DĂ©pendance aux abstractions
Le service dépend de l'interface, pas de la classe
// S14 â Open/Closed
On a ajouté Postgres sans toucher au service
đĄ Aujourd'hui, on donne juste des NOMS Ă ces pratiques
Single Responsibility
Une classe = une seule raison de changer
Open/Closed
Ouvert à l'extension, fermé à la modification
Liskov Substitution
Une sous-classe doit remplacer sa classe parente
Interface Segregation
Plusieurs interfaces fines plutĂŽt qu'une grosse
Dependency Inversion
Dépendre des abstractions, pas des implémentations
Module 2
Single Responsibility
Une classe = un seul rĂŽle
Une classe ne doit avoir qu'une seule raison de changer
Une classe = un métier
Si une classe gĂšre les users ET envoie les emails, elle a 2 responsabilitĂ©s â 2 raisons de changer
Pourquoi ?
đ On l'a vu en S8 : sĂ©parer les responsabilitĂ©s
â God class â 3 responsabilitĂ©s
class UserService {
createUser(data) { ... }
sendWelcomeEmail() { ... }
generatePdfReport() { ... }
logToCloudWatch() { ... }
}
// 4 raisons de changer
â Une classe = un rĂŽle
class UserService {
createUser(data) { ... }
}
class EmailService { ... }
class ReportService { ... }
class Logger { ... }
// 1 raison de changer chacun
Dans le codebase Bootcode, chaque classe a UN rĂŽle :
// UserRepository â gĂšre le stockage des users
class UserRepository {
save(user) { ... }
findById(id) { ... }
delete(id) { ... }
}
// UserService â orchestre la logique mĂ©tier
class UserService {
register(data) { ... }
}
â UserRepository ne sait pas ce qu'est un email. UserService ne sait pas comment on stocke. Chacun son mĂ©tier.
Module 3
Open/Closed
Ouvert à l'extension, fermé à la modification
Une classe doit ĂȘtre ouverte Ă l'extension mais fermĂ©e Ă la modification
Open = on peut étendre
Ajouter un nouveau comportement sans toucher au code existant
Closed = on ne modifie pas
Le code qui marche n'est pas touchĂ© â pas de regression
đĄ Le secret ? Les interfaces et le polymorphisme
â On modifie le service existant
class UserService {
save(user) {
if (driver === 'memory') {
// ...
} else if (driver === 'pg') {
// nouveau code ajouté
}
}
}
// On a touché au code qui marchait
â On crĂ©e une nouvelle classe
// Le service ne change PAS
class PostgresUserRepo
implements UserRepo {
save(user) {
// SQL INSERT...
}
}
// On Ă©tend sans modifier â
Quand on a ajouté PostgreSQL, on n'a PAS touché au UserService :
// S14 : on a juste créé une nouvelle implémentation
class PostgresUserRepository
implements UserRepository { ... }
// Le service existant n'a pas bougé d'une ligne
class UserService {
constructor(private repo: UserRepository) {}
}
â Open : on a ajoutĂ© Postgres. Closed : UserService n'a pas Ă©tĂ© modifiĂ©.
Module 4
Liskov Substitution
Une sous-classe doit pouvoir remplacer sa classe parente
Si B est une sous-classe de A, alors on doit pouvoir utiliser B partout oĂč A est attendu sans casser le programme
Le contrat respecté
La sous-classe respecte les mĂȘmes promesses : mĂȘmes paramĂštres, mĂȘmes retours, pas de surprise
Nom barbara
Barbara Liskov (informaticienne, 1987) â mais l'idĂ©e est simple : pas de mauvaise surprise
đĄ En pratique : InMemory et Postgres sont interchangeables grĂące Ă l'interface
â La sous-classe casse le contrat
class Bird {
fly() { /* vole */ }
}
class Penguin extends Bird {
fly() {
throw "Je ne vole pas!";
}
}
// Le programme qui attend un Bird casse
â Le contrat est respectĂ©
interface UserRepository {
save(user): void;
}
class InMemoryRepo
implements UserRepository { ... }
class PostgresRepo
implements UserRepository { ... }
// On peut swap sans casser â
Dans Bootcode, on peut swap InMemory â Postgres sans rien casser :
// Les tests utilisent InMemoryRepo
const service = new UserService(new InMemoryRepo());
// La prod utilise PostgresRepo
const service = new UserService(new PostgresRepo());
// Le service ne sait mĂȘme pas lequel il a !
â C'est ça Liskov : n'importe quelle implĂ©mentation de UserRepository peut remplacer une autre. Le programme continue de marcher.
Module 5
Interface Segregation
Des interfaces fines plutĂŽt qu'une grosse
Une classe ne doit pas ĂȘtre forcĂ©e d'implĂ©menter des mĂ©thodes qu'elle n'utilise pas
â Grosse interface
Une interface avec 20 mĂ©thodes â toutes les classes doivent tout implĂ©menter, mĂȘme ce qui ne les concerne pas
â Interfaces fines
DĂ©couper en petites interfaces ciblĂ©es â chaque classe implĂ©mente seulement ce qu'elle fait
đ On l'a vu en S10 : des interfaces ciblĂ©es
â Interface trop grosse
interface Repository {
save(); find(); delete();
sendEmail(); log();
}
// Un repo de logs doit implémenter
// sendEmail ?? Ăa n'a pas de sens
â Interfaces ciblĂ©es
interface Readable { find(); }
interface Writable { save(); }
interface Deletable { delete(); }
// Chaque classe choisit ce qu'elle fait
class LogRepo implements Writable {}
Dans Bootcode, on a des interfaces fines et ciblées :
// Pas une mega-interface qui fait tout
interface UserRepository {
save(user): void;
findById(id): User;
}
// Une autre interface pour un autre métier
interface SessionRepository { ... }
â UserRepository ne force pas Ă implĂ©menter des mĂ©thodes de session. Chaque interface = un besoin prĂ©cis.
Module 6
Dependency Inversion
Dépendre de l'abstraction, pas de l'implémentation
Les modules haut-niveau ne doivent pas dépendre des modules bas-niveau. Les deux doivent dépendre des abstractions.
â Couplage fort
UserService dĂ©pend directement de PostgresRepo â impossible Ă tester sans une vraie BDD
â Couplage faible
UserService dĂ©pend de l'interface UserRepository â on peut injecter InMemoryRepo pour les tests
đĄ C'est LA raison pour laquelle vos tests fonctionnent !
â DĂ©pend de l'implĂ©mentation
class UserService {
private repo = new PostgresRepo();
register(data) {
this.repo.save(user);
}
}
// Impossible Ă tester sans Postgres
â DĂ©pend de l'abstraction
class UserService {
constructor(
private repo: UserRepository
) {}
}
// On injecte ce qu'on veut â
C'est LA raison pour laquelle vos tests fonctionnent :
// Dans les tests : on injecte InMemoryRepo
const repo = new InMemoryUserRepo();
const service = new UserService(repo);
// En prod : on injecte PostgresRepo
const service = new UserService(new PostgresRepo());
â Le service dĂ©pend de l'interface, pas de la classe. On peut tester sans BDD, dĂ©ployer avec Postgres. C'est le D en action.
đ± "SOLID c'est compliquĂ©"
Non â vous le faites dĂ©jĂ depuis la S8. C'est juste donner des noms Ă vos pratiques.
đ Confondre les lettres
S = une classe un rÎle · O = étendre sans modifier · L = interchangeable · I = interfaces fines · D = dépendre de l'abstraction
⥠Aller trop vite sur O et L
O et L sont plus subtils : O = on ajoute sans modifier, L = on peut swap sans casser. Relisez les exemples du codebase.
đ§Ș Oublier que D = tests qui marchent
Si vos tests fonctionnent avec InMemoryRepo, c'est grùce au D. C'est le bénéfice concret.
Single Responsibility
Une classe = un rĂŽle. Vu en S8 (UserRepository vs UserService).
Open/Closed
Ătendre sans modifier. Vu en S14 (ajout de Postgres sans toucher au service).
Liskov Substitution
InMemory et Postgres sont interchangeables. Le service ne sait pas lequel il a.
Interface Segregation
Des interfaces fines et ciblées. Vu en S10.
Dependency Inversion
Dépendre de l'interface. Vu en S11. C'est pourquoi vos tests marchent.
đŻ SOLID = nommer ce que vous faites dĂ©jĂ . Chaque principe est visible dans le codebase Bootcode.
Quelle lettre vous parle le plus ?
Bootcode IWA-S04 â Semaine 15, Jour 4