Parties précédentes : Clean Code : fonctions et méthodes dans TypeScript [Partie 1] Code propre : dénomination et composition de code dans TypeScript [Partie 2] Code propre : classes et objets dans TypeScript [Partie 3] 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: Principe de responsabilité unique Principe ouvert/fermé 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 , qui, dans la méthode , 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 , et ce n'est pas sûr ; nous pourrions obtenir des résultats inattendus. HttpRequestCost getDeliveryCost HttpRequestCost Pour l'éviter, nous devons créer une de méthode abstraite dans la classe sans réalisations. La réalisation particulière aura hérité des classes : Ananas et Banane. Ils réaliseront eux-mêmes la demande. request Product prendra le paramètre suivant l'interface de la classe , et lorsque nous transmettrons des dépendances particulières dans , il réalisera déjà la méthode pour lui-même. HttpRequestCost product Product HttpRequestCost request // 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 . , et sont tous des Workers et ont hérité de la classe parent . 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 et brisé le principe de substitution de Liskov. Contractor Designer Programmer Seller Worker access Ce principe nous dit que si nous remplaçons la superclasse par sa sous-classe, par exemple la classe , la fonctionnalité ne doit pas être interrompue. Mais si nous le faisons, la fonctionnalité de la classe sera interrompue - la méthode aura des réalisations inattendues de la classe . Worker Designer Programmer access 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 et , déplacé la méthode vers la classe et défini une réalisation spécifique. Si nous remplaçons la classe par la sous-classe , la fonctionnalité ne sera pas interrompue. Employee Contractor access Employee Worker Contractor Worker 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.