Шаблон проектирования состояний C ++ с несколькими конечными автоматами - PullRequest
2 голосов
/ 29 июня 2010

У меня есть машина состояния C ++, реализованная с использованием шаблона проектирования State. Каждое состояние реализовано как вложенный класс друга контекстного класса.

class Context {
public:
  /* The Context class' user calls process1() to get it to perform an action */
  void process1();

private:
  class IState;

  void switchState( IState *newState );

  class IState {
    virtual void doProcess( Context &context ) = 0;
  };

  class StateA : public Context::IState {
    void doProcess( Context &context );
  };
  friend class StateA;

  class StateB : public Context::IState {
    void doProcess( Context &context );
  };
  friend class StateB;
  .
  .
  .
  class StateJ : public Context::IState {
    void doProcess( Context &context );
  };
  friend class StateJ;
};

В настоящее время успешная итерация конечного автомата выполняется от Context::StateA до Context::StateJ при вызове Context::process1(), но некоторые из состояний содержат внутреннюю логику, чтобы определить, следует ли вернуться к более раннему состоянию. Таким образом, типичное исполнение будет выглядеть так:

StateA
StateB
StateC
StateD
StateE
StateC
StateD
StateE
StateF
StateG
StateH
StateI
StateJ

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

Шаблон проектирования Visitor может помочь, но я не уверен, предназначен ли он для реализации конечных автоматов. При использовании Visitor process1() может содержать всю логику для порядка выполнения состояний, а затем просто вызывать каждое состояние в этом порядке. Точно так же process2() будет обрабатывать всю свою собственную логику.

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

Второй конечный автомат пройдет через следующие переходы состояний:

StateA
StateB
StateC
StateJ

Поэтому я пытаюсь удалить дублирующийся код.

Ответы [ 4 ]

3 голосов
/ 29 июня 2010

Вы можете использовать один или несколько методов для уменьшения дублирования. Например:

  1. Если у вас есть какая-то сложная обработка внутри каждого метода обработчика событий состояния, переместите эту обработку в Context.

    Или, что еще лучше, создайте третий класс X (я не знаю, как его назвать. Может быть, драйвер? Есть предложения? Или может быть этот класс должен называться Context, а Context должен называться SomeStateMachine?), Который будет содержать все распространенные данные и методы, которые его изменяют.

    Тогда в методе doProcess каждого состояния вы будете только:

    • Проверка ввода

    • Вызовите правильный метод X

    • Изменение состояния, возможно, на основе возвращаемого значения из предыдущего шага.

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

  2. Вместо переключения состояний, как это:

    context.switchState(new StateB());
    

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

    context.switchState(&Context::StateBInstance);
    

    Теперь еще лучше. Вы можете создавать поля в Context, содержащие экземпляры IState. Какой экземпляр будет решаться при государственном машиностроении. Это позволит вам подключить ваш конечный автомат во время выполнения.

    void Context::Context()
    {
      theStateThatComesAfterA = &StateBInstance;
      // [...]
    }
    
    void AnotherContext::AnotherContext()
    {
      theStateThatComesAfterA = &StateCInstance;
      // [...]
    }
    
    void StateA::doProcess(Context& context)
    {
      context.DoSomething();
      // [...]
      context.switchState(context.theStateThatComesAfterA);
    }
    

Последнее замечание. Не увлекайся этим обобщением. Помните. Иногда сходства внутри кода указывают на повторение, которое следует устранить. Но довольно часто мы смотрим слишком усердно и обобщаем на основе чисто случайного сходства двух понятий. Затем, когда в процессе эволюции концепции эти концепции начинают расходиться, очень трудно изменить код из-за сильной связи несвязанных вещей.

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

3 голосов
/ 29 июня 2010

Я предполагаю, что в вашем коде switchState вызывается отдельными состояниями, когда пришло время перейти к чему-то другому.Примерно так:

void StateA::doProcess(Context& context) {
   context.switchState(new StateB()); // NOTE: potential leak!
}

Так ли это?

Если это так, вы можете рассмотреть одну вещь: возвращать состояния объекты перехода , которые абстрактно представляют элемент управленияуказать на вашем графике состояния.Затем вы заставляете контекст запускать цикл, который выполняет состояния, извлекает результирующие переходы и отображает переходы в соответствующие следующие состояния для любого процесса, который у вас есть.Карта перехода может быть настроена по-разному для каждого имеющегося у вас метода process.

Плюсы:

  • Состояния не должны знать друг о друге.Переход - это их декларация миру о том, что они сделали.Это зависит от контекста, чтобы соответствующим образом прокладывать маршрут между государствами.Это может сделать состояния многократно используемыми в разных объектах контекста, например.
  • Позволяет идею "той же структуры, немного отличающееся поведение", позволяя контексту по существу поддерживать тот же граф состояний, но подключать немного разныесостояние с совместимым интерфейсом перехода в качестве цели конкретного перехода.

Минусы:

  • Добавляет дополнительный слой материала для настройки.Код отображения перехода в основном стандартный.Инструменты для генерации шаблона могут помочь.
  • Не поможет вам, если вы действительно хотите сделать что-то радикально отличающееся в двух процессах, которые состояния не имеют правильных переходов для поддержки - но тогда вы 'Вы пытаетесь использовать повторно на неправильном уровне - вам, вероятно, нужны другие объекты состояния в этой точке.

EDIT : образец кода имеет значение http://pastebin.com/eBauP060.

1 голос
/ 29 июня 2010

Престижность за попытку сделать что-то сложное, сделать это правильно и сделать это СУХИМ!

Как указал tomekszpakowicz, если у вас разные переходы между состояниями, то у вас разные конечные автоматы.Каждый «процесс», на который вы ссылаетесь, звучит для меня как отдельный конечный автомат.

Если вы укажете переходы внутри состояний, вам понадобятся отдельные состояния для каждой машины.Если вы хотите повторно использовать код в состояниях, а затем извлекать общие функциональные возможности из состояний и в представления состояний, которые можно использовать повторно.

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

0 голосов
/ 29 июня 2010

Этот фрагмент не компилируется.

private:
    class IState;
    void switchState( IState *newState );

    class IState {
        virtual void doProcess( Context &context ) = 0;
    };

Кстати, вы можете положиться на специализацию шаблонов для реализации doProcess () различных состояний, например:

class Context
{
public:
    class IState;
    void switchState( IState *newState );

    class IState {
        virtual void doProcess( Context &context ) = 0;
    };

    template<typename T>
    class StateA : public Context::IState {
    public:
        void doProcess( Context &context );
    };

    template<typename T>
    class StateB : public Context::IState {
    public:
        void doProcess( Context &context );
    };

    template<typename T>
    class StateJ : public Context::IState {
    public:    
        void doProcess( Context &context );
    };

public:
    /* The Context class' user calls process1() to get it to perform an action */
    template<typename T>
    void process()
    {
        StateA<T> a;
        a.doProcess(*this);
    }
};

struct Process1
{
};

struct Process2
{
};

void main(int, char **)
{
    Context c;
    c.process<Process1>();
    c.process<Process2>();
}

Кроме того, вы получаете универсальный конечный автомат при условии, что сохраняете одинаковое количество состояний.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...