6.2. State

  • EN: State

  • PL: Stan

  • Type: object

The State design pattern is a behavioral design pattern that allows an object to change its behavior when its internal state changes. This pattern is close to the concept of finite-state machines. The main idea is to declare each possible state of an object as a separate class and extract all state-specific behaviors into these classes.

In Python, we can implement the State pattern using classes and polymorphism. Here's a simple example:

First, we define an abstract base class State that represents the general interface for all states:

>>> from abc import ABC, abstractmethod
>>>
>>> class State(ABC):
...     @abstractmethod
...     def handle(self, context):
...         pass

Then, we define concrete state classes that implement the handle method differently:

>>> class ConcreteStateA(State):
...     def handle(self, context):
...         print("Handling in State A")
...         context.state = ConcreteStateB()
>>>
>>> class ConcreteStateB(State):
...     def handle(self, context):
...         print("Handling in State B")
...         context.state = ConcreteStateA()

Next, we define a Context class that maintains an instance of a State subclass and delegates the request to the current state:

>>> class Context:
...     def __init__(self, state: State):
...         self._state = state
...
...     @property
...     def state(self):
...         return self._state
...
...     @state.setter
...     def state(self, state: State):
...         self._state = state
...
...     def request(self):
...         self._state.handle(self)

Finally, we can use the Context and State classes like this:

>>> context = Context(ConcreteStateA())
>>> context.request()
Handling in State A
>>> context.request()
Handling in State B
>>> context.request()
Handling in State A

In this example, ConcreteStateA` and ``ConcreteStateB are interchangeable states that the Context` class can use. The ``Context class doesn't need to know the details of how the states perform their operations. It just calls the handle method on its current state object.

The State design pattern has several advantages:

  1. Flexibility: It provides a way to define a family of behaviors within an object and change the behavior at runtime.

  2. Encapsulation: Each state is encapsulated in its own class, which makes the code more maintainable and easier to understand.

  3. Simplicity: It simplifies the code of the object that changes its behavior. Instead of having many conditional statements to determine what behavior to execute, the object can simply delegate this to the state object.

  4. Open/Closed Principle: The State pattern adheres to the Open/Closed Principle as it allows introducing new states without changing the existing state classes or the context.

  5. Localizing State-Specific Behavior: Each state class has all the behavior related to a particular state. This makes it easier to understand and modify the behavior associated with a particular state.

  6. State Transitions: State transitions can be explicit and easy to understand because they are triggered by specific methods in the state classes.

  7. Reducing State Transition Complexity: The pattern can also reduce the complexity of state transitions because the decision logic of the transition can be encapsulated within the state classes themselves.

6.2.1. Pattern

  • Changes based on class

  • Open/Close principle

  • Using polymorphism

../../_images/designpatterns-state-pattern.png

6.2.2. Problem

  • Canvas object can behave differently depending on selected Tool

  • All behaviors are represented by subclass of the tool interface

design-patterns/behavioral/img/designpatterns-state-problem.png

from enum import Enum


class Tool(Enum):
    SELECTION = 1
    PENCIL = 2
    ERASE = 3
    BRUSH = 4


class Window:
    current_tool: Tool

    def on_left_mouse_button(self):
        if self.current_tool == Tool.SELECTION:
            print('Select')
        elif self.current_tool == Tool.PENCIL:
            print('Draw')
        elif self.current_tool == Tool.ERASE:
            print('Erase')
        elif self.current_tool == Tool.BRUSH:
            print('Paint')

    def on_right_mouse_button(self):
        if self.current_tool == Tool.SELECTION:
            print('Unselect')
        elif self.current_tool == Tool.PENCIL:
            print('Stop drawing')
        elif self.current_tool == Tool.ERASE:
            print('Undo erase')
        elif self.current_tool == Tool.BRUSH:
            print('Stop painting')



if __name__ == '__main__':
    window = Window()

    window.current_tool = Tool.BRUSH
    window.on_left_mouse_button()
    window.on_right_mouse_button()

    window.current_tool = Tool.SELECTION
    window.on_left_mouse_button()
    window.on_right_mouse_button()

    window.current_tool = Tool.ERASE
    window.on_left_mouse_button()
    window.on_right_mouse_button()

# Paint
# Stop painting
# Select
# Unselect
# Erase
# Undo erase

6.2.3. Solution

../../_images/designpatterns-state-solution.png

from abc import ABC, abstractmethod


#%% Abstracts

class WindowEvents(ABC):
    @abstractmethod
    def on_left_mouse_button(self): ...

    @abstractmethod
    def on_right_mouse_button(self): ...


class Tool(WindowEvents, ABC):
    pass


#%% Tools

class SelectionTool(Tool):
    def on_left_mouse_button(self):
        print('Select')

    def on_right_mouse_button(self):
        print('Unselect')


class EraseTool(Tool):
    def on_left_mouse_button(self):
        print('Erase')

    def on_right_mouse_button(self):
        print('Undo erase')

class PencilTool(Tool):
    def on_left_mouse_button(self):
        print('Draw')

    def on_right_mouse_button(self):
        print('Stop drawing')

class BrushTool(Tool):
    def on_left_mouse_button(self):
        print('Paint')

    def on_right_mouse_button(self):
        print('Stop painting')


#%% Main

class Window(WindowEvents):
    current_tool: Tool

    def on_left_mouse_button(self):
        self.current_tool.on_left_mouse_button()

    def on_right_mouse_button(self):
        self.current_tool.on_right_mouse_button()


if __name__ == '__main__':
    window = Window()

    window.current_tool = BrushTool()
    window.on_left_mouse_button()
    window.on_right_mouse_button()

    window.current_tool = SelectionTool()
    window.on_left_mouse_button()
    window.on_right_mouse_button()

    window.current_tool = EraseTool()
    window.on_left_mouse_button()
    window.on_right_mouse_button()

# Paint
# Stop painting
# Select
# Unselect
# Erase
# Undo erase

6.2.4. Use Case - 0x01

../../_images/designpatterns-state-usecase-01.jpg

Figure 6.4. GIMP (GNU Image Manipulation Project) window with tools and canvas [1]

>>> class Tool:
...     def on_mouse_over(self): raise NotImplementedError
...     def on_mouse_out(self): raise NotImplementedError
...     def on_mouse_click_leftbutton(self): raise NotImplementedError
...     def on_mouse_unclick_leftbutton(self): raise NotImplementedError
...     def on_mouse_click_rightbutton(self): raise NotImplementedError
...     def on_mouse_unclick_rightbutton(self): raise NotImplementedError
...     def on_key_press(self): raise NotImplementedError
...     def on_key_unpress(self): raise NotImplementedError
>>>
>>>
>>> class Pencil(Tool):
...     def on_mouse_over(self):
...         ...
...
...     def on_mouse_out(self):
...         ...
...
...     def on_mouse_click_leftbutton(self):
...         ...
...
...     def on_mouse_unclick_leftbutton(self):
...         ...
...
...     def on_mouse_click_rightbutton(self):
...         ...
...
...     def on_mouse_unclick_rightbutton(self):
...         ...
...
...     def on_key_press(self):
...         ...
...
...     def on_key_unpress(self):
...         ...
>>>
>>>
>>> class Pen(Tool):
...     def on_mouse_over(self):
...         ...
...
...     def on_mouse_out(self):
...         ...
...
...     def on_mouse_click_leftbutton(self):
...         ...
...
...     def on_mouse_unclick_leftbutton(self):
...         ...
...
...     def on_mouse_click_rightbutton(self):
...         ...
...
...     def on_mouse_unclick_rightbutton(self):
...         ...
...
...     def on_key_press(self):
...         ...
...
...     def on_key_unpress(self):
...         ...

6.2.5. References

6.2.6. Assignments

Code 6.53. Solution
"""
* Assignment: DesignPatterns Behavioral State
* Complexity: medium
* Lines of code: 34 lines
* Time: 13 min

English:
    1. Implement State pattern
    2. Then add another language:
        a. Chinese hello: 你好
        b. Chinese goodbye: 再见
    3. Run doctests - all must succeed

Polish:
    1. Zaimplementuj wzorzec State
    2. Następnie dodaj nowy język:
        a. Chinese hello: 你好
        b. Chinese goodbye: 再见
    3. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> polish = Translation(Polish())
    >>> english = Translation(English())
    >>> chinese = Translation(Chinese())

    >>> polish.hello()
    'Cześć'
    >>> polish.goodbye()
    'Do widzenia'

    >>> english.hello()
    'Hello'
    >>> english.goodbye()
    'Goodbye'

    >>> chinese.hello()
    '你好'
    >>> chinese.goodbye()
    '再见'
"""
from enum import Enum


class Language(Enum):
    POLISH = 'pl'
    ENGLISH = 'en'
    SPANISH = 'es'


class Translation:
    language: Language

    def __init__(self, language: Language):
        self.language = language

    def hello(self) -> str:
        if self.language is Language.POLISH:
            return 'Cześć'
        elif self.language is Language.ENGLISH:
            return 'Hello'
        elif self.language is Language.SPANISH:
            return 'Buenos Días'
        else:
            return 'Unknown language'

    def goodbye(self) -> str:
        if self.language is Language.POLISH:
            return 'Do widzenia'
        elif self.language is Language.ENGLISH:
            return 'Goodbye'
        elif self.language is Language.SPANISH:
            return 'Adiós'
        else:
            return 'Unknown language'