6.2. State

  • EN: State

  • PL: Stan

  • Type: object

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.1. 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.35. 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'