This will be a 4 part series on how to integrate State Design in Django. In this first segment, I’ll start by showing how State Design could be applied to python.

Introduction

State Design is a popular design pattern that allows an object to alter its behavior when its internal state changes. It’s particularly useful for modeling finite state machines, where an entity can be in one of many possible states, and transitions between states trigger specific actions.

A common example is the status of a sales order in an e-commerce system. Typically, an order might transition through several states: browsing, closing cart, processing payment, waiting for shipment, delivered, and possibly canceled. Each state has distinct behaviors and constraints.

For instance:

  1. Browsing: The user can add products to the cart but cannot proceed to payment or shipment.
  2. Closing Cart: The user can either proceed to payment or revert to browsing.
  3. Processing Payment: Items cannot be added to the cart; the user can either complete payment or cancel the order.
  4. Waiting for Shipment: Payment is complete, and the order is pending shipment.
  5. Delivered: The order is delivered, and no further actions are allowed.
  6. Canceled: The order can be canceled from any state, terminating the process.

The state transitions can be visualized in a state diagram, illustrating how each state leads to another.

Implementing State Design in Python

“So how do we do that in Python?”, you might ask. This too has been covered in different posts, blogs and videos, but to facilitate the understanding I’ll cover here.

The first thing we need to do is create a base state protocol. Sort of the blueprint of any state we create later. This will ensure consistency accross different states.

So we are going to create the SalesOrderState class, which will inherit from Protocol and will dictate what methods and attributes each state needs to have.`

Defining the Base State Protocol

from typing import Protocol

class SalesOrderState(Protocol):
    """
    A protocol that defines the required interface for state 
    classes handling sales orders.
    """
    name: str

    def shop(self): 
        ...
    def close_cart(self): 
        ...
    def process_payment(self): 
        ...
    def wait_for_shipment(self): 
        ...
    def deliver(self): 
        ...
    def cancel_order(self): 
        ...

Context for the Sales Order

Next, we define the context that represents the sales order. This context will use different state objects to manage its behavior. I took the liberty of changing the name of the methods to show that they don’t need to be the same names as the SalesOrderState objects.

from typing import Protocol

class SalesOrderContext(Protocol):
    """
    Protocol that defines the required interface for sales 
    order classes.
    """
    def set_state(self, state: SalesOrderState): 
        ...
    def shop_for_items(self): 
        ...
    def checkout_goods(self): 
        ...
    def pay_for_things(self): 
        ...
    def ship_the_goods(self): 
        ...
    def stuff_delivered(self): 
        ...
    def cancel_the_darn_thing(self): 
        ...

Implementing State Classes

Now, we’ll implement the concrete state classes. Each state class will encapsulate the behavior associated with that state.

class CancelState:
    """
    Represent the cancel state of a sales order.
    """
    name: str = "Canceled"
    sales_order: SalesOrderContext

    def shop(self):
        print("Order cancelled. Can't do that.")

    def close_cart(self):
        print("Order cancelled. Can't do that.")

    def process_payment(self):
        print("Order cancelled. Can't do that.")

    def wait_for_shipment(self):
        print("Order cancelled. Can't do that.")

    def deliver(self):
        print("Order cancelled. Can't do that.")

    def cancel_order(self):
        print("We're already there.")


class ShoppingState:
    """
    Represent the shopping state of a sales order.
    """
    name: str = "Shopping"
    sales_order: SalesOrderContext

    def shop(self):
        print("Merrily adding items to my cart with no care for my credit card bills")

    def close_cart(self):
        print("Seems that I'm ready")
        self.sales_order.set_state(ClosingCartState(self.sales_order))

    def process_payment(self):
        print("Not until you close the cart.")

    def wait_for_shipment(self):
        print("Pay first, ship later")

    def deliver(self):
        print("Nope")

    def cancel_order(self):
        print("Abandon Shi... aaahmm... Cart?")
        self.sales_order.set_state(CancelState(self.sales_order))


class ClosingCartState:
    """
    Represent the closed cart state of a sales order.
    """
    name: str = "Cart Closed"
    sales_order: SalesOrderContext
    
    def shop(self):
        print("Forgot a few things eh? Happens to the best of us.")
        self.sales_order.set_state(ShoppingState(self.sales_order))

    def close_cart(self):
        print("It's closed! I swear it's closed!")

    def process_payment(self):
        print("How about we pay for the stuff?")
        self.sales_order.set_state(ProcessingPaymentState(self.sales_order))

    def wait_for_shipment(self):
        print("You gotta pay first, my friend.")

    def deliver(self):
        print("Not yet")

    def cancel_order(self):
        print("No goods for you")
        self.sales_order.set_state(CancelState(self.sales_order))


class ProcessingPaymentState:
    """
    Represent the process payment state of a sales order. 
    """
    name: str = "Processing Payment"
    sales_order: SalesOrderContext

    def shop(self):
        print("Can't add things after you have paid.")

    def close_cart(self):
        print("But... You already paid for it...")

    def process_payment(self):
        print("We just did that. If you have some extra cash, send it my way.")

    def wait_for_shipment(self):
        print("Let's get you your stuff.")
        self.sales_order.set_state(ShippingState(self.sales_order))

    def deliver(self):
        print("Nope")

    def cancel_order(self):
        print("No goods for you")
        self.sales_order.set_state(CancelState(self.sales_order))


class ShippingState:
    """
    Represent the shipping state of a sales order.
    """
    name: str = "Shipping"
    sales_order: SalesOrderContext
    
    def shop(self):
        print("Can't add things after they have shipped.")

    def close_cart(self):
        print("Not gonna do that now...")

    def process_payment(self):
        print("You already paid for it.")

    def wait_for_shipment(self):
        print("It's been shipped already")

    def deliver(self):
        print("Should be there by now")
        self.sales_order.set_state(DeliveredState(self.sales_order))

    def cancel_order(self):
        print("No goods for you")
        self.sales_order.set_state(CancelState(self.sales_order))


class DeliveredState:
    """
    Represent the delivered state of a sales order.
    """
    name: str = "Delivered"
    sales_order: SalesOrderContext
    
    def shop(self):
        print("It should be with you already.")
    def process_payment(self):
        print("You already paid for it.")
    def wait_for_shipment(self):
        print("We've done that.")
    def deliver(self):
        print("Aren't you using it yet?")
    def cancel_order(self):
        print("Too late for that now...")

Final code

The final code for our sales_state.py file in this session would look like this:

from typing import Protocol



class SalesOrderState(Protocol):
    """
    A protocol that defines the required interface for state classes handling sales orders.
    """

    name: str

    def shop(self): 
        ...
    def close_cart(self):
        ...
    def process_payment(self): 
        ...
    def wait_for_shipment(self): 
        ...
    def deliver(self): 
        ...
    def cancel_order(self): 
        ...


class SalesOrderContext(Protocol):
    """
    Protocol that defines the required interface for sales order classes.
    """

    def set_state(self, state: SalesOrderState): 
        ...
    def shop_for_items(self): 
        ...
    def checkout_goods(self):
        ...
    def pay_for_things(self): 
        ...
    def ship_the_goods(self): 
        ...
    def stuff_delivered(self): 
        ...
    def cancel_the_darn_thing(self): 
        ...


class CancelState:
    """
    Represent the cancel state of a sales order.

    The user is able to cancel the sales order and no other changes will be permitted.
    """

    name: str = "Canceled"
    sales_order: SalesOrderContext

    def shop(self):
        print("Order cancelled. Can't do that.")

    def close_cart(self):
        print("Order cancelled. Can't do that.")

    def process_payment(self):
        print("Order cancelled. Can't do that.")

    def wait_for_shipment(self):
        print("Order cancelled. Can't do that.")

    def deliver(self):
        print("Order cancelled. Can't do that.")

    def cancel_order(self):
        print("We're already there.")


class ShoppingState:
    """
    Represent the shopping state of a sales order.

    The user can add items to the sales order, but cannot process do anything else.
    """

    name: str = "Shopping"
    sales_order: SalesOrderContext

    def shop(self):
        print("Marrily adding items to my cart with no care for my credit card bills")

    def close_cart(self):
        print("Seems that I'm ready")
        self.sales_order.set_state(ClosingCartState(self.sales_order))

    def process_payment(self):
        print("Not until you close the cart.")

    def wait_for_shipment(self):
        print("Pay first, ship later")

    def deliver(self):
        print("Nope")

    def cancel_order(self):
        print("Abandon Shi... aaahmm... Cart?")
        self.sales_order.set_state(CancelState(self.sales_order))


class ClosingCartState:
    """
    Represent the closed cart state of a sales order.

    The user is able to re-open the cart to keep shopping, process payment and cancel the order, but nothing else.
    """

    name: str = "Cart Closed"
    sales_order: SalesOrderContext
    
    def shop(self):
        print("Forgot a few things eh? Happens to the best of us.")
        self.sales_order.set_state(ShoppingState(self.sales_order))

    def close_cart(self):
        print("It's closed! I swear it's closed!")

    def process_payment(self):
        print("How about we pay for the stuff?")
        self.sales_order.set_state(ProcessingPaymentState(self.sales_order))

    def wait_for_shipment(self):
        print("You gotta pay first, my friend.")

    def deliver(self):
        print("Not yet")

    def cancel_order(self):
        print("No goods for you")
        self.sales_order.set_state(CancelState(self.sales_order))


class ProcessingPaymentState:
    """
    Represent the process payment state of a sales order. 
    
    The user can ship the order or can come back.
    """

    name: str = "Shopping"
    sales_order: SalesOrderContext

    def shop(self):
        print("Can't add things after you have paid.")

    def close_cart(self):
        print("But.. You already paid for it...")

    def process_payment(self):
        print("We just did that. If you have some extra cash, send it my way.")

    def wait_for_shipment(self):
        print("Let's get you your stuff.")
        self.sales_order.set_state(ShippingState(self.sales_order))

    def deliver(self):
        print("Nope")

    def cancel_order(self):
        print("No goods for you")
        self.sales_order.set_state(CancelState(self.sales_order))


class ShippingState:
    """
    Represent the shipping state of a sales order.

    The user is able to ship the item and cancel the item, but nothing else.
    """

    name: str = "Shipping"
    sales_order: SalesOrderContext
    
    def shop(self):
        print("Can't add things after they have shipped.")

    def close_cart(self):
        print("Not gonna do that now...")

    def process_payment(self):
        print("You already paid for.")

    def wait_for_shipment(self):
        print("It's been shipped already")

    def deliver(self):
        print("Should be there by now")
        self.sales_order.set_state(DeliveredState(self.sales_order))

    def cancel_order(self):
        print("No goods for you")
        self.sales_order.set_state(CancelState(self.sales_order))


class DeliveredState:
    """
    Represent the shipping state of a sales order.

    The user is able to ship the item and cancel the item, but nothing else.
    """

    name: str = "Delivered"
    sales_order: SalesOrderContext
    
    def shop(self):
        print("It should be with you already.")
    def process_payment(self):
        print("You already paid for.")
    def wait_for_shipment(self):
        print("We've done that.")
    def deliver(self):
        print("Aren't you using it yet?")
    def cancel_order(self):
        print("Too late for that now...")

Conclusion

In this first part of our series, we’ve introduced the State Design pattern and demonstrated how to implement it in Python. We defined a protocol for state classes and a context for managing state transitions. We then created several concrete state classes representing different stages of a sales order.

In the next part, we’ll explore how to integrate this design pattern into a Django project, ensuring our sales order workflow is robust and maintainable. Stay tuned!

One response to “State Design in Django – Part 1”

  1. State Design in Django – Part 2 – Dev Alvernaz Avatar

    […] ←Previous: State Design in Django – Part 1 […]

Leave a comment

I’m Leandro

Welcome to my blog page. I’m a developer with a lot of interest in technoogy in general. My main topics of knowledge are Python, Django, AWS and Docker. Feel free to contact me using my links below or using the comments section of the posts. Enjoy the articles!

Let’s connect