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.
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.
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.
classNoCardState(ATMState):definsert_card(self):print("🏧 Card inserted")self.atm_machine.state=self.atm_machine.has_card_statedefeject_card(self):print("🏧 No card to eject")defenter_pin(self,pin):print("🏧 Insert a card first")defrequest_cash(self):print("🏧 Insert a card first")classHasCardState(ATMState):definsert_card(self):print("🏧 You cannot insert more than one card")defeject_card(self):print("🏧 Card ejected")self.atm_machine.state=self.atm_machine.no_card_statedefenter_pin(self,pin):ifpin:print(f"🏧 Pin entered correctly: {pin}")self.atm_machine.state=self.atm_machine.has_correct_pin_stateelse:print("🏧 Wrong pin")self.atm_machine.wrong_pin_count+=1ifself.atm_machine.wrong_pin_count>=2:print("🏧 Too many wrong attempts")self.eject_card()defrequest_cash(self):print("🏧 Please enter your pin first")classHasCorrectPinState(ATMState):definsert_card(self):print("🏧 You cannot insert more than one card")defeject_card(self):print("🏧 Card ejected\n")self.atm_machine.state=self.atm_machine.no_card_statedefenter_pin(self,pin):print("🏧 You already entered your pin")defrequest_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
classATMMachine:def__init__(self):self.card_inserted=Falseself.pin_entered=Falseself.wrong_pin_count=0definsert_card(self):print("🤑 Insert card")ifself.card_inserted:print("🏧 You cannot insert more than one card")else:print("🏧 Card inserted")self.card_inserted=Truedefeject_card(self):print("🤑 Eject card")ifnotself.card_inserted:print("🏧 No card to eject")else:print("🏧 Card ejected\n")self.card_inserted=Falseself.pin_entered=Falseself.wrong_pin_count=0defenter_pin(self,pin):print("🤑 Enter pin")ifnotself.card_inserted:print("🏧 Insert a card first")else:ifself.pin_entered:print("🏧 You already entered your pin")elifpin:print(f"🏧 Pin entered correctly: {pin}")self.pin_entered=Trueelse:print("🏧 Wrong pin")self.wrong_pin_count+=1ifself.wrong_pin_count>=2:print("🏧 Too many wrong attempts")self.eject_card()defrequest_cash(self):print("🤑 Request cash")ifnotself.card_inserted:print("🏧 Insert a card first")elifnotself.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)
classATMState:def__init__(self,atm_machine):self.atm_machine=atm_machinedefinsert_card(self):passdefeject_card(self):passdefenter_pin(self,pin):passdefrequest_cash(self):passclassNoCardState(ATMState):definsert_card(self):print("🏧 Card inserted")self.atm_machine.state=self.atm_machine.has_card_statedefeject_card(self):print("🏧 No card to eject")defenter_pin(self,pin):print("🏧 Insert a card first")defrequest_cash(self):print("🏧 Insert a card first")classHasCardState(ATMState):definsert_card(self):print("🏧 You cannot insert more than one card")defeject_card(self):print("🏧 Card ejected")self.atm_machine.state=self.atm_machine.no_card_statedefenter_pin(self,pin):ifpin:print(f"🏧 Pin entered correctly: {pin}")self.atm_machine.state=self.atm_machine.has_correct_pin_stateelse:print("🏧 Wrong pin")self.atm_machine.wrong_pin_count+=1ifself.atm_machine.wrong_pin_count>=2:print("🏧 Too many wrong attempts")self.eject_card()defrequest_cash(self):print("🏧 Please enter your pin first")classHasCorrectPinState(ATMState):definsert_card(self):print("🏧 You cannot insert more than one card")defeject_card(self):print("🏧 Card ejected\n")self.atm_machine.state=self.atm_machine.no_card_statedefenter_pin(self,pin):print("🏧 You already entered your pin")defrequest_cash(self):print("🏧 Cash dispensed 💵")classATMMachine: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_stateself.wrong_pin_count=0definsert_card(self):print("🤑 Insert card")self.state.insert_card()defeject_card(self):print("🤑 Eject card")self.state.eject_card()defenter_pin(self,pin):print("🤑 Enter pin")self.state.enter_pin(pin)defrequest_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.