📧 Reste informé(e) !

Reçois les derniers articles et conseils EasyAngularKit directement dans ta boîte mail.

S'inscrire gratuitement

~2 min de lecture

SOLID en Angular 20+ : mauvaise vs bonne façon

Objectif : te donner des repères concrets. Pour chaque principe, on commence par ce qu’il ne faut pas faire puis on montre la bonne approche avec Angular v20+.


📦 S — Single Responsibility Principle (Responsabilité unique)

Idée clé : un fichier = un rôle clair (UI ou logique), pas les deux.

❌ Mauvaise façon

Le composant fait tout : fetch + état + affichage.

// product-list.component.ts (anti-pattern)
import { Component, signal } from '@angular/core';

type Product = { id: number; name: string; price: number };

@Component({
  selector: 'app-product-list',
  template: `
    <button (click)="load()">Recharger</button>
    @if(loading()) { <p>Chargement…</p> }
    @else {
      <ul>
        @for(p of products(); track p.id) {
          <li>{{ p.name }} — {{ p.price | currency:'EUR' }}</li>
        }
      </ul>
    }
  `
})
export class ProductListComponent {
  readonly products = signal<Product[]>([]);

  readonly loading = signal(false);

  async load(): void {
    this.loading.set(true);
    // Appel direct ici = couplage fort UI/données
    const data = await fetch('/api/products').then(r => r.json());
    this.products.set(data);
    this.loading.set(false);
  }
}

Problèmes : difficile à tester, UI couplée au transport (fetch), impossible de réutiliser la logique ailleurs.

✅ Bonne façon

Service pour la logique + composant pour l’UI.

// product.service.ts
import { Injectable, signal } from '@angular/core';

export interface Product {
  id: number;
  name: string;
  price: number
}

@Injectable({ providedIn: 'root' })
export class ProductService {
  private readonly _products = signal<Product[]>([]);

  readonly products = this._products.asReadonly();

  async load(): void {
    const data: Product[] = await fetch('/api/products').then(r => r.json());
    this._products.set(data);
  }
}
// product-list.component.ts
import { Component, inject, signal } from '@angular/core';
import { ProductService } from './product.service';

@Component({
  selector: 'app-product-list',
  template: `
    <button (click)="refresh()">Recharger</button>
    @if(loading()) { <p>Chargement…</p> }
    @else {
      <ul>
        @for(p of svc.products(); track p.id) {
          <li>{{ p.name }} — {{ p.price | currency:'EUR' }}</li>
        }
      </ul>
    }
  `
})
export class ProductListComponent {
  private readonly _svc = inject(ProductService);

  readonly loading = signal(false);

  async refresh() {
    this.loading.set(true);
    await this._svc.load();
    this.loading.set(false);
  }
}

🔒 O — Open/Closed Principle (Ouvert/Fermé)

Idée clé : ajouter une variante sans modifier le code existant.

❌ Mauvaise façon

switch répété dans le composant, à modifier à chaque nouveau moyen de paiement.

// checkout.component.ts (anti-pattern)
import { Component, input } from '@angular/core';

@Component({
  selector: 'app-checkout',
  template: `<button (click)="pay()">Payer</button>`
})
export class CheckoutComponent {
  method = input<'stripe' | 'paypal'>('stripe');

  async pay() {
    const amount = 49;
    if (this.method() === 'stripe') {
      /* appel Stripe */
    } else if (this.method() === 'paypal') {
      /* appel PayPal */
    }
    // Ajouter ApplePay ? => on modifie ce fichier encore…
  }
}

✅ Bonne façon

Stratégies sélectionnées par DI (InjectionToken). On ajoute une classe, on ne touche pas au composant.

// payment.port.ts
import { InjectionToken } from '@angular/core';

export interface PaymentStrategy {
  pay(amount: number): Promise<void>;
}

export const PAYMENT = new InjectionToken<PaymentStrategy>('PAYMENT');
// stripe.strategy.ts
export class StripePayment implements PaymentStrategy {
  async pay(amount: number) { /* appel Stripe */
  }
}
// paypal.strategy.ts
export class PaypalPayment implements PaymentStrategy {
  async pay(amount: number) { /* appel PayPal */
  }
}
// checkout.component.ts
import { Component, inject } from '@angular/core';
import { PAYMENT } from './payment.port';

@Component({
  selector: 'app-checkout',
  // change la stratégie ici sans toucher au composant, pourrait être fait au niveau du routing
  providers: [{ provide: PAYMENT, useClass: StripePayment }],
  template: `<button (click)="onPay()">Payer</button>`
})
export class CheckoutComponent {
  private payment = inject(PAYMENT);

  async onPay() {
    await this.payment.pay(49);
  }
}

🔄 L — Liskov Substitution Principle (Substitution de Liskov)

Idée clé : toute implémentation peut remplacer une autre sans effet de bord.

❌ Mauvaise façon

Contrat trop large / incohérent selon l’implémentation.

// notifier.ts (anti-pattern)
export interface Notifier {
  notify(message: string): Promise<void>;

  // Un impl nécessite 'subject' ? L'autre l'ignore => surprise
  setSubject?(subject: string): void;
}

✅ Bonne façon

Contrat minimal et cohérent. Chaque adaptateur respecte le même comportement.

// notifier.port.ts
import { InjectionToken } from '@angular/core';

export interface Notifier {
  notify(message: string): Promise<void>;
}

export const NOTIFIER = new InjectionToken<Notifier>('NOTIFIER');
// email.notifier.ts
export class EmailNotifier implements Notifier {
  async notify(message: string) { /* envoi email */
  }
}
// sms.notifier.ts
export class SmsNotifier implements Notifier {
  async notify(message: string) { /* envoi SMS */
  }
}
// profile.component.ts
import { Component, inject } from '@angular/core';
import { NOTIFIER } from './notifier.port';
import { EmailNotifier } from './email.notifier';

@Component({
  selector: 'app-profile',
  providers: [{ provide: NOTIFIER, useClass: EmailNotifier }],
  template: `<button (click)="save()">Sauvegarder</button>`
})
export class ProfileComponent {
  private notifier = inject(NOTIFIER);

  async save() {
    await this.notifier.notify('Profil sauvegardé ✅');
  }
}

🪶 I — Interface Segregation Principle (Segregation des interfaces)

Idée clé : éviter les interfaces « god object ».

❌ Mauvaise façon

Une interface force les consommateurs à implémenter tout.

// repository.ts (anti-pattern)
export interface Repository<T> {
  load(): Promise<T[]>;

  save(item: T): Promise<void>;

  delete(id: string): Promise<void>;

  // Beaucoup de composants n'ont besoin que de load()…
}

✅ Bonne façon

Des ports petits et ciblés.

export interface Loader<T> {
  load(): Promise<T[]>;
}

export interface Saver<T> {
  save(item: T): Promise<void>;
}
// product-editor.component.ts
import { Component, input, output, inject } from '@angular/core';

@Component({
  selector: 'app-product-editor',
  template: `
    <h3>Éditer {{ product()?.name }}</h3>
    <button (click)="onSave()">Enregistrer</button>
  `
})
export class ProductEditorComponent {
  product = input.required<{ id: number; name: string }>();

  saved = output<{ id: number; name: string }>();

  #saver = inject<ProductSaver>(ProductSaver); // DI classique

  async onSave() {
    await this.#saver.save(this.product()!);
    this.saved.emit(this.product()!);
  }
}

🔌 D — Dependency Inversion Principle (Inversion des dépendances)

Idée clé : dépendre d’abstractions (ports), pas de classes concrètes.

❌ Mauvaise façon

Le composant appelle directement une lib concrète (couplage fort).

// somewhere.component.ts (anti-pattern)
import { Component } from '@angular/core';

@Component({
  selector: 'app-somewhere',
  template: `<button (click)="buy()">Acheter</button>`
})
export class SomewhereComponent {
  buy(): void {
    // Couplage à GA4, impossible à mocker facilement
    // window.gtag('event', 'purchase', { amount: 49 });
  }
}

✅ Bonne façon

Port AnalyticsPort + adaptateurs interchangeables.

// analytics.port.ts
import { InjectionToken } from '@angular/core';

export interface AnalyticsPort {
  track(event: string, payload: Record<string, unknown>): void;
}

export const ANALYTICS = new InjectionToken<AnalyticsPort>('ANALYTICS');
// ga4.adapter.ts
export class GA4Adapter implements AnalyticsPort {
  track(e: string, payload: Record<string, unknown>) { /* gtag */
  }
}
// console.adapter.ts
export class ConsoleAdapter implements AnalyticsPort {
  track(e: string, payload: Record<string, unknown>) {
    console.log(e, payload);
  }
}
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { ANALYTICS } from './analytics.port';
import { GA4Adapter } from './ga4.adapter';

export const appConfig: ApplicationConfig = {
  providers: [{ provide: ANALYTICS, useClass: GA4Adapter }]
};
// buy.component.ts
import { Component, inject } from '@angular/core';
import { ANALYTICS } from './analytics.port';

@Component({
  selector: 'app-buy',
  template: `<button (click)="buy()">Acheter</button>`
})
export class BuyComponent {
  analytics = inject(ANALYTICS);

  buy() {
    this.analytics.track('purchase', { amount: 49 });
  }
}

✅ Audit express — « est‑ce SOLID ? »

  • S : mon composant a‑t‑il un rôle unique ?
  • O : puis‑je ajouter un cas sans modifier le composant ?
  • L : mes implémentations sont‑elles vraiment interchangeables ?
  • I : les contrats sont‑ils petits et ciblés ?
  • D : je dépends d’un InjectionToken plutôt que d’une classe concrète ?

👉🏼 Pour en apprendre davantage et mettre en pratique, viens découvrir EasyAngularKit : https://pim.ms/C31g7p2

📧 Reste informé(e) !

Reçois les derniers articles et conseils EasyAngularKit directement dans ta boîte mail.

S'inscrire gratuitement

EasyAngularKit

Formation complète pour maîtriser Angular et développer des applications web modernes.

Navigation

Contact

Légal

© 2026 Easy Angular Kit. Tous droits réservés.