Реализация паттерна состояний

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

  1. Конечный автомат может одновременно находиться в любом ОДНОМ состоянии.
  2. Переход из состояния X в состояние Y будет иметь другие параметры, чем переход из состояния Y в Z или из любого другого состояния в другое.
  3. Пользовательская программа, которая будет управлять «конечным автоматом», конечно, не может перейти в состояние, в которое нельзя переходить, если вы находитесь в определенном состоянии. например, для stateMachine.dispenseCard() не будет работать, если stateMachine.currentState() не CASHACCEPTED

Я пробовал перейти по ссылке по этой ссылке, но здесь:

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

    public abstract class DoorState : DomainObject    {
    protected Door _door;
    public Door Door
    {
        get { return _door; }
        set { _door = value; }
    }
    public abstract void Close();
    public abstract void Open();
    public abstract void Break();
    public abstract void Lock();
    public abstract void Unlock();
    /// <summary>
    /// Fix simulates a repair to the Door and resets 
    /// the initial state of the door to closed.
    /// </summary>
    public void Fix()
    {
        _door.DoorState = new DoorClosedState(this);
    }}
    
  2. Почему у класса State "есть" Device, которое переходит в разные состояния? Разве не должно быть иначе? Как будто Дверь должна «иметь» состояние.


person shabby    schedule 15.05.2019    source источник


Ответы (1)


В приведенном вами примере кода фактически определяется Состояние, которое имеет все поведения или < em> Контекст (в данном примере дверь). Состояние определяет, как Контекст должен вести себя в этом состоянии.

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

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

Вот пример конечного автомата для торгового автомата. Чтобы упростить пример и сконцентрироваться на конечном автомате и переходах, предположим, что в нашем конечном автомате есть только лапша, и он не возвращает лишние деньги. Итак, если чашка лапши стоит 5 долларов, а вы дадите ей 7 долларов, она не вернет 2 доллара.

ПРИМЕЧАНИЕ. Поскольку требуется связь между NoodleVendingMachine и каждым состоянием. Для простоты я сделаю эти методы внутренними только для того, чтобы пометить их. Для реального проекта может потребоваться дополнительный интерфейс, чтобы скрыть их от клиентского кода NoodleVendingMachine и держать их только между NoodleVendingMachine и его состояниями.

public class CacheStorage {

    public Cache AvailableCache { get; private set; }

    public void AddCache(Money cache) {
        AvailabletCache += cache;
    }

    public void ClearAvailableCache() {
        AvailabletCache = Money.None;
    }
}

public interface INoodleVendingMachineState {

    void TakeCache(Money money);

    Noodles DispenceNoodles();

    Money ReturnCache();
}

public class NoodleVendingMachine {

    private INoodleVendingMachineState mState;

    itnernal CacheStorage CacheStorage { get; private set; }

    public NoodlesPrice { get; private set; }

    public Money AvailableCache { get { return CacheStorage.AvailableCache; } }

    public NoodleVendingMachine() {

        NoodlesPrice = new Money(Currency.USD, 5); // 5 bucks for noodles
        CacheStorage = new CacheStorage();
        mState = new WaitingForCacheState(this);
    }

    public void TakeCache(Money money) {
        mState.TakeCache(money);
    }

    public Noodles DispenceNoodles() {
        return mState.DispenceNoodles();
    }

    public Money ReturnCache() {
        return mState.ReturnCache();
    }

    internal void TransitionTo(INoodleVendingMachineState state) {
        mState = state;
    }
}

public WaitingForCacheState : INoodleVendingMachineState {

    private NoodlesVendingMachine mVendingMachine;

    public WaitingForCacheState(NoodlesVendingMachine vendingMachine) {
        mVendingMachine = vendingMachine;
    }

    public void TakeCache(Money cache) { 

        mVendingMachine.CacheStorage.AddCache(cache);
        mVendingMachine.TransitionTo(new CacheAvailableState(mVendingMachine));
    }

    public Noodles DispenceNoodles() { 
        throw new InsertCacheFirstException();
    }

    public Money ReturnCache() {
        throw new CacheNotAvailableException();
    }
}

public CacheAvailableState : INoodleVendingMachineState {

    private CacheStorage mCacheStorage;
    private NoodleVendingMachine mVendingMachine;

    public CacheAvailableState(NoodleVendingMachine vendingMachine) {

        if (vendingMachine.AvailableCache == Money.None){
            throw new CacheNotAvailable()
        }

        mVendingMachine = vendingMachine;
        mCacheStorage = mVendingMachine.CacheStorage;
    }

    public void TakeCache(Money cache) {
         mCacheStorage.AddCache(cache);
    }

    public Noodles DispenceNoodles() {

        if(mCacheStorage.AvailableCache < mVendingMachine.NoodlesPrice) {
            throw new CacheNotEnoughtException();
        }

        mCacheStorage.ClearAvailableCache();

        mVendingMachine.TransitionTo(new WaitingForCacheState(mVendingMachine));

        return new Noodles();
    }

    public Money ReturnCache() {
        var cache = mCacheStorage.AvailableCache;
        mCacheStorage.ClearAvailableCache();
        mVendingMachine.TransitionTo(new WaitingForCacheState(mVendingMachine));
        return cache;
    }
}

Здесь мы фиксируем поведение торгового автомата с состояниями.

WaitingForCacheState вызовет исключения при вызове DispenceNoodles или ReturnCache, поскольку это недопустимое поведение в данном состоянии.

WaitingForCacheState выполнит переход состояния на CacheAvailableState, когда пользователь вводит данные в кеш. Когда кеш доступен, мы видим, что все поведения поддерживаются. Когда раздаётся лапша или пользователь просит вернуть свои деньги, мы переходим к состоянию WaitingForCacheState.

В этом примере каждое состояние переходит в следующее соответствующее состояние.

Если у вас есть более сложный пример для вашего конечного автомата, вам, вероятно, потребуется решить, где хранить параметры. Вы можете сохранить его в Context (в нашем случае NoodlesVendingMachine). В этом примере деньги хранятся в специальном CacheStorage, так что каждое состояние и NoodlesVendingMachineимеет доступ к ним, они могут принимать решения на основе их стоимости. когда выполняется действие (например, DispenceNoodles), текущее состояние проверяет значение CacheStorage и принимает решение, следует ли выполнить переход, выполнить какое-либо поведение (TakeMoney в CacheAvailableState), выдать ошибку или выполнить поведение, а затем выполнить переход ( ReturnCache в CacheAvailableState).

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

В этом примере CacheAvailableState может сохранить AvailableCache в качестве свойства. Я решил добавить его в другой класс, чтобы показать, что таким образом несколько объектов могут иметь доступ к данным. Конечно, нам нужно показать AvailableCache пользователю, поэтому NoodlesVendingMachine также нужен доступ к доступному кешу.

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

person expandable    schedule 15.05.2019
comment
вы сделали это таким подробным и сложным, что не можете понять - person shabby; 20.05.2019
comment
Государственный паттерн - непростой. Поэтому я привел подробный пример. Попробую подвести итог. Абстрактный класс State определяет все возможные варианты поведения (не состояния) контекста (Door в вашей статье, VendingMachine в моем примере). Каждый объект State представляет, как его Context (Door, VendingMachine, StateMachine и т. Д.) Должен вести себя в этом состоянии. Им может потребоваться совместное использование данных (CacheStorage в моем примере, поскольку всем им нужен доступ к деньгам, которые клиент предоставил и которые используются для переходов между состояниями, параметры в вашем примере, которые необходимы для переходов между состояниями). - person expandable; 20.05.2019