Design Pattern State
Faire varier le comportement d’un objet en fonction de son état
Introduction
Le design pattern State est un pattern de conception comportemental dont l'objectif est de permettre à un objet, de modifier son comportement lorsque son état (constitué de ses champs et propriétés) interne change.
Dans une application, le comportement d'un objet dépend parfois de son état courant. Une même opération peut être autorisée, interdite ou produire un résultat différent selon cet état. Une première approche consiste à utiliser des structures conditionnelles telles que if ou switch, afin de déterminer le traitement à exécuter. Cependant, plus le nombre d'états possibles augmente, et plus les conditions risquent de se multiplier dans la classe, rendant le code plus difficile à maintenir et à faire évoluer.
Le design pattern State propose une solution, en implémentant les comportements associés à chaque état dans des classes distinctes.
Problématique
Nous développons une application commerciale permettant de gérer des commandes. Une commande peut successivement être : en attente de paiement, payée, expédiée, livrée, annulée, etc. Nous pouvons implémenter ces différents états dans une énumération :
Nous utilisons cette énumération dans la classe Order pour définir son état courant :
public class Order
{
public OrderStatus Status { get; private set; }
public Order()
{
Status = OrderStatus.Pending;
}
public void Pay()
{
if (Status != OrderStatus.Pending)
throw new InvalidOperationException("La commande ne peut pas être payée.");
Console.WriteLine("Paiement de la commande.");
Status = OrderStatus.Paid;
}
public void Ship()
{
if (Status != OrderStatus.Paid)
throw new InvalidOperationException(
"La commande ne peut pas être expédiée.");
Console.WriteLine("Expédition de la commande.");
Status = OrderStatus.Shipped;
}
public void Deliver()
{
if (Status != OrderStatus.Shipped)
throw new InvalidOperationException("La commande ne peut pas être livrée.");
Console.WriteLine("Livraison de la commande.");
Status = OrderStatus.Delivered;
}
public void Cancel()
{
if (Status == OrderStatus.Shipped ||
Status == OrderStatus.Delivered ||
Status == OrderStatus.Cancelled)
throw new InvalidOperationException(
"La commande ne peut pas être annulée.");
Console.WriteLine("Annulation de la commande.");
Status = OrderStatus.Cancelled;
}
}
Cette implémentation est correcte, mais la classe Order a plusieurs responsabilités. Elle représente la commande, détermine les opérations autorisées pour chaque état et gère aussi les transitions entre ces états. L'ajout de nouveaux états ou de nouvelles opérations risque progressivement d'augmenter le nombre de conditions et d'opérations.
Le design pattern State et les machines à états
Le fonctionnement du design pattern State est similaire à celui d'une machine à états. Dans notre exemple, les transitions autorisées peuvent être représentées de cette manière :
Chaque état détermine les transitions pouvant être réalisées qui permettent de changer d'état.
Conception
Le diagramme de classes ci-dessous présente la structure du design pattern State appliquée à notre système de gestion des commandes. La classe Order, qui représente le contexte, possède une référence vers l'interface IOrderState représentant son état courant. Les classes PendingOrderState, PaidOrderState, ShippedOrderState, DeliveredOrderState et CancelledOrderState constituent les états et implémentent le comportement de la commande pour chacun d'entre eux.
Le design pattern State fait intervenir plusieurs éléments :
- Le Context qui représente l'objet dont le comportement dépend de son propre état. Il est implémenté par la classe
Order. - Le State qui définit les opérations dont le comportement peut varier selon l'état. Il est implémenté par l'interface
IOrderState. - Le Concrete State qui représente un état particulier et implémente les opérations correspondantes. Il s'agit des classes
PendingOrderState,PaidOrderState,DeliveredOrderState,ShippedOrderStateetCancelledOrderState.
Implémentation
Le design pattern State permet de déplacer ces comportements vers des objets spécialisés. Commençons par définir une interface représentant l'état d'une commande :
public interface IOrderState
{
void Pay(Order order);
void Ship(Order order);
void Deliver(Order order);
void Cancel(Order order);
}
Chaque état de la commande est ensuite représenté par une classe implémentant cette interface :
public class PaidOrderState : IOrderState
{
public void Pay(Order order)
{
throw new InvalidOperationException("La commande est déjà payée.");
}
public void Ship(Order order)
{
Console.WriteLine("Expédition de la commande.");
order.ChangeState(new ShippedOrderState());
}
public void Deliver(Order order)
{
throw new InvalidOperationException("Une commande payée doit être expédiée avant d'être livrée.");
}
public void Cancel(Order order)
{
Console.WriteLine("Annulation de la commande.");
order.ChangeState(new CancelledOrderState());
}
}
public class PendingOrderState : IOrderState
{
public void Pay(Order order)
{
Console.WriteLine("Paiement de la commande.");
order.ChangeState(new PaidOrderState());
}
public void Ship(Order order)
{
throw new InvalidOperationException("Une commande en attente ne peut pas être expédiée.");
}
public void Deliver(Order order)
{
throw new InvalidOperationException("Une commande en attente ne peut pas être livrée.");
}
public void Cancel(Order order)
{
Console.WriteLine("Annulation de la commande.");
order.ChangeState(new CancelledOrderState());
}
}
public class ShippedOrderState : IOrderState
{
public void Pay(Order order)
{
throw new InvalidOperationException("La commande est déjà payée.");
}
public void Ship(Order order)
{
throw new InvalidOperationException("La commande est déjà expédiée.");
}
public void Deliver(Order order)
{
Console.WriteLine("Livraison de la commande.");
order.ChangeState(new DeliveredOrderState());
}
public void Cancel(Order order)
{
throw new InvalidOperationException("Une commande expédiée ne peut plus être annulée.");
}
}
public class DeliveredOrderState : IOrderState
{
public void Pay(Order order)
{
throw new InvalidOperationException("La commande est déjà payée.");
}
public void Ship(Order order)
{
throw new InvalidOperationException("La commande est déjà expédiée.");
}
public void Deliver(Order order)
{
throw new InvalidOperationException("La commande est déjà livrée.");
}
public void Cancel(Order order)
{
throw new InvalidOperationException("Une commande livrée ne peut plus être annulée.");
}
}
public class CancelledOrderState : IOrderState
{
public void Pay(Order order)
{
throw new InvalidOperationException("Une commande annulée ne peut pas être payée.");
}
public void Ship(Order order)
{
throw new InvalidOperationException("Une commande annulée ne peut pas être expédiée.");
}
public void Deliver(Order order)
{
throw new InvalidOperationException("Une commande annulée ne peut pas être livrée.");
}
public void Cancel(Order order)
{
throw new InvalidOperationException("La commande est déjà annulée.");
}
}
Le contexte
Le contexte est la classe dont le comportement dépend de son état. Dans notre exemple, il s'agit de la classe Order :
public class Order
{
private IOrderState _state;
public Order()
{
_state = new PendingOrderState();
}
internal void ChangeState(IOrderState state)
{
_state = state;
}
public void Pay()
{
_state.Pay(this);
}
public void Ship()
{
_state.Ship(this);
}
public void Deliver()
{
_state.Deliver(this);
}
public void Cancel()
{
_state.Cancel(this);
}
}
La méthode
ChangeStatene devrait pas être accessible au code client, car celui-ci pourrait imposer une transition qui ne respecte pas les règles métier.
Cette classe ne contient plus de conditions permettant de déterminer les opérations à exécuter en fonction de son état. Elle délègue cette responsabilité à son champ nommé _state. Voici un exemple de traitement d'une commande :
Lors de l'appel à la méthode Pay(), la commande est associée à une instance de la classe PendingOrderState, qui exécute les opérations correspondant au paiement, puis remplace l'état courant par une instance de la classe PaidOrderState :
L'appel à la méthode Ship() sur la commande est donc automatiquement délégué à une instance de la classe PaidOrderState. Ainsi, le comportement de la commande évolue au fur et à mesure des modifications de son état, sans que le code qui l'utilise ait besoin de connaître son état courant.
Avantages
Le design pattern State permet de regrouper dans une même classe les opérations associées à un état particulier. Cela permet :
- D'éviter la multiplication de structures conditionnelles telles que
ifouswitchlorsque son comportement dépend fortement de son état. - L'ajout d'un nouvel état peut être réalisé en créant une nouvelle classe implémentant l'interface
IOrderState, limitant les modifications apportées aux états existants. - Rendre les transitions possibles plus explicites, étant implémentées dans les classes représentant les états.
Les design pattersn State et Strategy
Les design patterns State et Strategy possèdent une structure relativement proche : un objet délègue un comportement à une autre abstraction pouvant posséder plusieurs implémentations. Cependant, leur objectif est différent.
Avec le design pattern Strategy, une implémentation est sélectionnée afin de choisir une manière de réaliser un traitement.
Avec le design pattern State, l'implémentation utilisée représente l'état courant d'un objet. Elle peut évoluer au cours de son cycle de vie et les différents états participent à la définition des transitions possibles.
Ainsi, le pattern Strategy permet de faire varier le comportement d'un algorithme, alors que le pattern State permet de faire varier le comportement d'un objet en fonction de son état.

