26 жовтня 2010 р.

"Патерн Вівторка" #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 коментарі:

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

    ВідповістиВидалити