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;
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");
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
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.
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.
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.
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.