Featured image of post BP6 - State - Learn Design Pattern From Simple Things

BP6 - State - Learn Design Pattern From Simple Things

Have you ever thought about the ATM logic? How is ATM logic organized to cover all cases without any misses? Do you feel confused when handling many operations on many states?

State is a behavioral design pattern: šŸ–¤šŸ–¤šŸ–¤šŸ–¤šŸ–¤

What is State?

State (Pipeline) is a technique that separate use cases (insert_card, eject_card, enter_pin, request_card) into small stages(no_card_state, has_card_state, has_correct_pin_state)

  • insert_card when no_card_state
  • eject_card when no_card_state
  • enter_pin when no_card_state
  • request_card when no_card_state
  • insert_card when has_card_state
  • eject_card when has_card_state
  • enter_pin when has_card_state
  • request_card when has_card_state
  • insert_card when has_correct_pin_state
  • eject_card when has_correct_pin_state
  • enter_pin when has_correct_pin_state
  • request_card when has_correct_pin_state

It breaks down a complex process into a series of smaller, more manageable stages. Each stage is responsible for a specific task and passes its output to the next stage.

State diagram

Why use State?

This pattern is useful when you have a large, complex process that needs to be broken down into smaller, more manageable stages.

  • It allows an object to change its behavior based on its internal state.
  • It helps to avoid many if/else statements in the code.
  • It allows the object to change its behavior without changing its class.
  • It is useful when state varies the behavior of some object whose composition includes that state.

Question: I hate the State, what are the alternatives?

Answer: You can just code as normal without thinking about any pattern

  • One alternative is the Strategy design pattern, which is used when we want to change the behavior of an object based on the context in which it is used.
  • Another alternative is the Command design pattern, which is used when we want to encapsulate a request as an object, thereby letting us parameterize clients with different requests, queue or log requests, and support undoable operations.
  • Yet another alternative is the Template Method design pattern, which is used when we want to define the skeleton of an algorithm in a base class, but let subclasses override specific steps of the algorithm without changing its structure.

State

When to use State?

Question: When should I use State?

Answer: When you have situations like these:

  • You have an object that behaves differently depending on its current state.
  • The number of states is large and the state-specific code changes frequently.
  • You want to avoid complex conditional logic and improve code readability and maintainability.

Input:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class NoCardState(ATMState):
    def insert_card(self):
        print("šŸ§ Card inserted")
        self.atm_machine.state = self.atm_machine.has_card_state

    def eject_card(self):
        print("šŸ§ No card to eject")

    def enter_pin(self, pin):
        print("šŸ§ Insert a card first")

    def request_cash(self):
        print("šŸ§ Insert a card first")


class HasCardState(ATMState):
    def insert_card(self):
        print("šŸ§ You cannot insert more than one card")

    def eject_card(self):
        print("šŸ§ Card ejected")
        self.atm_machine.state = self.atm_machine.no_card_state

    def enter_pin(self, pin):
        if pin:
            print(f"šŸ§ Pin entered correctly: {pin}")
            self.atm_machine.state = self.atm_machine.has_correct_pin_state
        else:
            print("šŸ§ Wrong pin")
            self.atm_machine.wrong_pin_count += 1
            if self.atm_machine.wrong_pin_count >= 2:
                print("šŸ§ Too many wrong attempts")
                self.eject_card()

    def request_cash(self):
        print("šŸ§ Please enter your pin first")


class HasCorrectPinState(ATMState):
    def insert_card(self):
        print("šŸ§ You cannot insert more than one card")

    def eject_card(self):
        print("šŸ§ Card ejected\n")
        self.atm_machine.state = self.atm_machine.no_card_state

    def enter_pin(self, pin):
        print("šŸ§ You already entered your pin")

    def request_cash(self):
        print("šŸ§ Cash dispensed šŸ’µ")

Expected Output:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
šŸ¤‘ Request cash
šŸ§ Insert a card first
šŸ¤‘ Insert card
šŸ§ Card inserted
šŸ¤‘ Enter pin
šŸ§ Pin entered correctly: 123456
šŸ¤‘ Request cash
šŸ§ Cash dispensed šŸ’µ
šŸ¤‘ Eject card
šŸ§ Card ejected

šŸ¤‘ Insert card
šŸ§ Card inserted
šŸ¤‘ Enter pin
šŸ§ Wrong pin
šŸ¤‘ Enter pin
šŸ§ Wrong pin
šŸ§ Too many wrong attempts
šŸ§ Card ejected

How to implement State?

Non-State implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class ATMMachine:

    def __init__(self):
        self.card_inserted = False
        self.pin_entered = False
        self.wrong_pin_count = 0

    def insert_card(self):
        print("šŸ¤‘ Insert card")
        if self.card_inserted:
            print("šŸ§ You cannot insert more than one card")
        else:
            print("šŸ§ Card inserted")
            self.card_inserted = True

    def eject_card(self):
        print("šŸ¤‘ Eject card")
        if not self.card_inserted:
            print("šŸ§ No card to eject")
        else:
            print("šŸ§ Card ejected\n")
            self.card_inserted = False
            self.pin_entered = False
            self.wrong_pin_count = 0

    def enter_pin(self, pin):
        print("šŸ¤‘ Enter pin")
        if not self.card_inserted:
            print("šŸ§ Insert a card first")
        else:
            if self.pin_entered:
                print("šŸ§ You already entered your pin")
            elif pin:
                print(f"šŸ§ Pin entered correctly: {pin}")
                self.pin_entered = True
            else:
                print("šŸ§ Wrong pin")
                self.wrong_pin_count += 1
                if self.wrong_pin_count >= 2:
                    print("šŸ§ Too many wrong attempts")
                    self.eject_card()

    def request_cash(self):
        print("šŸ¤‘ Request cash")
        if not self.card_inserted:
            print("šŸ§ Insert a card first")
        elif not self.pin_entered:
            print("šŸ§ Please enter your pin first")
        else:
            print("šŸ§ Cash dispensed šŸ’µ")


if __name__ == "__main__":
    atm = ATMMachine()
    atm.request_cash()
    atm.insert_card()
    atm.enter_pin(123456)
    atm.request_cash()
    atm.eject_card()
    atm.insert_card()
    atm.enter_pin(None)
    atm.enter_pin(None)

State Implementation:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
class ATMState:
    def __init__(self, atm_machine):
        self.atm_machine = atm_machine

    def insert_card(self):
        pass

    def eject_card(self):
        pass

    def enter_pin(self, pin):
        pass

    def request_cash(self):
        pass


class NoCardState(ATMState):
    def insert_card(self):
        print("šŸ§ Card inserted")
        self.atm_machine.state = self.atm_machine.has_card_state

    def eject_card(self):
        print("šŸ§ No card to eject")

    def enter_pin(self, pin):
        print("šŸ§ Insert a card first")

    def request_cash(self):
        print("šŸ§ Insert a card first")


class HasCardState(ATMState):
    def insert_card(self):
        print("šŸ§ You cannot insert more than one card")

    def eject_card(self):
        print("šŸ§ Card ejected")
        self.atm_machine.state = self.atm_machine.no_card_state

    def enter_pin(self, pin):
        if pin:
            print(f"šŸ§ Pin entered correctly: {pin}")
            self.atm_machine.state = self.atm_machine.has_correct_pin_state
        else:
            print("šŸ§ Wrong pin")
            self.atm_machine.wrong_pin_count += 1
            if self.atm_machine.wrong_pin_count >= 2:
                print("šŸ§ Too many wrong attempts")
                self.eject_card()

    def request_cash(self):
        print("šŸ§ Please enter your pin first")


class HasCorrectPinState(ATMState):
    def insert_card(self):
        print("šŸ§ You cannot insert more than one card")

    def eject_card(self):
        print("šŸ§ Card ejected\n")
        self.atm_machine.state = self.atm_machine.no_card_state

    def enter_pin(self, pin):
        print("šŸ§ You already entered your pin")

    def request_cash(self):
        print("šŸ§ Cash dispensed šŸ’µ")


class ATMMachine:
    def __init__(self):
        self.no_card_state = NoCardState(self)
        self.has_card_state = HasCardState(self)
        self.has_correct_pin_state = HasCorrectPinState(self)

        self.state = self.no_card_state
        self.wrong_pin_count = 0

    def insert_card(self):
        print("šŸ¤‘ Insert card")
        self.state.insert_card()

    def eject_card(self):
        print("šŸ¤‘ Eject card")
        self.state.eject_card()

    def enter_pin(self, pin):
        print("šŸ¤‘ Enter pin")
        self.state.enter_pin(pin)

    def request_cash(self):
        print("šŸ¤‘ Request cash")
        self.state.request_cash()


if __name__ == "__main__":
    atm = ATMMachine()
    atm.request_cash()
    atm.insert_card()
    atm.enter_pin(123456)
    atm.request_cash()
    atm.eject_card()
    atm.insert_card()
    atm.enter_pin(None)
    atm.enter_pin(None)

By using the State Design Pattern, we’ve separated the behavior of the ATM into discrete states, which makes it easier to modify the behavior of the ATM without affecting other parts of the code. We’ve also avoided using conditional statements to determine the behavior of the ATM based on the current state. Instead, each state encapsulates its behavior, and the ATM simply delegates its behavior to the current state.

This approach is more flexible and easier to extend. For example, if we wanted to add a new state, such as “out of service,” we could simply create a new class for that state and modify the ATM to transition to that state when needed. Similarly, if we wanted to modify the behavior of an existing state, we could simply modify the relevant class without affecting other parts of the code.

Overall, the State Design Pattern is a powerful tool for managing complex behavior in software systems. It allows us to encapsulate behavior in discrete states, making the system easier to maintain and extend over time.

Source Code

Made with the laziness šŸ¦„
by a busy guy