State Pattern

  • Object change their behavior depending on the status they are currently in

  • It does not matter, how the object got into a particular state, only the state itself

Example: Turnstile at subway station which requires card swipe to open

        stateDiagram
    Closed --> Process : pay
    Closed --> Closed : pay_failed
    Closed --> Closed : pay_ok (Timeout)
    Closed --> Closed : enter
    Process --> Open : pay_ok
    Process --> Closed : pay_failed
    Process --> Process : enter
    Open --> Closed : enter
    Open --> Open : pay_ok
    Open --> Open : pay_failed
    

Turnstile (e.g. subway example)

Transitioning table (current state + action -> new state)

States

Actions

enter

pay_ok

pay_failed

pay

closed

closed

closed

closed

process

open

closed

open

open

open

process

process

open

closed

process

  • An object must have a state variable, which contains a state object (conditionals are replaced by polymorphism)

  • The state object will implements all action methods

Issue

  • Each action method has to be implemented for each current state

  • Having more states and actions increases number of implementation significantly (each is a combination of a state and an action, so implementations = amount of states x amount of actions)

  • Keeping track of state-action combinations hard for complex scenarios

  • Each action method had to check the current status (is_closed(), is_processing(), …)

  • Solving it using a lot of if-statements or switch-cases is possible, but probably much harder as using the State Pattern

Solution

Definition

The State Pattern allows an object to alter its behavior when its internal state changes. The object will appear to change its class.

As seen from the diagram above, if the state of the object changes, the implementation of its action methods may change.

        classDiagram
    State "1..*" <-- Context : HAS-A
    State <|-- ConcreteStateA
    State <|-- ConcreteStateB
    class Context {
        +request()
    }
    class State {
        <<interface>>
        +handle()
    }
    class ConcreteStateA {
        +handle()
    }
    class ConcreteStateB {
        +handle()
    }
    

General class diagram

  • When calling request() method from the Context class, it will delegate to state.handle() to handle the State.

  • THe Context class does not know, which state it currently has, but calls the handle() method of the State class (actually, it’s implementation in the currently active ConcreteState class) to handle the state

        classDiagram
    GateState "1..*" <-- Gate : HAS-A
    GateState <|-- OpenGateState
    GateState <|-- ClosedGateState
    GateState <|-- ProcessingPaymentGateState
    Gate <-- OpenGateState : HAS-A
    Gate <-- ClosedGateState : HAS-A
    Gate <-- ProcessingPaymentGateState : HAS-A
    class Gate {
        +enter()
        +pay()
        +pay_OK()
        +pay_failed()
        +transition_to(state: State)
    }
    class GateState {
        <<interface>>
        +enter()
        +pay()
        +pay_OK()
        +pay_failed()
    }
    class OpenGateState {
        +enter()
        +pay()
        +pay_OK()
        +pay_failed()
    }
    class ClosedGateState {
        +enter()
        +pay()
        +pay_OK()
        +pay_failed()
    }
    class ProcessingPaymentGateState {
        +enter()
        +pay()
        +pay_OK()
        +pay_failed()
    }
    

Applied to upper example

  • the methods of Gate blindly delegate to the GateState, which the logic about the gate’s state (here, the Gate class doesn’t do anything but calling the respective method on the GateState implementing class, though it also could do something before or after calling it)

  • each concrete State class implements all Context methods to have all combinations from the transitioning table and implements the behavior expected from that state

  • there are various ways of changing the state of the Gate class:

    • The simplest way is to give then ConcreteState classes a reference to the Gate class in order to call a Gate’s transition_to(state: State) method which changes the Gate classes state. This is the approach used in the Gate example

      Warning

      This approach violates the Open-Closed Principle since the original Gate class is mutated

    • To avoid mutation make the ConcreteState classes changing-state methods return a ConcreteState object, which the Gate class will use the return a new Gate class to the Client (original caller) with the proper state set

from abc import ABC, abstractmethod


class GateState(ABC):

    @abstractmethod
    def enter(self):
        pass

    @abstractmethod
    def pay(self):
        pass

    @abstractmethod
    def pay_ok(self):
        pass

    @abstractmethod
    def pay_failed(self):
        pass


class Gate:

    def __init__(self):
        # initial state cannot be dependency injected, since the State requires
        # an instance of the Gate upon instantiation, therefore no arguments
        # for the __init__() method, but state is set within the method
        self._state = ClosedGateState(self)

    def enter(self):
        self._state.enter()

    def pay(self):
        self._state.pay()

    def pay_ok(self):
        self._state.pay_ok()

    def pay_failed(self):
        self._state.pay_failed()

    def change_state(self, new_state: GateState):
        self._state = new_state


class OpenGateState(GateState):

    def __init__(self, gate: Gate):
        self._gate = gate

    def enter(self):
        print("User entering. Closing gate ... ")
        self._gate.change_state(ClosedGateState(self._gate))

    # here: return None.
    # in case the Gate is supposed to be handled as closed for mutation
    # a new instance of OpenGateState must be returned
    def pay(self):
        print("Ignoring unexpected card swipe. Keeping gate open ...")

    def pay_ok(self):
        print("Gate already open. Ignore Payment OK ...")

    def pay_failed(self):
        print("Payment failed. Keep gates open ... ")


class ClosedGateState(GateState):

    def __init__(self, gate: Gate):
        self._gate = gate

    def enter(self):
        print("Cannot enter before payment. Keeping gate closed ...")

    def pay(self):
        print("Received card swipe. Process payment ...")
        self._gate.change_state(ProcessingPaymentGateState(self._gate))

    def pay_ok(self):
        print("Invalid payment OK received (probably error). Keeping gate "
              "closed ...")

    def pay_failed(self):
        print("Invalid payment failed received (probably error). Keeping"
              "gate closed ...")


class ProcessingPaymentGateState(GateState):

    def __init__(self, gate: Gate):
        self._gate = gate

    def enter(self):
        print("Cannot enter while processing ...")

    def pay(self):
        print("Already processing previous payment. Please wait ...")

    def pay_ok(self):
        print("Payment OK. Opening gates ...")
        self._gate.change_state(OpenGateState(self._gate))

    def pay_failed(self):
        print("Payment failed. Gates remain closed ...")
        self._gate.change_state(ClosedGateState(self._gate))


if __name__ == "__main__":

    gate = Gate()

    # try to enter without paying
    gate.enter()

    # enter after successful payment
    gate.pay()
    gate.pay_ok()
    gate.enter()

    # forbid enter due to failed payment
    gate.pay()
    gate.pay_failed()
    gate.enter()