Les design patterns à connaître en TypeScript

Les design patterns à connaître en TypeScript

Vous pouvez lire notre introduction aux design patterns avant de continuer cet article lus approfondi.

Decorator Pattern

Le Decorator Pattern est un modèle de conception structurel qui permet d'ajouter dynamiquement de nouvelles fonctionnalités à des objets sans altérer leur structure. En TypeScript, ce pattern est particulièrement puissant grâce au support natif des décorateurs, une fonctionnalité expérimentale qui peut être activée dans la configuration TypeScript.

Description du Decorator Pattern

Un décorateur en TypeScript est une expression qui permet d'annoter et de modifier des classes et des propriétés au moment de la définition. Cela est très utile pour ajouter des comportements ou modifier des attributs existants de manière transparente.

Exemple de code

Supposons que nous avons une application avec des services nécessitant des niveaux différents de journalisation et de sécurité. Voici comment nous pourrions appliquer le Decorator Pattern pour enrichir nos classes avec ces fonctionnalités supplémentaires :

// Décorateur pour ajouter une fonction de journalisation
function Loggable(target: Function) {
    target.prototype.log = function() {
        console.log(`Logging access for ${this.constructor.name}`);
    }
}

// Décorateur pour ajouter une sécurité de base
function Secure(role: string) {
    return function(target: Function) {
        target.prototype.authorize = function() {
            console.log(`Authorizing access for role: ${role}`);
        }
    }
}

@Loggable
@Secure('admin')
class UserManager {
    constructor(private users: string[]) {}
    listUsers() {
        this.log();
        this.authorize();
        console.log('Listing users:', this.users);
    }
}

const userManager = new UserManager(['Alice', 'Bob']);
userManager.listUsers();

Dans cet exemple, les décorateurs @Loggable et @Secure sont appliqués à la classe UserManager, ajoutant des méthodes log et authorize qui sont appelées dans listUsers.

Cas pratique

Dans un système de gestion des utilisateurs, il est courant de devoir étendre les fonctionnalités sans modifier le code existant, surtout lorsqu'on traite avec des bases de code larges et des systèmes en production. En utilisant le Decorator Pattern, nous pouvons ajouter des fonctionnalités de journalisation et de sécurité aux classes existantes sans perturber les autres parties du système. Cela aide à maintenir le code propre, modulaire et facile à tester.

Avec cette approche, vous pouvez voir comment le Decorator Pattern, bien que simple, peut être extrêmement puissant dans un environnement de développement TypeScript. Cela aide non seulement à maintenir la séparation des préoccupations mais aussi à augmenter la modularité et la réutilisabilité du code.

Factory Pattern

Le Pattern Factory est un modèle de conception qui appartient à la catégorie des patterns de création. Il est utilisé pour déléguer la logistique de création d'instances à une entité spécifique, permettant ainsi au code client de s'abstraire des détails de construction des objets. En TypeScript, ce pattern est particulièrement utile pour gérer la création d'objets dans une application typée de manière sûre et flexible.

Description du Factory Pattern

Le Factory Pattern simplifie l'ajout de nouveaux types d'objets à un système en encapsulant la création d'objets dans une classe ou une fonction dédiée. Cela centralise la création d'objets et rend le système plus modulable et facile à maintenir.

Exemple de code

Imaginons que vous développez une application de commerce électronique qui doit gérer différents types de produits, chacun ayant des attributs spécifiques. Voici comment le Factory Pattern pourrait être implémenté en TypeScript pour gérer cette diversité :

interface Product {
    display(): void;
}

class Book implements Product {
    constructor(private title: string, private author: string) {}

    display(): void {
        console.log(`Book: ${this.title} by ${this.author}`);
    }
}

class Electronic implements Product {
    constructor(private name: string, private price: number) {}

    display(): void {
        console.log(`Electronic: ${this.name}, Price: ${price}`);
    }
}

class ProductFactory {
    static createProduct(type: string, ...args: any[]): Product {
        switch (type) {
            case 'book':
                return new Book(args[0], args[1]);
            case 'electronic':
                return new Electronic(args[0], args[1]);
            default:
                throw new Error('Product type not supported');
        }
    }
}

const book = ProductFactory.createProduct('book', '1984', 'George Orwell');
const electronic = ProductFactory.createProduct('electronic', 'Echo Dot', 49.99);

book.display();
electronic.display();

Dans cet exemple, la ProductFactory est responsable de la création d'instances de produits. Le client peut demander des instances sans connaître les détails de la classe concrète ou les paramètres nécessaires pour créer ces objets.

Cas pratique

Dans une application de commerce électronique, le Factory Pattern permet d'ajouter facilement de nouveaux types de produits sans modifier le code existant. Par exemple, si demain vous décidez d'introduire une nouvelle catégorie de produits comme des vêtements, vous pouvez simplement ajouter une nouvelle classe et modifier légèrement la ProductFactory sans perturber les autres parties de l'application.

Ce pattern est extrêmement utile pour gérer la diversité des produits dans un système complexe, tout en gardant le code propre et organisé. Il aide également à préparer l'application pour d'éventuelles extensions en minimisant les modifications nécessaires lors de l'ajout de nouvelles fonctionnalités.

Observer Pattern

Le Observer Pattern est un modèle de conception comportemental qui permet à un objet, appelé "sujet", de maintenir une liste d'objets dépendants, appelés "observateurs", et de les notifier automatiquement de tout changement d'état. Ce pattern est très pertinent dans le développement d'applications où les changements dans une partie du système nécessitent des mises à jour automatiques dans d'autres parties.

Description du Observer Pattern

En TypeScript, l'Observer Pattern peut être implémenté en utilisant des interfaces pour définir les sujets et les observateurs. Cela garantit que l'implémentation respecte le contrat défini par l'interface, rendant le code plus robuste et maintenable.

Exemple de code

Considérons une application de messagerie où les utilisateurs doivent être notifiés lorsque de nouveaux messages sont disponibles. Voici comment vous pourriez implémenter l'Observer Pattern pour gérer cette fonctionnalité :

interface Subject {
    attach(observer: Observer): void;
    detach(observer: Observer): void;
    notify(): void;
}

interface Observer {
    update(subject: Subject): void;
}

class MessageStream implements Subject {
    private observers: Observer[] = [];
    private messages: string[] = [];

    attach(observer: Observer): void {
        this.observers.push(observer);
    }

    detach(observer: Observer): void {
        const index = this.observers.indexOf(observer);
        if (index > -1) {
            this.observers.splice(index, 1);
        }
    }

    notify(): void {
        for (const observer of this.observers) {
            observer.update(this);
        }
    }

    addMessage(message: string): void {
        this.messages.push(message);
        this.notify();
    }

    getMessages(): string[] {
        return this.messages;
    }
}

class User implements Observer {
    update(subject: Subject): void {
        if (subject instanceof MessageStream) {
            console.log('New message:', subject.getMessages().slice(-1)[0]);
        }
    }
}

const messageStream = new MessageStream();
const user1 = new User();
const user2 = new User();

messageStream.attach(user1);
messageStream.attach(user2);

messageStream.addMessage('Hello, this is a new message!');

Dans cet exemple, MessageStream est le sujet, et les User sont les observateurs. Chaque fois qu'un message est ajouté, tous les observateurs sont notifiés.

Cas pratique

Utiliser l'Observer Pattern dans une application de messagerie en temps réel permet de s'assurer que tous les utilisateurs reçoivent les mises à jour dès qu'elles sont disponibles. Cela évite d'avoir à sonder ou à rafraîchir manuellement l'interface utilisateur pour les nouveaux messages, améliorant l'efficacité et l'expérience utilisateur.

Ce pattern est crucial pour les applications où le temps réel est une composante clé, comme dans les systèmes de trading, les dashboards de surveillance, ou les applications de collaboration en ligne, car il permet de maintenir tous les participants à jour avec les dernières informations.

Singleton Pattern

Le Singleton Pattern est un modèle de conception qui assure qu'une classe n'a qu'une seule instance et fournit un point d'accès global à cette instance. Ce pattern est particulièrement utile en TypeScript pour gérer les configurations globales ou les services partagés au sein d'une application.

Description du Singleton Pattern

Dans le contexte de TypeScript, le Singleton Pattern permet de s'assurer que certaines classes critiques comme les gestionnaires de configuration ou les services de connexion à une base de données ne sont instanciés qu'une seule fois. Cela aide à éviter les conflits potentiels dans les données et réduit la consommation de ressources en évitant les instanciations multiples.

Exemple de code

Voici comment on pourrait implémenter le Singleton Pattern en TypeScript pour une classe de configuration globale :

class AppConfig {
    private static instance: AppConfig;
    private settings: { [key: string]: string } = {};

    private constructor() {
        // Les configurations sont initialisées ici
        this.settings['theme'] = 'dark';
        this.settings['language'] = 'EN';
    }

    public static getInstance(): AppConfig {
        if (!AppConfig.instance) {
            AppConfig.instance = new AppConfig();
        }
        return AppConfig.instance;
    }

    public getSetting(key: string): string {
        return this.settings[key];
    }
}

const config = AppConfig.getInstance();
console.log(config.getSetting('theme'));  // Output: dark
const anotherConfig = AppConfig.getInstance();
console.log(anotherConfig === config);  // Output: true

Dans cet exemple, AppConfig est notre Singleton. Le constructeur est privé pour empêcher l'instanciation externe, et une méthode statique getInstance gère l'accès à l'instance unique.

Cas pratique

Utiliser le Singleton Pattern pour la configuration globale d'une application SaaS complexe est extrêmement bénéfique. Il garantit que toutes les parties de l'application accèdent à une configuration cohérente et synchronisée, évitant ainsi les incohérences qui pourraient survenir avec des instances multiples.

De plus, dans les cas où plusieurs services doivent accéder à une ressource partagée, comme une connexion à une base de données ou un gestionnaire de cache, le Singleton assure que chaque service utilise la même instance, maximisant l'efficacité et la cohérence des opérations.

Conclusion

Les design patterns en TypeScript sont des outils puissants pour créer des applications robustes, maintenables et évolutives. Le Decorator, le Factory, l'Observer et le Singleton ne sont que quelques exemples de patterns qui peuvent améliorer significativement la qualité de votre code. En les intégrant judicieusement, vous pouvez résoudre des problèmes complexes de développement tout en gardant votre code organisé et performant !

Prêt à vous lancer ?