Les design patterns sont des méthodes fréquemment utilisées lors du développement logiciel. Ceux-ci permettent notamment d’optimiser le code informatique, de le clarifier et surtout le rendre plus robuste.
Design Pattern d’état
Un design pattern apporte une solution à un problème fréquemment rencontré en programmation orientée objet. Dans les années 90, le « Gang Of Four” dans son ouvrage “Design Patterns: Elements of Reusable Object-Oriented Software” en a fait ressortir 3 catégories :
- Les patrons de création
- Les patrons de comportement
- Les patrons de structure
Le design pattern State, ou Design Pattern d’état, fait partie de la famille des patrons de comportement. Ces types de patterns décrivent une structure de classes pour le comportement de l’application (une réponse à un évènement par exemple). Ainsi, le changement d’état de l’objet permet de modifier son comportement.
Enoncé du problème
Imaginez que vous êtes amenés à gérer un changement d’état. En fonction de l’état de vos objets, vos actions auront un comportement différent. Ceci est un cas de figure que l’on retrouve régulièrement dans nos spécifications.
La première idée que vous pourriez avoir pour résoudre ce problème serait d’implémenter un automate fini, en se basant sur un switch, de la façon suivante :
switch (state) { case 1: A(); break; case 2: B(); break; case 3: C(); break; }
Solution avec le patron de conception d’état
L’idée principale du state pattern est de créer de nouvelles classes pour l’ensemble des états possibles d’un objet et d’extraire les comportements liés aux états dans ces classes.
Ainsi l’objet original, qui sera nommé contexte, stocke une référence vers un des objets, état qui représente son état actuel. Tout ce qui concerne la manipulation des états est donc délégué à cet objet.
Toutes les classes doivent implémenter la même interface de façon à pouvoir faire passer le contexte d’un état à un autre, en remplaçant l’objet état par celui qui représente son nouvel état.
Cas théorique : exemple d’implémentation
Nous allons créer une classe qui va prendre le rôle du contexte :
using System; public class Context { // Reference to the current state of the Context private State _state = null; public Context(State state) { this.MoveTo(state); } public void MoveTo(State state) { Console.WriteLine($"Context: Move to {state.GetType().Name}"); this._state = state; this._state.SetContext(this); } public void Request() { this._state.DoSomething(this); } }
Nous déclarons « classe abstraite » l’état qui va encapsuler le comportement des états :
public abstract class State { protected Context _context; public void SetContext(Context context) { this._context = context; } public abstract void DoSomething(); }
Pour chaque état, nous créons une classe qui dérive de cette classe abstraite contenant le code qui concerne cet état :
public class ConcreteStateA : State { public override void DoSomething() { Console.WriteLine("ConcreteStateA wants to change the state of the context."); this._context.MoveTo(new ConcreteStateB()); } } public class ConcreteStateB : State { public override void DoSomething() { Console.WriteLine("ConcreteStateB wants to change the state of the context."); this._context.MoveTo(new ConcreteStateC()); } } public class ConcreteStateC : State { public override void DoSomething() { Console.WriteLine("ConcreteStateC wants to change the state of the context."); this._context.MoveTo(new ConcreteStateA()); } }
Enfin pour changer l’état du contexte, nous créons une instance de l’une des classes états et on la passe au contexte :
public class program { public static void Main(string[] args) { var context = new Context(new ConcreteStateA()); context.Request(); context.Request(); context.Request(); } }
Voici le résultat à l’exécution :
Context: Move to ConcreteStateA ConcreteStateA wants to change the state of the context. Context: Move to ConcreteStateB ConcreteStateB wants to change the state of the context. Context: Move to ConcreteStateC ConcreteStateC wants to change the state of the context. Context: Move to ConcreteStateA
Cas concret : Le lecteur audio
Partons du principe que nous avons un lecteur audio avec 1 bouton qui correspond à 1 action possible pour l’utilisateur : - Play : “P”
Néanmoins le comportement de ces actions sera différent selon l’état du lecteur. Nous allons commencer par 2 états : - Lecture - Pause
Reprenons l’exercice précédent. Nous commençons par créer notre contexte (ici nommé Reader) de notre lecture, qui reprendra notre action :
using System; public class Reader { // Reference to the current state of the Reader private ReaderState _state = null; public Reader(ReaderState state) { this._state = state; } public void PressPlay() { this._state.PressPlay(this); } public ReaderState CurrentState { get { return _state; } set { _state = value; } } }
Ensuite notre classe abstaite d’état ainsi que nos deux classes concrètes qui concernent l’état de lecture et de pause :
public abstract class ReaderState { public abstract void PressPlay(Reader reader); } public class ReaderPlayingState : ReaderState { public ReaderPlayingState() { Console.WriteLine("Reader playing"); } public override void PressPlay(Reader reader) { reader.CurrentState = new ReaderPausedState(); } } public class ReaderPausedState : ReaderState { public ReaderPausedState() { Console.WriteLine("Reader paused"); } public override void PressPlay(Reader reader) { reader.CurrentState = new ReaderPlayingState(); } }
Afin de pouvoir tester notre implémentation de façon ludique nous allons rajouter l’action Play/Pause dans le programme.cs à l’aide d’un console.readkey qui va attendre la touche P :
public class program { public static void Main(string[] args) { var reader = new Reader(new ReaderPausedState()); ConsoleKeyInfo cki; Console.WriteLine($"Press P to launch : "); do { cki = Console.ReadKey(); if(cki.Key.ToString().Equals("P")) { Console.WriteLine($"{Environment.NewLine}"); reader.PressPlay(); } } while (cki.Key != ConsoleKey.Escape); } }
Ainsi en appuyant plusieurs fois, on obtient le changement d’état suivant :
Reader paused
Press P to launch :
P
Reader playing
P
Reader paused
P
Reader playing
Si vous souhaitez ajouter de nouveaux états, comme une avance rapide par exemple, il vous suffira dans un premier temps d’ajouter la classe concrète correspondant à cet état. Puis, dans un second temps, vous devrez mettre à jour les différentes actions selon vos envies, tout en respectant les principes SOLID.
Conclusion
Si le comportement d’un de vos objets est amené à varier en fonction de son état, ce patron de conception peut être une solution afin de respecter les principes SOLID de responsabilité unique et ouvert/fermé. Pour cela, il faut organiser le code lié aux différents états dans des classes séparées tout en ajoutant de nouveaux états sans modifier les classes états ou le contexte existant. La maintenance du code sera simplifiée en évitant les gros blocs de conditionnels de l’automate.