Уявімо, що ми маємо розробити програму для відправки Замовлень (Orders). Замовлення можуть бути в одному із декількох станів: Новий (New Order), Зареєстрований (Registered), Погоджений (Granted), Відправлений (Shipped), Оплачений (Invoiced), Відмінений (Cancelled).
Також є певні правила, по яких Замовлення може перейти в інший стан. Для прикладу не можна відправити не зареєстроване замовлення.
Крім правил переходу є ще й інші правила, що визначають поведінку вашого замовлення. Наприклад, не можна додати Продукт до Замовлення коли воно є у відміненому стані.
Як можна гарно й чітко реалізувати таку систему поведінки Замовлення?
СТАН
Можливі стани
Щоб поведінка Замовлення і його станів була зрозуміліла, глянемо на наступну діаграму станів (state-chart):
Класична UML-діаграма дизнайн патерну Стан
UML-діаграма патерну Стан для нашого прикладу із Замовленнями

І як же воно працює?
Для початку зауважимо, що клас Order має поле-посилання на стан _state. Для того щоб приклад вигладав більш правдоподібніше добавимо також товари _products.
public class Order
{
private OrderState _state;
private List _products = new List();
public Order()
{
_state = new NewOrder(this);
}
public void SetOrderState(OrderState state)
{
_state = state;
}
public void WriteCurrentStateName()
{
Console.WriteLine("Current Order's state: {0}", _state.GetType().Name);
}
//...
Order делегує специфічну для стану поведінку поточному стану:
public void Ship()
{
_state.Ship();
}
Наприклад, якщо поточний стан Granted, то метод _stete.Ship() змінить стан Order's на Shipped і якщо потрібно зробить ще якусь специфічну для цього стану роботу.
Код нижче зображає конкретну реалізацію одного із станів. Конструктор базового класу містить параметер типу Order, що дозволяє стану містити поле-посилання на його власника - на Замовлення якому присвоєний даний стан.
public class Granted : OrderState
{
public Granted(Order order) : base(order)
{
}
public override void AddProduct()
{
_order.DoAddProduct();
}
public override void Ship()
{
_order.DoShipping();
_order.SetOrderState(new Shipped(_order));
}
Якщо вас зацікавив метод DoShipping() то для нашого прикладу він просто виводить що він у процесі перевезення:
public void DoShipping()
{
Console.WriteLine("Shipping...");
}
Проте логіку яка стосується самого продукту ми можемо виконувати зразу у зовнішніх методах нашого замовлення не перевикликаючи її потім із стану, але це залежить від нас:
public void AddProduct(Product product)
{
_products.Add(product);
_state.AddProduct();
}
Якщо поточний стан Registered, то швидше за все такий стан не має перевизначеного методу ship(), а має тільки методи addProduct(), grant(), and cancel(). Таким чином метод базового класу буде викликаний. OrderState, він же базовий клас, має всі методи які можуть бути перевизначені у станах, але всі вони плуються ексепшинами, або ж просто виводять щось у консоль як у нашому прикладі:
public class OrderState
{
public Order _order;
public OrderState(Order order)
{
_order = order;
}
public virtual void Ship()
{
OperationIsNotAllowed("Ship");
}
// Other methods look similar...
private void OperationIsNotAllowed(string operationName)
{
Console.WriteLine("Operation {0} is not allowed for Order's state {1}", operationName, this.GetType().Name);
}
}
Приклад використання
Здійснимо певний перелік операцій із ствонення замовлення, додання до нього нашого улюбленого пива і доставки на дім:
public static void Run()
{
Product beer = new Product();
beer.Name = "MyBestBeer";
beer.Price = 78000;
Order order = new Order();
order.WriteCurrentStateName();
order.AddProduct(beer);
order.WriteCurrentStateName();
order.Register();
order.WriteCurrentStateName();
order.Grant();
order.WriteCurrentStateName();
order.Ship();
order.WriteCurrentStateName();
order.Invoice();
order.WriteCurrentStateName();
}
Вивід:
Current Order's state: NewOrder
Adding product...
Current Order's state: NewOrder
Registration...
Current Order's state: Registered
Granting...
Current Order's state: Granted
Shipping...
Current Order's state: Shipped
Invoicing...
Current Order's state: Invoiced
Press any key to continue . . .
Adding product...
Current Order's state: NewOrder
Registration...
Current Order's state: Registered
Granting...
Current Order's state: Granted
Shipping...
Current Order's state: Shipped
Invoicing...
Current Order's state: Invoiced
Press any key to continue . . .
Нумо додамо ще трохи пивка до замовлення, яке вже нам відправили:
order.Ship();
order.WriteCurrentStateName();
//trying to add more beer to already shipped order
order.AddProduct(beer);
order.WriteCurrentStateName();
Вивід:
Current Order's state: NewOrder
Adding product...
Current Order's state: NewOrder
Registration...
Current Order's state: Registered
Granting...
Current Order's state: Granted
Shipping...
Current Order's state: Shipped
Press any key to continue . . .
Adding product...
Current Order's state: NewOrder
Registration...
Current Order's state: Registered
Granting...
Current Order's state: Granted
Shipping...
Current Order's state: Shipped
Operation AddProduct is not allowed for Order's state Shipped
Current Order's state: ShippedPress any key to continue . . .
Інші способи вирішити нашу проблему (не із пивом)
Одним із суттєвих недоліків цього дизайн патерну є розплід векилої кількості класів станів:
Але із іншої сторони саме так ми можемо чітко розділяти поведінку в залежності від станів. Я читав про вирішення цієї проблеми за допомогою таблички на подобі [state|method|state], що зберігає дозволені переходи. Проблема може бути також вирішена за допомогою свіча (мда щось воно не звучить)! Гарно про еволюцію цих підходів можна прочитати в книзі Jimmy Nilsson "Applying Domain-Driven Design and Patterns".
Моя табличка Патернів



Автор видалив цей коментар.
ВідповістиВидалитиОбратите внимание на отличный FSM кит для .NET. Stateless - http://code.google.com/p/stateless/ . Намного проще (и прозрачнее) конфигурируется и работает. И каши (в виде большого количества подклассов) не просит :)
ВідповістиВидалити