Chargement...

Modèles de conception en C# (Factory, Singleton, Repository, Observer)

Découvrez les modèles de conception en C#, tels que Factory, Singleton et Repository, pour créer des applications maintenables.

Les Design Patterns fournissent des solutions standardisées et éprouvées aux problèmes récurrents dans le développement logiciel. Les modèles couramment utilisés en C# incluent Factory, Singleton, Repository, Observer, Strategy, Adapter et Decorator. Cet article explore les concepts clés de ces modèles, leurs cas d’utilisation et des exemples en C#.


Patron Factory

Le patron Factory centralise la création d’objets. Au lieu d’utiliser new partout, la responsabilité de créer les objets est déléguée à une classe Factory.


public interface IProduct { void DoWork(); }

public class ProductA : IProduct
{
    public void DoWork() => Console.WriteLine("Produit A");
}

public class ProductB : IProduct
{
    public void DoWork() => Console.WriteLine("Produit B");
}

public static class ProductFactory
{
    public static IProduct Create(string type) => type switch
    {
        "A" => new ProductA(),
        "B" => new ProductB(),
        _ => throw new ArgumentException("Type invalide")
    };
}

Patron Singleton

Le patron Singleton garantit qu’une seule instance d’une classe est créée. Il est couramment utilisé pour le logging, le cache ou les objets de configuration.


public class Singleton
{
    private static Singleton? _instance;
    private static readonly object _lock = new();

    private Singleton() { }

    public static Singleton Instance
    {
        get
        {
            lock (_lock)
                return _instance ??= new Singleton();
        }
    }
}

Patron Repository

Le patron Repository abstrait la couche d’accès aux données. Ainsi, la couche de logique métier devient indépendante des détails de la base de données.


public class Product { public int Id; public string Name = ""; }

public interface IProductRepository
{
    void Add(Product p);
    Product? GetById(int id);
}

public class InMemoryProductRepository : IProductRepository
{
    private readonly List<Product> _list = new();
    public void Add(Product p) => _list.Add(p);
    public Product? GetById(int id) => _list.FirstOrDefault(x => x.Id == id);
}

Patron Observer

Le patron Observer établit une relation éditeur–abonné (publisher–subscriber). Lorsqu’un objet change, tous les abonnés sont notifiés. En C#, cela est implémenté avec le mécanisme des événements et des délégués.


public class NewsAgency
{
    public event EventHandler<string>? NewsPublished;
    public void Publish(string news)
    {
        Console.WriteLine($"Nouvelle: {news}");
        NewsPublished?.Invoke(this, news);
    }
}

Patron Strategy

Le patron Strategy encapsule les algorithmes de manière à les rendre interchangeables. La classe cliente n’a pas besoin de savoir quel algorithme est utilisé ; la stratégie est fournie de l’extérieur.


public interface IPaymentStrategy { void Pay(decimal amount); }

public class CreditCardPayment : IPaymentStrategy
{
    public void Pay(decimal amount) => Console.WriteLine($"{amount} EUR payé par carte de crédit.");
}

public class PayPalPayment : IPaymentStrategy
{
    public void Pay(decimal amount) => Console.WriteLine($"{amount} EUR payé via PayPal.");
}

public class Checkout
{
    private readonly IPaymentStrategy _strategy;
    public Checkout(IPaymentStrategy strategy) => _strategy = strategy;
    public void ProcessOrder(decimal total) => _strategy.Pay(total);
}

Patron Adapter

Le patron Adapter permet à deux classes incompatibles de travailler ensemble. Il est souvent utilisé pour intégrer d’anciennes bibliothèques ou API dans de nouveaux systèmes.


// Ancien système
public class LegacyPrinter { public void PrintText(string t) => Console.WriteLine(t); }

// Nouvelle interface
public interface IPrinter { void Print(string text); }

// Adapter
public class PrinterAdapter : IPrinter
{
    private readonly LegacyPrinter _legacy;
    public PrinterAdapter(LegacyPrinter legacy) => _legacy = legacy;
    public void Print(string text) => _legacy.PrintText(text);
}

Patron Decorator

Le patron Decorator permet d’ajouter de nouveaux comportements dynamiquement aux objets existants. Il s’agit d’une approche basée sur la composition plutôt que sur l’héritage.


public interface IMessage { string GetText(); }

public class SimpleMessage : IMessage
{
    private readonly string _text;
    public SimpleMessage(string text) => _text = text;
    public string GetText() => _text;
}

// Decorator
public class EncryptedMessage : IMessage
{
    private readonly IMessage _inner;
    public EncryptedMessage(IMessage inner) => _inner = inner;
    public string GetText() => Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(_inner.GetText()));
}

Avantages


En résumé (TL;DR)

  • Factory : Abstrait la création d’objets.
  • Singleton : Garantit une seule instance.
  • Repository : Abstrait l’accès aux données.
  • Observer : Notifie les abonnés des changements.
  • Strategy : Les algorithmes peuvent être facilement remplacés.
  • Adapter : Adapte des classes incompatibles.
  • Decorator : Ajoute dynamiquement des fonctionnalités.

Exemple : Création de produits, liste, flux de commande et journalisation

L’exemple ci-dessous illustre la création de produits (Factory), la liste (Repository), la création de commande et la mise à jour du statut (Observer), le paiement (Strategy), l’impression du reçu (Adapter) et le traitement des messages (Decorator) ensemble. Il utilise une configuration Singleton sécurisée avec Lazy, une factory basée sur enum, des EventArgs personnalisés, et un formatage monétaire sensible à la culture.


using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;

// =======================
//  DOMAINE (Petit modèle)
// =======================

// Exprimer les types de produits avec un enum plutôt que des chaînes pour réduire les erreurs.
public enum ProductType { Book, Phone }

// Type record immuable pour transporter des données
public record Product(int Id, string Name, decimal Price);

// =======================
//  REPOSITORY (Stockage)
// =======================

public interface IProductRepository
{
    void Add(Product p);
    Product? GetById(int id);
    IEnumerable<Product> GetAll(); // Exposer IEnumerable ; ne pas exposer la liste interne.
}

public class InMemoryProductRepository : IProductRepository
{
    private readonly List<Product> _list = new();

    public void Add(Product p) => _list.Add(p);

    public Product? GetById(int id) => _list.FirstOrDefault(x => x.Id == id);

    // Si vous souhaitez retourner une copie protectrice : return _list.ToList();
    public IEnumerable<Product> GetAll() => _list;
}

// =======================
//  FACTORY (Création de produits)
// =======================

public static class ProductFactory
{
    // Création centralisée : pour ajouter un nouveau type, étendre le switch suffit.
    public static Product Create(ProductType type, int id)
        => type switch
        {
            ProductType.Book  => new Product(id, $"Livre-{id}",   29.90m),
            ProductType.Phone => new Product(id, $"Téléphone-{id}", 599.00m),
            _ => throw new ArgumentOutOfRangeException(nameof(type))
        };
}

// =======================
//  SINGLETON PARAMÈTRES
// =======================

// Singleton simple, thread-safe, chargé à la demande avec Lazy<T>.
public sealed class AppSettings
{
    private static readonly Lazy<AppSettings> _lazy = new(() => new AppSettings());
    public static AppSettings Instance => _lazy.Value;

    private AppSettings() { }

    public string StoreName { get; init; } = "Ma Boutique";
}

// =======================
//  OBSERVER (Modèle d’événement)
// =======================

// EventArgs personnalisé : structure extensible plutôt qu’une simple chaîne.
public sealed class OrderStatusChangedEventArgs : EventArgs
{
    public string Status { get; }
    public DateTime When { get; }
    public OrderStatusChangedEventArgs(string status, DateTime when)
    {
        Status = status;
        When = when;
    }
}

public class Order
{
    public event EventHandler<OrderStatusChangedEventArgs>? StatusChanged;

    public int Id { get; }
    public Product Product { get; }

    public Order(int id, Product product)
    {
        Id = id;
        Product = product;
    }

    public void UpdateStatus(string status)
    {
        Console.WriteLine($"Commande {Id} statut : {status}");
        StatusChanged?.Invoke(this, new OrderStatusChangedEventArgs(status, DateTime.Now));
    }
}

// Abonnés aux événements (écouteurs)
public class EmailNotifier
{
    public void OnOrderStatusChanged(object? sender, OrderStatusChangedEventArgs e)
        => Console.WriteLine($"[Email] Notification envoyée au client : {e.Status} ({e.When:T})");
}

public class SmsNotifier
{
    public void OnOrderStatusChanged(object? sender, OrderStatusChangedEventArgs e)
        => Console.WriteLine($"[SMS] Notification envoyée au client : {e.Status} ({e.When:T})");
}

// =======================
//  STRATEGY (Paiement)
// =======================

public interface IPaymentStrategy
{
    void Pay(decimal amount);
}

public class CreditCardPayment : IPaymentStrategy
{
    public void Pay(decimal amount) => Console.WriteLine($"{amount:0.00} payé par carte de crédit.");
}

public class PayPalPayment : IPaymentStrategy
{
    public void Pay(decimal amount) => Console.WriteLine($"{amount:0.00} payé via PayPal.");
}

// =======================
//  ADAPTER (Impression de reçu)
// =======================

// API héritée
public class LegacyPrinter
{
    public void PrintText(string t) => Console.WriteLine("[LegacyPrinter] " + t);
}

// Nouvelle interface
public interface IPrinter
{
    void Print(string text);
}

// Adaptateur : relie la nouvelle interface à l’API héritée.
public class PrinterAdapter : IPrinter
{
    private readonly LegacyPrinter _legacy;
    public PrinterAdapter(LegacyPrinter legacy) => _legacy = legacy;
    public void Print(string text) => _legacy.PrintText(text);
}

// =======================
//  DECORATOR (Message)
// =======================

public interface IMessage
{
    string GetText();
}

public class SimpleMessage : IMessage
{
    private readonly string _text;
    public SimpleMessage(string text) => _text = text;
    public string GetText() => _text;
}

// Décorateur qui journalise le message
public class LoggedMessageDecorator : IMessage
{
    private readonly IMessage _inner;
    public LoggedMessageDecorator(IMessage inner) => _inner = inner;

    public string GetText()
    {
        var text = _inner.GetText();
        var preview = text.Length > 60 ? text.Substring(0, 60) + "..." : text;
        Console.WriteLine("[Log] Message créé (aperçu) : " + preview);
        return text;
    }
}

// Décorateur simple qui « chiffre » le message avec Base64
public class EncryptedMessageDecorator : IMessage
{
    private readonly IMessage _inner;
    public EncryptedMessageDecorator(IMessage inner) => _inner = inner;

    public string GetText()
    {
        var raw = _inner.GetText();
        return Convert.ToBase64String(Encoding.UTF8.GetBytes(raw));
    }
}

// =======================
//  APPLICATION (Flux de démo)
// =======================

class Program
{
    static void Main()
    {
        // Formatage monétaire avec la culture fr-FR
        var frFR = new CultureInfo("fr-FR");

        Console.WriteLine($"Bienvenue chez : {AppSettings.Instance.StoreName}");
        Console.WriteLine();

        // Ajouter des produits via Repository + Factory
        IProductRepository repo = new InMemoryProductRepository();
        var p1 = ProductFactory.Create(ProductType.Book,  1);
        var p2 = ProductFactory.Create(ProductType.Phone, 2);
        repo.Add(p1);
        repo.Add(p2);

        // Lister les produits (prix sensible à la culture)
        Console.WriteLine("Produits en stock :");
        foreach (var p in repo.GetAll())
            Console.WriteLine($"- {p.Id}: {p.Name} {p.Price.ToString("C", frFR)}");
        Console.WriteLine();

        // Créer une commande et s’abonner aux événements
        var order = new Order(1001, p1);
        var email = new EmailNotifier();
        var sms   = new SmsNotifier();

        // Ajouter des écouteurs aux événements
        order.StatusChanged += email.OnOrderStatusChanged;
        order.StatusChanged += sms.OnOrderStatusChanged;

        // Paiement (Strategy)
        IPaymentStrategy payment = new CreditCardPayment();
        Console.WriteLine("Traitement du paiement :");
        payment.Pay(order.Product.Price);
        Console.WriteLine();

        // Cycle de vie de la commande (Observer déclenché)
        order.UpdateStatus("Approuvée");
        order.UpdateStatus("Expédiée");

        // (Optionnel) Désabonner le listener SMS
        order.StatusChanged -= sms.OnOrderStatusChanged;
        order.UpdateStatus("Livrée");
        Console.WriteLine();

        // Imprimer le reçu (API héritée via l’Adaptateur)
        IPrinter printer = new PrinterAdapter(new LegacyPrinter());
        printer.Print($"Reçu pour la commande #{order.Id} : {order.Product.Name} - {order.Product.Price.ToString("C", frFR)}");
        Console.WriteLine();

        // Traitement des messages (Décorateur)
        // Objectif : journaliser le texte brut, puis le chiffrer avant stockage/envoi.
        // Donc enchaîner Logged puis Encrypted.
        IMessage msg = new SimpleMessage($"Merci pour votre achat : {order.Product.Name} !");
        msg = new LoggedMessageDecorator(msg);      // 1) Log : journaliser le TEXTE BRUT
        msg = new EncryptedMessageDecorator(msg);   // 2) Chiffrement : produire du texte chiffré

        var finalText = msg.GetText();
        Console.WriteLine("Message final à stocker/envoyer (Base64) :");
        Console.WriteLine(finalText);
        Console.WriteLine();

        // Changer de stratégie à l’exécution
        payment = new PayPalPayment();
        Console.WriteLine("Deuxième paiement avec une autre stratégie :");
        payment.Pay(199.99m);

        Console.WriteLine("\nDémo terminée.");
    }
}

Articles connexes