IT Notes

Паттерн Состояние на C++

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

Допустим, мы разрабатываем класс управления веб-камерой. Камера может находиться в трех Состояниях:

  1. Не инициализирована. Назовем NotConnectedState;
  2. Инициализирована и готова к работе, но кадры еще не захватываются. Пусть это будет ReadyState;
  3. Активный режим захвата кадров. Обозначим ActiveState.

Поскольку мы работаем с паттером Состояние, то лучше всего начать с изображения Диаграммы состояний:

webcam-state-diagram

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

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

#include <iostream>

#define DECLARE_GET_INSTANCE( ClassName ) \
    static ClassName* getInstance() {\
        static ClassName instance;\
        return &instance;\
    }


class WebCamera {
public:
    typedef std::string Frame;

public:
    // **************************************************
    // Exceptions
    // **************************************************

    class NotSupported : public std::exception { };

public:
    // **************************************************
    // States
    // **************************************************

    class NotConnectedState;
    class ReadyState;
    class ActiveState;

    class State {
    public:
        virtual ~State() { }

        virtual void connect( WebCamera* ) {
            throw NotSupported();
        }

        virtual void disconnect( WebCamera* cam ) {
            std::cout << "Деинициализируем камеру…" << std::endl;
            // …

            cam->changeState( NotConnectedState::getInstance() );
        }

        virtual void start( WebCamera* ) {
            throw NotSupported();
        }

        virtual void stop( WebCamera* ) {
            throw NotSupported();
        }

        virtual Frame getFrame( WebCamera* ) {
            throw NotSupported();
        }

    protected:
        State() { }

    };

    // **************************************************
    class NotConnectedState : public State {
    public:
        DECLARE_GET_INSTANCE( NotConnectedState )

        void connect( WebCamera* cam ) {
            std::cout << "Инициализируем камеру…" << std::endl;
            // …

            cam->changeState( ReadyState::getInstance() );
        }

        void disconnect( WebCamera* ) {
            throw NotSupported();
        }

    private:
        NotConnectedState() { }
    };

    // **************************************************
    class ReadyState : public State {
    public:
        DECLARE_GET_INSTANCE( ReadyState )

        void start( WebCamera* cam ) {
            std::cout << "Запускаем видео-поток…" << std::endl;
            // …

            cam->changeState( ActiveState::getInstance() );
        }

    private:
        ReadyState() { }
    };

    // **************************************************
    class ActiveState : public State {
    public:
        DECLARE_GET_INSTANCE( ActiveState )

        void stop( WebCamera* cam ) {
            std::cout << "Останавливаем видео-поток…" << std::endl;
            // …

            cam->changeState( ReadyState::getInstance() );
        }

        Frame getFrame( WebCamera* ) {
            std::cout << "Получаем текущий кадр…" << std::endl;
            // …

            return "Current frame";
        }

    private:
        ActiveState() { }
    };

public:
    explicit WebCamera( int camID ) :
        m_camID( camID ),
        m_state( NotConnectedState::getInstance() ) {
    }

    ~WebCamera() {
        try {
            disconnect();
        } catch( const NotSupported& e ) {
            // Обрабатываем исключение
        } catch( … ) {
            // Обрабатываем исключение
        }
    }

    void connect() {
        m_state->connect( this );
    }

    void disconnect() {
        m_state->disconnect( this );
    }

    void start() {
        m_state->start( this );
    }

    void stop() {
        m_state->stop( this );
    }

    Frame getFrame() {
        return m_state->getFrame( this );
    }

private:
    void changeState( State* newState ) {
        m_state = newState;
    }

private:
    int m_camID;
    State* m_state;

};

Обращаю внимание на макрос DECLARE_GET_INSTANCE. Конечно, использование макросов в C++ не поощряется. Однако это относится к случаям, когда макрос выступает в роли аналога шаблонной функции. В этом случае всегда отдавайте предпочтение последним.

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

Классы-Состояния мы объявляем в главном классе - WebCamera. Для краткости я использовал inline-определения функций-членов всех классов. Однако в реальных приложениях лучше следовать рекомендациям о разделении объявления и реализации по h и cpp файлам.

Классы Состояний объявлены внутри WebCamera для того, чтобы они имели доступ к закрытым полям этого класса. Конечно, это создает крайне жесткую связь между всеми этими классами. Но Состояния оказываются настолько специфичными, что об их повторном использовании в других контекстах не может быть и речи.

Основу иерархии классов состояний образует абстрактный класс WebCamera::State:

class State {
public:
    virtual ~State() { }

    virtual void connect( WebCamera* ) {
        throw NotSupported();
    }

    virtual void disconnect( WebCamera* cam ) {
        std::cout << "Деинициализируем камеру…" << std::endl;
        // …

        cam->changeState( NotConnectedState::getInstance() );
    }

    virtual void start( WebCamera* ) {
        throw NotSupported();
    }

    virtual void stop( WebCamera* ) {
        throw NotSupported();
    }

    virtual Frame getFrame( WebCamera* ) {
        throw NotSupported();
    }

protected:
    State() { }

};

Все его функции-члены соответствуют функциям самого класса WebCamera. Происходит непосредственное делегирование:

class WebCamera {
// …

    void connect() {
        m_state->connect( this );
    }

    void disconnect() {
        m_state->disconnect( this );
    }

    void start() {
        m_state->start( this );
    }

    void stop() {
        m_state->stop( this );
    }

    Frame getFrame() {
        return m_state->getFrame( this );
    }

// …
    State* m_state;
}

Ключевой особенностью является то, что объект Состояния принимает указатель на вызывающий его экземпляр WebCamera. Это позволяет иметь всего три объекта Состояний для сколь угодно большого числа камер. Достигается такая возможность за счет использования паттерна Синглтон. Конечно, в рамках примера существенного выигрыша вы от этого не получите. Но знать такой прием все равно полезно.

Сам по себе класс WebCamera не делает практически ничего. Он полностью зависит от своих Состояний. А эти Состояния, в свою очередь, определяют условия выполнения операций и обеспечивают нужный контекст.

Большинство функций-членов WebCamera::State выбрасывают наше собственное исключение WebCamera::NotSupported. Это вполне уместное поведение по умолчанию. Например, если кто-то попытается инициализировать камеру, когда она уже инициализирована, то вполне закономерно получит исключение.

При этом для WebCamera::State::disconnect() мы предусматриваем реализацию по умолчанию. Такое поведение подойдет для двух состояний из трех. В результате мы предотвращаем дублирование кода.

Для смены состояния предназначена закрытая функция-член WebCamera::changeState():

void changeState( State* newState ) {
    m_state = newState;
}

Теперь к реализации конкретных Состояний. Для WebCamera::NotConnectedState достаточно переопределить операции connect() и disconnect():

class NotConnectedState : public State {
public:
    DECLARE_GET_INSTANCE( NotConnectedState )

    void connect( WebCamera* cam ) {
        std::cout << "Инициализируем камеру…" << std::endl;
        // …

        cam->changeState( ReadyState::getInstance() );
    }

    void disconnect( WebCamera* ) {
        throw NotSupported();
    }

private:
    NotConnectedState() { }
};

Для каждого Состояния можно создать единственный экземпляр. Это нам гарантирует объявление закрытого конструктора.

Другим важным элементом представленной реализации является то, что в новое Состояние мы переходим лишь в случае успеха. Например, если во время инициализации камеры произойдет сбой, то в Состояние ReadyState переходить рано. Главная мысль - полное соответствие фактического состояния камеры (в нашем случае) и объекта-Состояния.

Итак, камера готова к работе. Заведем соответствующий класс Состояния WebCamera::ReadyState:

class ReadyState : public State {
public:
    DECLARE_GET_INSTANCE( ReadyState )

    void start( WebCamera* cam ) {
        std::cout << "Запускаем видео-поток…" << std::endl;
        // …

        cam->changeState( ActiveState::getInstance() );
    }

private:
    ReadyState() { }
};

Из Состояния готовности мы можем попасть в активное Состояние захвата кадров. Для этого предусмотрена операция start(), которую мы и реализовали.

Наконец мы дошли до последнего логического Состояния работы камеры WebCamera::ActiveState:

class ActiveState : public State {
public:
    DECLARE_GET_INSTANCE( ActiveState )

    void stop( WebCamera* cam ) {
        std::cout << "Останавливаем видео-поток…" << std::endl;
        // …

        cam->changeState( ReadyState::getInstance() );
    }

    Frame getFrame( WebCamera* ) {
        std::cout << "Получаем текущий кадр…" << std::endl;
        // …

        return "Current frame";
    }

private:
    ActiveState() { }
};

В этом Состоянии можно прервать захват кадров с помощью stop(). В результате мы попадем обратно в Состояние WebCamera::ReadyState. Кроме того, мы можем получать кадры, которые накапливаются в буфере камеры. Для простоты под "кадром" мы понимаем обычную строку. В реальности это будет некоторый байтовый массив.

А теперь мы можем записать типичный пример работы с нашим классом WebCamera:

int main() {
    WebCamera cam( 0 );

    try {
        // cam в Состоянии NotConnectedState
        cam.connect();
        // cam в Состоянии ReadyState
        cam.start();
        // cam в Состоянии ActiveState
        std::cout << cam.getFrame() << std::endl;
        cam.stop(); // Можно было сразу вызвать disconnect()
        // cam в Состоянии ReadyState
        cam.disconnect();
        // cam в Состоянии NotConnectedState
    } catch( const WebCamera::NotSupported& e ) {
        // Обрабатываем исключение
    } catch( … ) {
        // Обрабатываем исключение
    }

    return 0;
}

Вот что в результате будет выведено на консоль:

Инициализируем камеру…
Запускаем видео-поток…
Получаем текущий кадр…
Current frame
Останавливаем видео-поток…
Деинициализируем камеру…

А теперь попробуем спровоцировать ошибку. Вызовем connect() два раза подряд:

int main() {
    WebCamera cam( 0 );

    try {
        // cam в Состоянии NotConnectedState
        cam.connect();
        // cam в Состоянии ReadyState
        // Но для этого Состояния операция connect() не предусмотрена!
        cam.connect(); // Выбрасывает исключение NotSupported
    } catch( const WebCamera::NotSupported& e ) {
        std::cout << "Произошло исключение!!!" << std::endl;
        // …
    } catch( … ) {
        // Обрабатываем исключение
    }

    return 0;
}

Вот что из этого получится:

Инициализируем камеру…
Произошло исключение!!!
Деинициализируем камеру…

Обратите внимание, что камера все же была деинициализирована. Вызов disconnect() произошел в деструкторе WebCamera. Т.е. внутреннее Состояние объекта осталось абсолютно корректным.

Выводы

С помощью паттерна Состояние вы можете однозначно преобразовать Диаграмму состояний в код. На первый взгляд реализация получилась многословной. Однако мы пришли к четкому делению по возможным контекстам работы с основным классом WebCamera. В результате при написании каждого отдельного Состояния мы смогли сконцентрироваться на узкой задаче. А это лучший способ написать ясный, понятный и надежный код.

Похожие публикации

Комментарии

Интересный сайт!

Спасибо :)

олично:)спасибо большое!

Интересно!

>#define DECLARE_GET_INSTANCE( ClassName )

Если память мне не изменяет, "по фэншую" такие трейты в C++ делаются так:

template <class ClassName> class Singleton

{

public:

static ClassName* getInstance()

{

static ClassName instance;

return &instance;

}

};

class NotConnectedState : public State, public Singleton<NotConnectedState>

{

//....

};

P.S. Здесь вообще какая-нибудь разметка в комментариях есть? Без нее код выглядит ужасно…

Anonymous:

>#define DECLARE_GET_INSTANCE( ClassName )

Если память мне не изменяет, "по фэншую" такие трейты в C++ делаются так …

Здравствуйте. Спасибо за комментарий. Да, очень неплохой вариант

Anonymous:

P.S. Здесь вообще какая-нибудь разметка в комментариях есть? Без нее код выглядит ужасно…

К сожалению, на данный момент такая возможность для комментариев не предусмотрена

Подскажите, если ли смысл реализовывать и сам класс управления (в данном случае WebCamera) как синглтон? Если нет, то почему?

Anonymous:

Подскажите, если ли смысл реализовывать и сам класс управления (в данном случае WebCamera) как синглтон? Если нет, то почему?

Здравствуйте. Я бы не рекомендовал для WebCamera использовать паттерн Сигнлтон из простых соображений: может быть подключено несколько камер. Синглтон предполагает, что объект один и больше подобных ему быть не может. Например, в виде Синглтона можно реализовать фабрику камер, т.е. объект, ответственный за перечисление доступных устройств, а также их первичную инициализацию и выдачу объектов WebCamera по запросу