Aller au contenu principal

Design Patterns — Singleton, Factory, Strategy, Observer

Notions théoriques

Les design patterns (ou patrons de conception) sont des solutions réutilisables à des problèmes récurrents de conception logicielle. Ce ne sont pas des bibliothèques à importer, mais des modèles d'organisation du code.

Singleton

Le Singleton garantit qu'une classe n'a qu'une seule instance dans toute l'application. Utilisé pour les gestionnaires globaux (configuration, connexion à une base de données, gestionnaire de jeu).

public class GestionnaireJeu
{
// Lazy<T> : instanciation différée ET thread-safe
private static readonly Lazy<GestionnaireJeu> _instance =
new Lazy<GestionnaireJeu>(() => new GestionnaireJeu());

// Constructeur privé : impossible d'instancier de l'extérieur
private GestionnaireJeu()
{
Console.WriteLine("GestionnaireJeu créé.");
}

// Point d'accès global
public static GestionnaireJeu Instance => _instance.Value;

public int Score { get; set; }
public void DemarrerPartie() => Console.WriteLine("Partie démarrée !");
}

// Utilisation
GestionnaireJeu.Instance.DemarrerPartie();
GestionnaireJeu.Instance.Score = 100;
attention

Le Singleton facilite les tests unitaires difficiles à écrire car la dépendance est cachée. Préférez l'injection de dépendances dans les projets ASP.NET Core (voir séance 700). Réservez le Singleton aux cas véritablement globaux (ex: logger, cache en mémoire).

Factory

La Factory (ou Fabrique) centralise la création d'objets. Au lieu de savoir comment construire chaque type concret, le code client demande à la fabrique de créer l'objet voulu.

public enum TypePersonnage { Guerrier, Mage, Archer }

public class PersonnageFactory
{
public static Personnage Creer(TypePersonnage type, string nom)
{
return type switch
{
TypePersonnage.Guerrier => new Guerrier { Nom = nom, Force = 80, Defense = 60 },
TypePersonnage.Mage => new Mage { Nom = nom, Force = 90, Mana = 200 },
TypePersonnage.Archer => new Archer { Nom = nom, Force = 70, Precision = 85 },
_ => throw new ArgumentException($"Type inconnu : {type}")
};
}
}

// Utilisation : le code client ne connaît pas les détails de construction
var guerrier = PersonnageFactory.Creer(TypePersonnage.Guerrier, "Aragorn");
var mage = PersonnageFactory.Creer(TypePersonnage.Mage, "Gandalf");

Strategy

Le pattern Strategy permet de choisir un algorithme à l'exécution en l'injectant comme dépendance. Dans le RPG, cela sert à varier la façon dont un personnage attaque.

// Interface Strategy
public interface IStrategieAttaque
{
int CalculerDegats(int force);
string Description { get; }
}

// Stratégies concrètes
public class AttaqueNormale : IStrategieAttaque
{
public string Description => "Attaque normale";
public int CalculerDegats(int force) => force;
}

public class AttaqueCritique : IStrategieAttaque
{
public string Description => "Attaque critique x2";
public int CalculerDegats(int force) => force * 2;
}

// Personnage qui délègue son calcul de dégâts à la stratégie injectée
public class Personnage
{
public string Nom { get; set; } = "";
public int Force { get; set; }
public IStrategieAttaque Strategie { get; set; } = new AttaqueNormale();

public int Attaquer() => Strategie.CalculerDegats(Force);
}

// Utilisation
var guerrier = new Personnage { Nom = "Aragorn", Force = 80, Strategie = new AttaqueNormale() };
Console.WriteLine($"Dégâts : {guerrier.Attaquer()}"); // 80

guerrier.Strategie = new AttaqueCritique();
Console.WriteLine($"Dégâts : {guerrier.Attaquer()}"); // 160

Observer avec événements C#

Le pattern Observer définit une relation 1-N : quand un objet change d'état, tous ses observateurs sont notifiés. En C#, les événements (event) implémentent ce pattern de façon idiomatique.

public class Combat
{
// Observable : l'événement
public event Action<string>? AttaqueEffectuee;

public void LancerAttaque(string attaquant, string cible)
{
var message = $"{attaquant} attaque {cible} !";
AttaqueEffectuee?.Invoke(message);
}
}

// Observateurs
var combat = new Combat();
combat.AttaqueEffectuee += msg => Console.WriteLine($"[JOURNAL] {msg}");
combat.AttaqueEffectuee += msg => Console.WriteLine($"[UI] Affichage : {msg}");

combat.LancerAttaque("Aragorn", "Saruman");
info

La différence entre Observer et les événements C# : les événements sont la façon native de C# d'implémenter Observer. Ils sont plus sûrs que les implémentations manuelles (le mot-clé event empêche les abonnés d'effacer accidentellement les autres abonnements).

Exemple pratique

using System;

// === Singleton ===
public class GestionnaireJeu
{
private static readonly Lazy<GestionnaireJeu> _instance =
new Lazy<GestionnaireJeu>(() => new GestionnaireJeu());
private GestionnaireJeu() { }
public static GestionnaireJeu Instance => _instance.Value;
public int TourActuel { get; private set; } = 1;
public void PasserTourSuivant() => TourActuel++;
}

// === Entités ===
public abstract class Personnage
{
public string Nom { get; set; } = "";
public int Force { get; set; }
public int PointsDeVie { get; protected set; } = 100;
public IStrategieAttaque Strategie { get; set; } = new AttaqueNormale();
public int Attaquer() => Strategie.CalculerDegats(Force);
}
public class Guerrier : Personnage { public Guerrier() { Force = 80; } }
public class Mage : Personnage { public Mage() { Force = 90; } }

// === Strategy ===
public interface IStrategieAttaque
{
int CalculerDegats(int force);
string Description { get; }
}
public class AttaqueNormale : IStrategieAttaque
{
public string Description => "Normale";
public int CalculerDegats(int force) => force;
}
public class AttaqueCritique : IStrategieAttaque
{
public string Description => "Critique x2";
public int CalculerDegats(int force) => force * 2;
}

// === Factory ===
public enum TypePersonnage { Guerrier, Mage }
public class PersonnageFactory
{
public static Personnage Creer(TypePersonnage type, string nom) => type switch
{
TypePersonnage.Guerrier => new Guerrier { Nom = nom },
TypePersonnage.Mage => new Mage { Nom = nom },
_ => throw new ArgumentException($"Type inconnu : {type}")
};
}

// === Observer ===
public class Combat
{
public event Action<string>? AttaqueEffectuee;

public void Attaquer(Personnage attaquant, Personnage cible)
{
int degats = attaquant.Attaquer();
var message = $"Tour {GestionnaireJeu.Instance.TourActuel} — " +
$"{attaquant.Nom} [{attaquant.Strategie.Description}] " +
$"inflige {degats} dégâts à {cible.Nom}";
AttaqueEffectuee?.Invoke(message);
GestionnaireJeu.Instance.PasserTourSuivant();
}
}

class Program
{
static void Main()
{
// Factory
var aragorn = PersonnageFactory.Creer(TypePersonnage.Guerrier, "Aragorn");
var gandalf = PersonnageFactory.Creer(TypePersonnage.Mage, "Gandalf");

// Observer
var combat = new Combat();
combat.AttaqueEffectuee += msg => Console.WriteLine($"[JOURNAL] {msg}");
combat.AttaqueEffectuee += msg => Console.WriteLine($"[SCORE] Tour terminé");

// Strategy
aragorn.Strategie = new AttaqueNormale();
combat.Attaquer(aragorn, gandalf);

aragorn.Strategie = new AttaqueCritique();
combat.Attaquer(aragorn, gandalf);
}
}

Test de mémorisation/compréhension


Quel est le rôle du pattern Singleton ?


Pourquoi utilise-t-on Lazy<T> pour implémenter un Singleton ?


Quel est l'avantage du pattern Factory ?


Dans le pattern Strategy, comment change-t-on l'algorithme utilisé ?


Comment le pattern Observer est-il implémenté de façon idiomatique en C# ?


TP pour réfléchir et résoudre des problèmes

Dans ce TP, vous allez implémenter les patterns Factory, Strategy et Observer dans votre projet RPG.

Étape 1 — Implémenter le pattern Factory

Créez une PersonnageFactory qui crée des personnages selon un TypePersonnage.


Bonne pratique - Le cas _ dans le switch expression

Le cas _ est le cas par défaut du switch expression. Il est important de le faire lancer une exception explicite (throw new ArgumentException(...)) plutôt que de retourner null : si un nouveau type est ajouté à l'énumération sans mise à jour de la Factory, le bug est détecté immédiatement.

Étape 2 — Implémenter le pattern Strategy

Créez une interface IStrategieAttaque et deux implémentations : AttaqueNormale et AttaqueCritique.


Bonne pratique - Injecter la stratégie plutôt que de mettre des if/switch dans la méthode

Sans Strategy, la méthode Attaquer() contiendrait un switch sur le type d'attaque. Chaque nouvel type d'attaque nécessiterait de modifier cette méthode. Avec Strategy, on ajoute simplement une nouvelle classe sans toucher au code existant. C'est le principe Open/Closed (ouvert à l'extension, fermé à la modification).

Étape 3 — Connecter Factory, Strategy et Observer

Dans Main, utilisez la Factory pour créer les personnages, assignez des stratégies et abonnez-vous à l'événement de combat.


Bonne pratique - Les patterns se complètent

Factory, Strategy et Observer sont souvent utilisés ensemble : Factory crée les objets avec la bonne configuration initiale, Strategy détermine le comportement à l'exécution, et Observer diffuse les événements importants sans coupler les composants. Cette combinaison est au cœur des architectures logicielles modernes.

📌 Une solution