“Патерн Вівторка” #12: Стан (State)

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

Як можна гарно й чітко реалізувати таку систему поведінки Замовлення?

СТАН
Можливі стани
Щоб поведінка Замовлення і його станів була зрозуміліла, глянемо на наступну діаграму станів (state-chart):

Ми можемо інкапсулювати поведінку що пов’язана із станом об’єкту в класах різних станів, що наслідуються від якогось базового класу. Кожена із конкретних реалізацій буде відповідальна за надання можливості переходу із одного стану в інший.

Класична UML-діаграма дизнайн патерну Стан

UML-діаграма патерну Стан для нашого прикладу із Замовленнями

Order State

І як же воно працює?

Для початку зауважимо, що клас 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 . . .

Нумо додамо ще трохи пивка до замовлення, яке вже нам відправили:

       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

Operation AddProduct is not allowed for Order’s state Shipped

Current Order’s state: Shipped
Press any key to continue . . .

Інші способи вирішити нашу проблему (не із пивом)

Одним із суттєвих недоліків цього дизайн патерну є розплід векилої кількості класів станів:

Але із іншої сторони саме так ми можемо чітко розділяти поведінку в залежності від станів. Я читав про вирішення цієї проблеми за допомогою таблички на подобі [state|method|state], що зберігає дозволені переходи. Проблема може бути також вирішена за допомогою свіча (мда щось воно не звучить)! Гарно про еволюцію цих підходів можна прочитати в книзі Jimmy Nilsson “Applying Domain-Driven Design and Patterns“.

Моя табличка Патернів

Developer's RoadMap To Success
2 коментарі

Add a Comment

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *