Само-собой напрашивается паттерн State. Взвесив все за и против, решил хранить данные, связанные с конкретным состоянием, в классе Device, а класс State сделать синглтоном. Задача усложняется тем, что при попадании в некоторое состояние и при выходе из него нужно выполнять определенные действия, поэтому в State добавляются методы entry() и exit().
class Device {
public:
void poll() { state_->poll(*this); }
void process_command(const Command *command) { state_->process_command(*this, command); }
private:
class State {
virtual void entry(Device &device);
virtual void exit(Device &device);
virtual void poll(Device &device);
virtual void process_command(Device &device, const Command *command);
};
// Состояния устройства.class Ready;
class Steady;
class Go;
const State *state_;
// Сменить состояние.void change_state(State *state)
{
state_->exit(*this);
state_ = state;
state_->entry(*this);
}
};
Внимание, вопрос: какие минусы в такой "конструкции"?
Лично мне не нравится, что для кода, выполняющего при входе (выходе) из состояния, используются отдельные методы, а не пара конструктор-деструктор.
Здравствуйте, syomin, Вы писали:
S>Добрый день!
S>Столкнулся со сложностями в реализации паттерна State. GoF читал, но просветления не пришло, поэтому решил обратиться к местным гуру за помощью.
S>Есть класс Device, поведение которого сильно зависит от его состояния. Интерфейс у класса следующий: S>...
А какие события вызывают переключение состояния. Может лучше создать несколько методов SetState_statename_() и использовать new и delete для объектов состояний, тогда можно логику enter и exit вынести в конструктор и деструктор.
G>А какие события вызывают переключение состояния. Может лучше создать несколько методов SetState_statename_() и использовать new и delete для объектов состояний, тогда можно логику enter и exit вынести в конструктор и деструктор.
Забыл уточнить. Переходы между состояниями происходят в следующих случаях:
если при выполнении poll() или process_command() произошла ошибка (отказ оборудования) -> переход в состояние неисправность.
при получении команды выполняется переход в другое состояние, зависящее от вида команды и текущего состояния.
по истечении какого-то времени, т.е. ранее была дана команда устройству на выполнение какого-то действия, по окончанию которого происходит переход в другое состояние.
ИМХО, нормальная реализация.
У самого как-то давно уже была подобная проблема и решил ее именно так, как и ты.
А то, что entry/exit реализуются явными вызовами извне, а не в ctro/dtor, так в этом есть свои плюсы.
Например, нет необходимости убивать объекты-состояния при выходе из этого состояния и создавать новый объект-состояние при входе в него.
В некоторых случаях это может быть полезно с т.з. производительности.
Плюс это может быть более надежным в плане обработки исключительных ситуаций (вдруг новый объект-состояние не сможет быть создан динамически при смене состояния (E_OUTOFMEMORY)).
Ну и еще такой момент, как возврат в состояние. Иногда бывает необходимо, чтобы при возврате в определенное состояние это самое состояние не начинало свою работу "с нуля", а продолжало с того места, где его прервали. Т.е. само состояние должно обладать памятью:
class CWorker
{
Dig();
Build();
Think();
class CConcreteWorker
{
virtual DoWork() = 0;
};
class CDigger : CConcreteWorker;
class CBuilder : CConcreteWorker;
class CThinker : CConcreteWorker;
DoWork()
{
m_worker->DoWork();
}
CConcreteWorker *m_worker;
}
main()
{
CWokrer w;
w->Dig();
// some code
w->Think();
// some code
w->Dig(); // Копать заново или с того места где остановились в прошлый раз? :)
}
Более логично чтобы каждый State сам определял свое следующее состояние. Тогда логика перехода будет реализована в пределах конкретного StateConcrete, соответствующуя этому StateConcrete и не будет засорять общий код. Почти то же с небольшими изменениями:
S>Само-собой напрашивается паттерн State. Взвесив все за и против, решил хранить данные, связанные с конкретным состоянием, в классе Device, а класс State сделать синглтоном.
Что значит синглтоном? а если девайсов будет два? синглтон тут не применим.
Получается два класса Device и Statе, объекты которых всегда в соотношении 1 к 1. Кандидаты на рефакторинг путем объединения зачем их два делать-то? незачем.
S>Задача усложняется тем, что при попадании в некоторое состояние и при выходе из него нужно выполнять определенные действия, поэтому в State добавляются методы entry() и exit
Все не так. У стейт-машины действия связаны с ребром графа, соединяющем два состояния, там нет понятия "вход" и "выход" из состояния. При этом ничто не мешает связать те же два состояния несколькими разными ребрами, какое из них исполнится — зависит не от состояния, а от типа внешнего воздействия.
Потому при кодировании стейт-машин делают по методу на каждый тип внешнего воздействия, это, помимо всего прочего, изолирует внешний мир от деталей реализации стейт-машины.
Каждый из этих методов есть такой большой switch по текущему состоянию (из которого выходим). Совокупность (ТипВоздействия, ТекущееСостояние) однозначно задает ребро графа, которое будет исполняться, а значит — и действия, и новое состояние. Все это просто прописывается в ветке свитча, если надо — то выносится в отдельную функцию.
В общем, самая лучшая архитектура — это хорошо продуманное старое, еще на Си реализуемое.
G>А какие события вызывают переключение состояния. Может лучше создать несколько методов SetState_statename_()
Нет, не лучше. Дело в том, что у стейт-машины выполняемые действия связаны с ребром, а не с финальным состоянием. Если в состояние ведут 2 разных ребра — то связанные с ними действия могут быть абсолютно разными, а потому метод SetState бессмысленен, и заменяется одним присваиванием в m_State.
Что действительно может иметь смысл, так это полиморфизм по текущему (а не таржет-) состоянию. Дело в том, что исполняемое ребро однозначно выбирается по комбинации (ТекущееСостояние, ВидВнешнегоВоздействия). Таким образом, если мы делаем свою таблицу указателей на функции для каждого текущего состояния, функции в которой отличаются в зависимости от типа внешних воздействий — то задача выбора ребра исполняется не свитчом, а вшитым в язык полиморфизмом. Собственно, значение m_OpTable может заодно использоваться просто для хранения текущего состояния, и переход в новое состояние будет чем-то типа m_OpTable = &GlobalOpTableForState1;
На Си делается элементарно, на классических ОО языках — чуть сложнее там для реализации этих таблиц функций придется, скорее всего, использовать синглетоны без данных (только код), и передавать в эти синглетонные функции свой объект state machine как параметр.
G>и использовать new и delete для объектов состояний
Просто прелесть какая-то! теперь не просто объект State, а объект, инстанс которого предполагается разрушать и создавать новый при каждом прохождении ребра. Во бред-то! чем enum State хуже? тем, что он избавляет от избыточных new и delete?
_>Ну и еще такой момент, как возврат в состояние. Иногда бывает необходимо, чтобы при возврате в определенное состояние это самое состояние не начинало свою работу "с нуля", а продолжало с того места, где его прервали. Т.е. само состояние должно обладать памятью:
Количество "мест возврата" конечно. Тогда все они объявляются состояниями, стейт-машина растет. Это гибко и разумно (никакой "памяти"), хотя кодировать туповато.
Совсем другое дело, если нужна многоуровневая память — помнить то, что было до, что было "до до", и так далее. Тогда вообще стейт-машина неприменима, а нужна стековая машина.
Здравствуйте, Maxim S. Shatskih, Вы писали:
MSS>Количество "мест возврата" конечно. Тогда все они объявляются состояниями, стейт-машина растет. Это гибко и разумно (никакой "памяти"), хотя кодировать туповато.
Не совсем понял что имеется ввиду.
Если вернуться к моему примеру выше, то CDigger, CBuilder & CThinker это и есть конретные состояния, одно из которых является активным в какой-то момент времени.
А под "обладать памятью" я имел ввиду, что каждое конкретное состояние может, ну скажем вести статистику своей работы и сохранять ее в промежутках между переходами из одного состояния в другое:
class CDigger : CConcreteWorker
{
entry()
{
m_diggedHoles = 0; // Или не делать этого в случае с "продолжением"
}
exit()
{
LogMessage("%d holes digged", m_diggedHoles);
}
DoWork()
{
m_diggedHoles++;
}
private:
int m_diggedHoles;
}
Добрый день!
MSS>Что значит синглтоном? а если девайсов будет два? синглтон тут не применим.
Может, я неправильно выразился, классы, соответствующие конкретным состояниям сделаны синглтонами, т.к. никаких данных в них не храниться. В общем, ничего нового — в точности по книжке GoF.
Здравствуйте, Maxim S. Shatskih, Вы писали:
S>>Само-собой напрашивается паттерн State. Взвесив все за и против, решил хранить данные, связанные с конкретным состоянием, в классе Device, а класс State сделать синглтоном.
MSS>Что значит синглтоном? а если девайсов будет два? синглтон тут не применим.
Ну и что, пусть будет 10 device, если функции используют локальные переменные, какие проблемы от того что девайсы будут держать указатель на один и тот же стейт ?
S>Ну и что, пусть будет 10 device, если функции используют локальные переменные, какие проблемы от того что девайсы будут держать указатель на один и тот же стейт ?
То, что девайсы могут находиться в разных состояниях — не смущает?
Здравствуйте, Maxim S. Shatskih, Вы писали:
MSS>То, что девайсы могут находиться в разных состояниях — не смущает?
Ну и будет держать каждый указатель на свой state.
Прочитал еще раз ветку, ссори, говорим об одном и том же, вы уже писали здесь