paint-brush
Code propre : responsabilité unique, ouvert/fermé, substitution Liskov Principes SOLID dans TS [Partie 4]par@alenaananich
4,731 lectures
4,731 lectures

Code propre : responsabilité unique, ouvert/fermé, substitution Liskov Principes SOLID dans TS [Partie 4]

par Alena Ananich7m2023/11/09
Read on Terminal Reader

Trop long; Pour lire

Dans cet article, nous examinerons les trois premiers principes SOLID : le principe de responsabilité unique, le principe ouvert/fermé et le principe de substitution de Liskov sur des exemples pratiques.
featured image - Code propre : responsabilité unique, ouvert/fermé, substitution Liskov Principes SOLID dans TS [Partie 4]
Alena Ananich HackerNoon profile picture


Parties précédentes :


Nous continuons à réfléchir à des approches pour rendre notre code propre, flexible et maintenable. Et maintenant, commençons à étudier des solutions architecturales propres.


Les principes SOLID ont été introduits par Robert C. Martin et sont largement considérés comme les meilleures pratiques en matière de programmation orientée objet et de conception de logiciels.


Dans cet article, nous examinerons les trois premiers principes SOLID : le principe de responsabilité unique, le principe ouvert/fermé et le principe de substitution de Liskov sur des exemples pratiques.

Table des matières:

  1. Principe de responsabilité unique
  2. Principe ouvert/fermé
  3. Principe de substitution de Liskov

1. Principe de responsabilité unique (SRP)

Une classe ne devrait avoir qu’une seule raison de changer


En d’autres termes, une classe ne devrait avoir qu’une seule responsabilité ou un seul travail. C’est important car une classe avec une seule responsabilité est plus facile à comprendre, à modifier et à maintenir. Les modifications apportées à une zone du code ne se répercuteront pas sur l'ensemble du système, ce qui réduit le risque d'introduction de bogues.


Jetons un coup d'œil à un exemple pratique et aux façons de suivre le principe de responsabilité unique :

 // Bad class UserSettingsService { constructor(user: IUser) { this.user = user; } changeSettings(settings: IUserSettings): void { if (this.isUserValidated()) { // ... } } getUserInfo(): Promise<IUserSettings> { // ... } async isUserValidated(): Promise<boolean> { const userInfo = await this.getUserInfo(); // ... } }

Dans cet exemple, notre classe effectue des actions dans différentes directions : elle définit le contexte, le modifie et le valide.


Pour suivre le SRP, nous devons diviser ces différentes responsabilités.

 // Better class UserAuth { constructor(user: IUser) { this.user = user; } getUserInfo(): Promise<IUserSettings> { // ... } async isUserValidated(): boolean { const userInfo = await this.getUserInfo(); // ... } } class UserSettings { constructor(user: IUser) { this.user = user; this.auth = new UserAuth(user); } changeSettings(settings: IUserSettings): void { if (this.auth.isUserValidated()) { // ... } } }


Pourquoi c'est important?

SRP vise à améliorer la modularité du code, en minimisant les défis liés aux interdépendances. En organisant le code en fonctions, en classes séparées et en favorisant la modularité, il devient plus réutilisable, ce qui permet de gagner du temps qui pourrait autrement être consacré à recoder les fonctionnalités existantes.

2. Principe ouvert/fermé (OCP)

Les entités logicielles (classes, modules, fonctions) doivent être ouvertes à l'extension mais fermées à la modification


Vous devriez pouvoir ajouter de nouvelles fonctionnalités sans modifier le code existant. C'est important car en adhérant à ce principe, vous pouvez introduire de nouvelles fonctionnalités ou composants dans votre système sans risquer la stabilité du code existant.


Il favorise la réutilisabilité du code et réduit le besoin de modifications importantes lors de l'extension des fonctionnalités.

 // Bad class Product { id: number; name: string[]; price: number; protected constructor(id: number, name: string[], price: number) { this.id = id; this.name = name; this.price = price; } } class Ananas extends Product { constructor(id: number, name: string[], price: number) { super(id, name, price); } } class Banana extends Product { constructor(id: number, name: string[], price: string) { super(id, name, price); } } class HttpRequestCost { constructor(product: Product) { this.product = product; } getDeliveryCost(): number { if (product instanceOf Ananas) { return requestAnanas(url).then(...); } if (product instanceOf Banana) { return requestBanana(url).then(...); } } } function requestAnanas(url: string): Promise<ICost> { // logic for ananas } function requestBanana(url: string): Promise<ICost> { // logic for bananas }

Dans cet exemple, le problème réside dans la classe HttpRequestCost , qui, dans la méthode getDeliveryCost , contient des conditions pour le calcul de différents types de produits, et nous utilisons des méthodes distinctes pour chaque type de produit. Donc, si nous devons ajouter un nouveau type de produit, nous devons modifier la classe HttpRequestCost , et ce n'est pas sûr ; nous pourrions obtenir des résultats inattendus.


Pour l'éviter, nous devons créer une request de méthode abstraite dans la classe Product sans réalisations. La réalisation particulière aura hérité des classes : Ananas et Banane. Ils réaliseront eux-mêmes la demande.


HttpRequestCost prendra le paramètre product suivant l'interface de la classe Product , et lorsque nous transmettrons des dépendances particulières dans HttpRequestCost , il réalisera déjà la méthode request pour lui-même.

 // Better abstract class Product { id: number; name: string[]; price: string; constructor(id: number, name: string[], price: string) { this.id = id; this.name = name; this.price = price; } abstract request(url: string): void; } class Ananas extends Product { constructor(id: number, name: string[], price: string) { super(id, name, price); } request(url: string): void { // logic for ananas } } class Banana extends Product { constructor(id: number, name: string[], price: string) { super(id, name, price); } request(url: string): void { // logic for bananas } } class HttpRequestCost { constructor(product: Product) { this.product = product; } request(): Promise<void> { return this.product.request(url).then(...); } }


Pourquoi c'est important?

En suivant ce principe, vous réduirez le couplage de code et sauverez le système d'un comportement imprévisible.

3. Principe de substitution de Liskov (LSP)

Les objets d'une superclasse doivent pouvoir être remplacés par des objets de ses sous-classes sans interrompre l'application.


Pour comprendre ce principe, regardons cet exemple :

 // Bad class Worker { work(): void {/../} access(): void { console.log('Have an access to closed perimeter'); } } class Programmer extends Worker { createDatabase(): void {/../} } class Seller extends Worker { sale(): void {/../} } class Designer extends Worker { access(): void { throwError('No access'); } }

Dans cet exemple, nous avons un problème avec la classe Contractor . Designer , Programmer et Seller sont tous des Workers et ont hérité de la classe parent Worker . Mais en même temps, les concepteurs n’ont pas accès à un périmètre fermé car ils sont des entrepreneurs et non des employés. Et nous avons remplacé la méthode access et brisé le principe de substitution de Liskov.


Ce principe nous dit que si nous remplaçons la superclasse Worker par sa sous-classe, par exemple la classe Designer , la fonctionnalité ne doit pas être interrompue. Mais si nous le faisons, la fonctionnalité de la classe Programmer sera interrompue - la méthode access aura des réalisations inattendues de la classe Designer .


Suivant le principe de substitution de Liskov, nous ne devons pas réécrire les réalisations en sous-classes mais devons créer de nouvelles couches d'abstraction dans lesquelles nous définissons des réalisations particulières pour chaque type d'abstraction.


Corrigeons-le :

 // Better class Worker { work(): void {/../} } class Employee extends Worker { access(): void { console.log('Have an access to closed perimeter'); } } class Contractor extends Worker { addNewContract(): void {/../} } class Programmer extends Employee { createDatabase(): void {/../} } class Saler extends Employee { sale(): void {/../} } class Designer extends Contractor { makeDesign(): void {/../} }

Nous avons créé de nouvelles couches d'abstractions Employee et Contractor , déplacé la méthode access vers la classe Employee et défini une réalisation spécifique. Si nous remplaçons la classe Worker par la sous-classe Contractor , la fonctionnalité Worker ne sera pas interrompue.

Pourquoi c'est important?

Si vous adhérez à LSP, vous pouvez remplacer n'importe quelle instance de sous-classe partout où une instance de classe de base est attendue, et le programme devrait toujours fonctionner comme prévu. Cela favorise la réutilisation du code, la modularité et rend votre code plus résistant aux changements.


Dans le prochain article, nous examinerons les principes SOLID de ségrégation d'interface et d'inversion de dépendances.