→ Пошук по сайту       Увійти / Зареєструватися
Знання Патерни Патерни поведінки — Behavioral patterns

20. Стан — State

20.	Стан — State

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

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

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

Стан дозволяє винести логіку визначення стану об’єкту та його поведінку, характерну для цього стану, в інші класи.

Щоб поведінка замовлення і його станів була зрозуміла, глянемо на наступну діаграму станів (state-chart):

UML-діаграма 11. Діаграма станів замовлення

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

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

І як же воно працює? Для початку зауважимо, що клас Order має поле-посилання на стан _state. Для того, щоб приклад вигладав правдоподібніше, добавимо також товари _products.

Уривок коду 20.1. Замовлення

 class  Order
{
    private OrderState _state;
    private List<Product> _products = new List<Product>();
        
    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);
    }
    // І так далі...

Уривок коду 20.2. В базовому класі Order делегує поведінку поточному стану

 public void Ship()
{
    _state.Ship();
}

Наприклад, якщо поточний стан Granted, то метод _state.Ship() змінить стан замовлення на Shipped і якщо потрібно, зробить ще якусь специфічну для цього стану роботу.

Код нижче зображає конкретну реалізацію одного із станів. Конструктор базового класу містить параметер типу Order, що дозволяє стану містити поле-посилання на власника цього стану.

Уривок коду 20.3. Конкретна реалізація одного із станів

 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));
    }
    public override void Cancel()
    {
        _order.DoCancel();
        _order.SetOrderState(new Cancelled(_order));
    }
}

Якщо вас зацікавили методи класу Order на подобі DoShipping(), то в нашому прикладі вони імітують роботу, просто виводячи інформацію про дії з замовленням, але, звісно, ми вміщаємо там необхідну хитру логіку для виконання операцій, пов’язаних із поточним станом:

Уривок коду 20.4. Метод DoShipping для виконання загальної логіки

 public void DoShipping()
{
    // Тут водій вантажівки нагружає ваше замовлення і «рулить»
    Console.WriteLine("Shipping...");
}

З іншого боку, логіку, яка стосується самого продукту, ми можемо виконувати у зовнішніх методах нашого замовлення не перевикликаючи її потім із стану, але це залежить від нас:

Уривок коду 20.5. Додавання продукту до замовлення

 public void AddProduct(Product product)
{
    _products.Add(product);
    _state.AddProduct();
}

Якщо поточний стан Registered, то швидше за все такий стан не має перевизначеного методу ship(), а має тільки методи addProduct(), grant() та cancel(). Таким чином метод базового класу буде викликаний. OrderState, він же базовий клас, має всі методи, які можуть бути перевизначені у станах, але всі вони «плюються» ексепшинами, або ж просто виводять щось у консоль, як у нашому прикладі:

Уривок коду 20.6. Базовий клас стану замовлення

 class  OrderState
{
    public Order _order;

    public OrderState(Order order)
    {
        _order = order;
    }
    public virtual void AddProduct()
    {
        OperationIsNotAllowed("AddProduct");
    }
    // Наступні методи (Register, Grant, Ship, Invoice, Cancel) виглядають так же
    private void OperationIsNotAllowed(string operationName)
    {
        Console.WriteLine("Operation {0} is not allowed for Order's state {1}",    
                                            operationName, this.GetType().Name);
    }
}

Здійснимо певний перелік операцій із ствонення замовлення, додаванням до нього нашого улюбленого пива і доставки на дім:

Уривок коду 20.7. Декілька операцій для демонстрації використання

 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();

Вивід:

 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 . . .

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

Уривок коду 20.8. Демонстрація неправильного використання замовлення і поведінки стану

 // Пробуємо дозамовити пива до вже відправленого замовлення
// і дивимося що буде (див. довгий червоний рядок у виводі)
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 . . .

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

Малюнок 4. Розплід великої кількості класів-станів

Але з іншої сторони, саме так ми можемо чітко розділяти поведінку в залежності від станів. Я читав про вирішення цієї проблеми за допомогою таблички на зразок [state|method|state], що зберігає дозволені переходи. Проблема також може бути вирішена за допомогою switch («мда», щось воно не звучить)!

По матеріалам книги Андрія Будая "Дизайн патерни – просто, як двері". Матеріал розміщується за домовленістю з автором.
Робота представлена за умовами ліцензії Creative Commons Attribution-NonCommercial 3.0 Unported License.

загрузка...
Сторінки, близькі за змістом