State Machine : comment modéliser un workflow en C# ?

State Machine est un design pattern comportemental simple à comprendre, à mettre en oeuvre et à maintenir dans le temps. Il sert à modéliser un workflow et à gérer les transitions entre chaque état. C’est une problématique à laquelle nous sommes confrontés très souvent, et avant de s’aventurer dans du Workflow Foundations ou une autre solution lourde de gestion de workflow, pourquoi ne pas le faire nous-mêmes ?

Nous allons prendre l’exemple d’un workflow de commande relativement basique. Nous sommes dans le cas d’un processus asynchrone, et le workflow démarre à partir du moment où le client a validé ses informations d’adresse et bancaires.

Order Workflow

Dans cet exemple, la commande passe par une phase de détection de fraude. Si une possibilité de fraude est détectée, un mail est envoyé au client pour qu’il envoie un justificatif. Si au bout de 7 jours, ce justificatif n’est pas reçu, la commande est annulée. Dans le cas contraire, la commande sera préparée en logistique. Là encore, si un des produits n’est plus disponible en stock, la commande est annulée. Sinon, elle est payée. En cas de défaut de paiement, là encore, on annule. Sinon, la commande est expédiée, ce qui est le statut final.

Notre traitement sera exécutée par un automate à intervalles régulières.

Commençons par modéliser nos commandes :

using System;
using System.Collections.Generic;

namespace StateMachine
{
public class Order
{
public int Id { get; set; }
public State State { get; set; }
public DateTime CreationDate { get; set; }
public Customer Customer { get; set; }
public IEnumerable<OrderItem> OrderItems { get; set; }
public decimal Amount { get; set; }
public Currency Currency { get; set; }
public Address ShippingAddress { get; set; }
public Address BillingAddress { get; set; }
}
}

namespace StateMachine
{
public enum State
{
OrderInitialized,
FraudSuspected,
ProofReceived,
FraudEvaluated,
OrderPrepared,
OrderPayed,
OrderShipped,
OrderCancelled
}
}

namespace StateMachine
{
public class OrderItem
{
public int Id { get; set; }
public Product Product { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
}
}

namespace StateMachine
{
public class Product
{
public string Name { get; set; }
public string Brand { get; set; }
public decimal UnitPrice { get; set; }
}
}

namespace StateMachine
{
public class Customer
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public Sex Sex { get; set; }
public int NumberOfOrders { get; set; }
}
}

namespace StateMachine
{
public class Address
{
public int Id { get; set; }
public string Line1 { get; set; }
public string Line2 { get; set; }
public string Line3 { get; set; }
public string PostalCode { get; set; }
public Country Country { get; set; }
}
}

Nous avons également à notre disposition deux repositories permettant d’effectuer des opérations sur nos commandes :

using System;

namespace StateMachine.Repositories
{
public class OrderRepository
{

bool GetFraudRisk(Order order)
{
bool result = true;

if (order.Customer.NumberOfOrders == 0)
{
return false;
}

return result;
}

bool Prepare(Order order)
{
int numberOfOutOfStockProducts = 0;
OrderItemRepository orderItemRepository = new OrderItemRepository();
foreach (var orderItem in order.OrderItems)
{
if (!orderItemRepository.HasStock(orderItem))
{
numberOfOutOfStockProducts++;
}
}
return numberOfOutOfStockProducts == 0;
}

bool ProcessPayment(Order order)
{
return true;
}

void Ship(Order order)
{
//Ship;
}
}
}

namespace StateMachine.Repositories
{
class OrderItemRepository
{
internal bool HasStock(OrderItem orderItem)
{
return !(orderItem.Product.Name == "HyperPopulaires" == orderItem.Quantity > 1);
}
}
}

Voilà pour les classes qui nous intéressent. Sex, Country et Currency sont de simples enums. Nous allons maintenant pouvoir commencer à modéliser notre machine à états. Nous la voulons évidemment la plus simple possible. Dans l’implémentation du pattern State Machine que nous allons voir, chaque état de workflow contient les méthodes nécessaires pour avancer dans le workflow.

Alors qu’est-ce qu’un état ? C’est la position courante d’une commande, qui est donc une information portée par la commande. Nous allons avoir besoin d’une interface IStateRepository, qui pour chaque état possible du workflow effectuera les opérations de transition d’un état vers un autre :

namespace StateMachine.Repositories
{
public interface IStateRepository
{
void Process(Order order, OrderRepository orderRepository);
}
}

C’est tout ? C’est tout. Nous allons maintenant introduire le StateManager, qui contient simplement un dictionnaire State/IStateRepository. Il va nous servir à faire le mapping entre l’état porté par la commande et l’action à effectuer dans le repository :

using StateMachine.Repositories;
using System.Collections.Generic;

namespace StateMachine
{
public class StateManager
{
public Dictionary<State, IStateRepository> States { get; private set; }
public StateManager()
{
States = new Dictionary<State, IStateRepository>();
States.Add(State.OrderInitialized, new OrderInitializedRepository());
States.Add(State.FraudEvaluated, new FraudEvaluatedRepository());
States.Add(State.FraudSuspected, new FraudSuspectedRepository());
States.Add(State.OrderCancelled, new OrderCancelledRepository());
States.Add(State.OrderPayed, new OrderPayedRepository());
States.Add(State.OrderPrepared, new OrderPreparedRepository());
States.Add(State.OrderShipped, new OrderShippedRepository());
States.Add(State.ProofReceived, new ProofReceivedRepository());
}
}
}

Ce qui nous amène à la façon dont l’automate va faire avancer tout ce petit monde.

using StateMachine;
using StateMachine.Repositories;
using System.Collections.Generic;

namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
StateManager stateManager = new StateManager();
OrderRepository orderRepository = new OrderRepository();
IEnumerable<Order> orders = orderRepository.GetOrders();

foreach (var order in orders)
{
stateManager.States[order.State].Process(order);
}
}
}
}

Il suffit de récupérer la liste des commandes à traiter, et à l’aide d’une instance de StateManager d’appeler la méthode Process de l’IStateRepository associé à l’état de chaque commande. La dernière chose à faire est maintenant de développer chaque repository implémentant IStateRepository.

namespace StateMachine.Repositories
{
class OrderInitializedRepository : IStateRepository
{
public void Process(Order order, OrderRepository orderRepository)
{
bool isFraudPossible = orderRepository.GetFraudRisk(order);
if (isFraudPossible)
{
order.State = State.FraudSuspected;
}
else
{
order.State = State.FraudEvaluated;
}
}
}
}

Les autres repositories seront implémentés de la même façon. Chacun gère l’aiguillage vers le ou les prochains états. Le système est également facilement testable, puisqu’il suffit d’injecter une commande dans l’état et de vérifier que le Process fait bien passer sa propriété State à l’état attendu.

Le système est enfin facilement évolutif. Si l’on veut ajouter un nouvel état possible, il suffit de mettre à jour la classe StateManager et de modifier les états qui ont une transition vers celle-ci.

Le principal problème avec cette implémentation du pattern State Machine, contrepartie de son évolutivité, est que la représentation du workflow est diluée à travers l’ensemble des états. Il est souvent nécessaire de maintenir à jour en parallèle une représentation graphique de cette dernière, à des fins de documentation. Il existe d’autres implémentations de ce pattern, légèrement plus complexes, mais qui centralisent la formalisation du workflow. Nous en parlerons (peut-être) dans un prochain épisode.

3 Commentaires Laisser un commentaire

Bonjour,

Excellent billet !
Est il envisageable d’obtenir un lien vers la source du projet complet ?

Merci

Répondre

je serais aussi intéressé !

Répondre
BILISSOR Yann
juin 1, 2016 16:59

Bonjour,

Petite erreur. Vous avez oublié de passer l’orderRepository à la fonction Process dans votre démo. Toute fois, l’article est intéressant et simple à comprendre.

Répondre

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *