Сообщений 9    Оценка 240        Оценить  
Система Orphus

Паттерн проектирования State

Примеры реализации

Автор: Михаил Новиков
Источник: RSDN Magazine #4-2005
Опубликовано: 17.02.2006
Исправлено: 10.12.2016
Версия текста: 1.0
State
List State
Enum State
Индуктивный пользовательский интерфейс

Примеры к статье

State

Паттерн State (Состояние) используется в тех случаях, когда во время выполнения программы объект должен менять свое поведение в зависимости от своего состояния.


Рисунок 1. Диаграмма классов паттерна Состояние.

Паттерн состоит из 3 блоков:

Реализация паттерна Состояние
      public
      interface IState
{
  event StateHandler NextState;

  void SomeMethod();
}

publicinterface IWidget
{
  void SomeMethod();
}

publicclass StateA : IState
{
  publicevent StateHandler NextState;

  publicvoid SomeMethod()
  {
    Console.WriteLine("StateA.SomeMethod");
    if (NextState != null)
    {
      NextState(new StateB());
    }
  }
}

publicclass StateB : IState
{
  publicevent StateHandler NextState;

  publicvoid SomeMethod()
  {
    Console.WriteLine("StateB.SomeMethod");
    if (NextState != null)
    {
      NextState(new StateA());
    }
  }
}

publicdelegatevoid StateHandler(IState state);

publicclass Widget : IWidget
{
  public Widget(IState state)
  {
    OnNextState(state);    
  }

  privatevoid OnNextState(IState state)
  {
    if (state == null)
      thrownew ArgumentNullException("state");

    if (State != state)
    {
      State = state;
      State.NextState += new StateHandler(OnNextState);
    }
  }

  private IState state;    

  publicvoid SomeMethod()
  {
    state.SomeMethod();
  }

  public IState State
  {
    get { return state; }
    set { state = value; }
  }
}

Рассмотрим пример:

IWidget widget = new Widget(new StateA());
widget.SomeMethod();
widget.SomeMethod();

При создании объекта Widget через параметр конструктора передается объект, инкапсулирующий состояние. Это состояние будет являться текущим, на его событие NextState подписывается метод OnNextState(), который заменяет state на присланный объект состояния. При вызове метода SomeMethod() в первый раз объект Widget делегирует этот вызов объекту StateA. После того, как метод StateA.SomeMethod() выполнился, объект вызовет событие NextState и передаст в параметр объект StateB, который заменяет текущее состояние StateA. При вызове SomeMethod() второй раз будет вызван StateB.SomeMethod(). То есть формально вызывается один и тот же метод, но его поведение различно.

Важно отметить, что реально в данный момент времени существует только текущий объект состояния. То есть при переходе из одного состояния в другое, предыдущий объект удаляется и на его место встает новый.

Такого же эффекта, как и от паттерна State, можно было добиться с помощью использования условных операторов и констант, или перечислений.

      public
      class Widget 
{
 publicconstint StateA = 0;
 publicconstint StateB = 1;

 privateint state;
 
 publicvoid SomeMethod() 
 {
  switch (state) 
  {
   case StateA:
    Console.WriteLine("StateA.SomeMethod");
    state = StateB;
    break;
   case StateB:
    Console.WriteLine("StateB.SomeMethod");
    state = StateA;
    break;
  }
 }
}

В случае с двумя состояниями такой код приемлем и выглядит довольно компактно по сравнению с приведенной выше реализацией паттерна. Однако представьте ситуацию, когда состояний намного больше, и методов Widget, которые зависят от них, больше одного.

List State

Есть один существенный недостаток: очередность состояний закреплена в самих классах состояний. Справиться с этим можно сразу несколькими способами.

В частности, возможны случаи, когда состояния идут по очереди одно за другим, пока не будет достигнут конец очереди. Если немного изменить код, то применимы следующие конструкции:

IWidget widget = new Widget(new StateA(new StateB(new StateC())));
widget.SomeMethod(); // Вызывается StateA.SomeMethod
widget.SomeMethod(); // Вызывается StateB.SomeMethod
widget.SomeMethod(); // Вызывается StateC.SomeMethod
widget.SomeMethod(); // Опять StateC.SomeMethod

В данном случае все объекты должны быть созданы вместе с Widget, что влияет на количество занимаемой памяти. Это является одним из минусов данного подхода. Также возникает проблема с тем, чтобы замкнуть цепочку, то есть, чтобы за StateC следовал StateA или любое другое состояние. Чтобы решить ее, можно реализовать список на основе объектов состояний. Для этого каждый из объектов должен содержать ссылку на предыдущий и следующий элемент. Так, достигнув конца списка, можно по цепочке вернуться к началу или к другому элементу. Примером такому поведения может служить светофор, где через определенные промежутки времени меняются цвета.

Возможно также создать список в самом Widget (с помощью одного из контейнеров) и заполнить его объектами состояний. Так, при появлении события меняется индекс, указывающий на элемент в списке, тем самым меняется и состояние. Если развить эту идею, то список можно заменить ассоциативным контейнером, и тогда при появлении события передавать через аргументы ключ, по которому находить следующее состояние в контейнере.

Избавиться от хранения объектов состояний можно, если хранить не сами объекты, а их типы, и во время выполнения программы создавать экземпляры. Тогда код может выглядеть так:

IWidget widget = new Widget(new StateA(typeof(StateB), typeof(StateC)));

То есть в конструктор состояния передается список типов состояний, в том порядке, в котором они должны сменять друг друга. Тогда при переходе в другое состояние необходимо получить крайний первый элемент списка и создать объект этого типа, передав в конструктор преобразованный список без первого элемента. Если элементов в списке нет, сохраняется текущее состояние. Другой вариант подразумевает, передачу между состояниями всего списка типов и дополнительно индекса, указывающий на текущий тип состояния. Здесь появляется возможность замкнуть смену состояний.

Ответственность за хранение списка типов можно переложить на Widget. Благодаря этому пользователь может выбирать состояния во время выполнения программы.

Альтернативой является использование generic-ов.


Рисунок 2. Диаграмма классов паттерна List State.

Реализация паттерна List State
      public
      interface IState
{
  event StateHandler NextState;

  void SomeMethod();  
}
publicabstractclass State<T> : IState
  where T : IState, new()
{
  publicevent StateHandler NextState;

  publicvirtualvoid SomeMethod()
  {
    if (NextState != null)
    {        
      if (typeof(T) != typeof(NullState))
      {
        T state = new T();
        NextState(new T());
      }
    }
  }  
}
publicclass StateA<T> : State<T> 
  where T : IState, new()
{
  publicoverridevoid SomeMethod()
  {
    Console.WriteLine("StateA.SomeMethod");
    base.SomeMethod();
  }        
}
publicclass StateB<T> : State<T>
  where T : IState, new()
{
  publicoverridevoid SomeMethod()
  {
    Console.WriteLine("StateB.SomeMethod");      
    base.SomeMethod();
  }    
}
publicclass NullState : IState
{
  publicevent StateHandler NextState;

  publicvoid SomeMethod()
  {
  }
}

Пример использования:

IState state = new StateA<StateB<NullState>>();
Widget widget = new Widget(state);
widget.SomeMethod();

При реализации классов состояний с помощью generic-ов возникает необходимость обозначить конец последовательности. Для этой цели предназначен NullState. При смене состояния проверяется, является ли типом следующего состояния NullState, если да, то смены не происходит. Такой способ позволяет менять последовательность состояний во время выполнения с помощью вызовов метода Type.MakeGenericType().

Enum State

Интерес представляет реализация паттерна State на Java с помощью перечислений. В Java перечисление является классом, поэтому перечисления могут реализовывать интерфейсы или являться подклассами. С помощью перечислений удобно работать с состояниями, так как существует возможность создания общих для всех и уникальных для каждого состояния атрибутов и методов без применения базового класса, как при обычной реализации. Удобным является и возможность получить все значения, которые содержит перечисление.

Реализация паттерна Состояние с помощью перечислений.
      public
      interface StateObserver 
{
  publicvoid nextState(State state);
}

publicenum State 
{
  STATEA() 
  {
    publicvoid someMethod() 
    {
      System.out.println("STATEA.someMethod()");
      nextState(State.STATEB);
    }
  },
  STATEB() 
  {
    publicvoid someMethod() 
    {
      System.out.println("STATEB.someMethod()");
      nextState(State.STATEC);
    }
  },
  STATEC() 
  {
    publicvoid someMethod() 
    {
      System.out.println("STATEC.someMethod()");
      nextState(State.STATEA);
    }
  };

  private Vector<StateObserver> observers = new Vector<StateObserver>();

  publicvoid addObserver(StateObserver observer) 
  {
    observers.add(observer);    
  }

  publicvoid removeObserver(StateObserver observer) 
  {
    observers.remove(observer);
  }

  publicvoid nextState(State state) 
  {
    for (StateObserver observer : observers) 
    {
      observer.nextState(state);
    }
  }
}

publicclass Widget 
{
  public Widget(State state) 
  {
    onNextState(state);
  }

  privatevoid onNextState(State state) 
  {
    this.state = state;
    this.state.addObserver(new StateObserver() 
    {
      publicvoid nextState(State state) 
      {
        onNextState(state);
      }
    });
  }

  private State state;

  publicvoid someMethod() 
  {
    state.someMethod();
  }
}

Пример использования:

Widget widget = new Widget(State.STATEA);
widget.someMethod();
// Получить все состояния
State[] states = State.values();
// Получить STATEA
State state = State.valueOf("STATEA"); 

Перечисления состояний можно использовать совместно обычными классами. Для этого необходимо, чтобы работа с состояниями в Widget происходила через общий интерфейс StateIF, реализующий и перечисление, и классы состояний.

Индуктивный пользовательский интерфейс

С помощью паттерна State можно реализовать индуктивный пользовательский интерфейс (IUI). Каждый экран приложения, или страница с элементами управления и информацией, будут являться состоянием формы, а форма, в свою очередь, будет являться объектом, поведение которого необходимо изменять во время выполнения. В качестве страницы в данной статье используется UserControl.


Рисунок 3. Реализация IUI с помощью паттерна State.

Разработка оконного приложения становится очень похожей на разработку Web-приложений, только в качестве браузера выступает форма, а в качестве страниц – UserControl. Благодаря этому можно создать довольно гибкий интерфейс.

В соответствии с паттерном State необходимо, чтобы каждая страница имела обратную связь с формой. Это нужно для того, чтобы известить форму о необходимости заменить текущую страницу на следующую.

Иногда бывает нужно предоставить странице возможность изменять форму. Для этого можно добавить соответствующие события или просто передать странице ссылку на форму.

Кнопки Next и Back можно реализовать, если добавить к странице ссылки на предыдущую и на следующую страницу и при нажатии Next перемещаться вперед по списку, а при нажатии Back – назад, соответственно. Другой вариант – список страниц, которые были открыты ранее, должен храниться в форме. Последний способ предпочтительнее, так как позволяет централизованно управлять страницами: перемещаться по ним, ограничивать количество страниц, которые могут быть запомнены.

При нажатии кнопки, находящейся на текущей странице, происходит событие Click, которое обрабатывается и, далее, вызывается событие NextPage, на которое в свою очередь подписана форма. Через аргумент передается страница, на которую форма заменит текущую страницу. Таким образом, происходит переход между экранами.

Ничего не мешает разместить страницу в странице. Для этого просто на один UserControl добавляется второй. Необходимо также подписаться на событие NextPage встраиваемой страницы и перенаправить его форме.

Используя List State, удобно генерировать сразу последовательности страниц, идущих друг за другом, и менять их очередность во время выполнения.

Разработанный таким образом интерфейс удобно использовать в приложениях для мобильных устройств, где место на экране ограничено.


Эта статья опубликована в журнале RSDN Magazine #4-2005. Информацию о журнале можно найти здесь
    Сообщений 9    Оценка 240        Оценить