dogmadogmassage.com

Exploring Python Design Patterns with First-Class Functions

Written on

Chapter 1: Introduction to Design Patterns

Design patterns serve as universal solutions to prevalent design challenges encountered by software developers. Though they are not specific to any programming language, their application can vary across different languages. In Python, certain design patterns can be effectively utilized with first-class functions. Today, we will delve into two such patterns: The Strategy Pattern and The Command Pattern.

First-class functions are particularly valuable because they enable developers to write more succinct code, minimizing boilerplate.

For those interested in staying updated with my latest articles or joining a community of fellow developers, consider following me on Medium; your support is greatly appreciated! ❤️

Section 1.1: The Strategy Pattern

The Strategy Pattern is succinctly described in the book "Design Patterns" as follows:

"Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy enables the algorithm to vary independently of the clients that utilize it."

To elaborate, the Strategy Pattern suggests that we should take a class or callable, which in Python’s context can perform specific tasks in various ways, and separate them into distinct callables referred to as strategies.

This pattern comprises three components: The Context, The Strategy, and Concrete Strategies. The Context holds a reference to one of the strategies and delegates the task to the selected strategy callable rather than executing it directly. The Context remains unaware of which strategy is being used, thus having limited knowledge about the strategies available.

To illustrate, let’s consider a scenario where a customer wants to apply a discount based on their attributes and the order details. The book "Fluent Python" offers an in-depth discussion on this example.

Case Study: Discount Rules in an Online Store

Imagine an online shop with the following discount policies:

  1. Customers with 1,000 or more loyalty points receive a 5% discount on their entire order.
  2. Each line item with 20 or more units gets a 10% discount.
  3. Orders containing at least 10 different items qualify for a 7% overall discount.
  4. Only one discount can be applied per order.
UML diagram illustrating order discount processing

Context

The context maintains a reference to the strategy in use. In our case, the context is represented by an Order that is set up to apply a promotional discount.

Strategy

The interface that is common to the components implementing different algorithms is defined by an abstract class named Promotion.

Concrete Strategies

The concrete subclasses of the Strategy utilized by the Context include FidelityPromo, BulkPromo, and LargeOrderPromo.

There are various approaches to implementing the Strategy Pattern in Python:

  1. The classic method using classes, as illustrated in the UML diagram.
  2. Utilizing first-class functions as strategies.

Classic Strategy Pattern Using Classes:

from abc import ABC, abstractmethod

from collections.abc import Sequence

from decimal import Decimal

from typing import NamedTuple, Optional

class Customer(NamedTuple):

name: str

fidelity: int

class LineItem(NamedTuple):

product: str

quantity: int

price: Decimal

def total(self) -> Decimal:

return self.price * self.quantity

class Order(NamedTuple): # the Context

customer: Customer

cart: Sequence[LineItem]

promotion: Optional['Promotion'] = None

def total(self) -> Decimal:

totals = (item.total() for item in self.cart)

return sum(totals, start=Decimal(0))

def due(self) -> Decimal:

if self.promotion is None:

discount = Decimal(0)

else:

discount = self.promotion.discount(self)

return self.total() - discount

def __repr__(self):

return f''

class Promotion(ABC): # the Strategy: an abstract base class

@abstractmethod

def discount(self, order: Order) -> Decimal:

"""Return discount as a positive dollar amount"""

class FidelityPromo(Promotion): # first Concrete Strategy

"""5% discount for customers with 1000 or more fidelity points"""

def discount(self, order: Order) -> Decimal:

rate = Decimal('0.05')

if order.customer.fidelity >= 1000:

return order.total() * rate

return Decimal(0)

class BulkItemPromo(Promotion): # second Concrete Strategy

"""10% discount for each LineItem with 20 or more units"""

def discount(self, order: Order) -> Decimal:

discount = Decimal(0)

for item in order.cart:

if item.quantity >= 20:

discount += item.total() * Decimal('0.1')

return discount

class LargeOrderPromo(Promotion): # third Concrete Strategy

"""7% discount for orders with 10 or more distinct items"""

def discount(self, order: Order) -> Decimal:

distinct_items = {item.product for item in order.cart}

if len(distinct_items) >= 10:

return order.total() * Decimal('0.07')

return Decimal(0)

To see it in action:

ann = Customer('Ann Smith', 1100)

cart = (LineItem('banana', 4, Decimal('.5')), LineItem('apple', 10, Decimal('1.5')), LineItem('watermelon', 5, Decimal(5)))

# Ann qualifies for a 5% discount due to her 1,000 points.

Order(ann, cart, FidelityPromo())

Using First-Class Functions as Concrete Strategies

In the traditional Strategy Pattern, classes model promotions; however, since these instances lack internal state, they can be represented as functions, allowing us to eliminate the Promo abstract class entirely.

from collections.abc import Sequence

from dataclasses import dataclass

from decimal import Decimal

from typing import Optional, Callable, NamedTuple

class Customer(NamedTuple):

name: str

fidelity: int

class LineItem(NamedTuple):

product: str

quantity: int

price: Decimal

def total(self):

return self.price * self.quantity

@dataclass(frozen=True)

class Order: # the Context

customer: Customer

cart: Sequence[LineItem]

promotion: Optional[Callable[['Order'], Decimal]] = None

def total(self) -> Decimal:

totals = (item.total() for item in self.cart)

return sum(totals, start=Decimal(0))

def due(self) -> Decimal:

if self.promotion is None:

discount = Decimal(0)

else:

discount = self.promotion(self)

return self.total() - discount

def __repr__(self):

return f''

def fidelity_promo(order: Order) -> Decimal:

"""5% discount for customers with 1000 or more fidelity points"""

if order.customer.fidelity >= 1000:

return order.total() * Decimal('0.05')

return Decimal(0)

def bulk_item_promo(order: Order) -> Decimal:

"""10% discount for each LineItem with 20 or more units"""

discount = Decimal(0)

for item in order.cart:

if item.quantity >= 20:

discount += item.total() * Decimal('0.1')

return discount

def large_order_promo(order: Order) -> Decimal:

"""7% discount for orders with 10 or more distinct items"""

distinct_items = {item.product for item in order.cart}

if len(distinct_items) >= 10:

return order.total() * Decimal('0.07')

return Decimal(0)

# Example usage

ann = Customer('Ann Smith', 1100)

cart = (LineItem('banana', 4, Decimal('.5')), LineItem('apple', 10, Decimal('1.5')), LineItem('watermelon', 5, Decimal(5)))

# Ann gets a 5% discount due to her 1,000 points.

Order(ann, cart, fidelity_promo())

Chapter 2: The Command Pattern

The Command Pattern is an architectural approach that encapsulates a request into a standalone entity that contains all relevant request data. This design makes it easier to forward requests as method arguments, or to suspend and queue their execution.

The primary objective of the Command Pattern is to decouple the object that invokes an operation (the invoker) from the object that implements it (the receiver).

A Command object serves as an intermediary between the invoker and the receiver, providing a unified interface with a method, typically named execute, which instructs the receiver to perform the designated operation. Consequently, the invoker does not need to comprehend the receiver's interface, allowing for the adaptation of various receivers through distinct Command callables.

The example presented in the book "Design Patterns" illustrates that each invoker corresponds to a menu item in a graphical application, while the receivers are the document being edited or the application itself.

Command Pattern Implementation:

from abc import ABC, abstractmethod

class Command(ABC):

"""Constructor method"""

def __init__(self, receiver):

self.receiver = receiver

"""Process method"""

def process(self):

pass

class CommandImplementation(Command):

"""Constructor method"""

def __init__(self, receiver):

self.receiver = receiver

"""Process method"""

def process(self):

self.receiver.perform_action()

class Receiver:

"""Perform-action method"""

def perform_action(self):

print('Action performed in receiver.')

class Invoker:

"""Command method"""

def command(self, cmd):

self.cmd = cmd

"""Execute method"""

def execute(self):

self.cmd.process()

# Main method

if __name__ == "__main__":

"""Create Receiver object"""

receiver = Receiver()

cmd = CommandImplementation(receiver)

invoker = Invoker()

invoker.command(cmd)

invoker.execute()

Conclusion

Utilizing callables in Python emerges as the most straightforward option due to the availability of various callable types, including functions and objects. This flexibility allows us to apply well-known design patterns in a clear and comprehensible manner without the clutter associated with classes.

Further Reading

  • Recipe 8.21: Implementing the Visitor Pattern in the Python Cookbook, 3rd ed.
  • The Fluent Python Book.
  • The Design Patterns Book.
  • Learning Python Design Patterns Book.
  • Expert Python Programming Book.
  • Level Up Coding

Thanks for being a part of our community! Before you leave, please consider giving a clap for this article and following the author for more insights!

The first video explains the significance of design patterns in Python, addressing the common question: "Why Use Design Patterns When Python Has Functions?"

The second video discusses underrated design patterns that can enhance your Python coding practices.

Share the page:

Twitter Facebook Reddit LinkIn

-----------------------

Recent Post:

The Journey of Self-Reclamation: Rising from Within

Explore the transformative journey of self-reclamation, embracing growth, and finding harmony within yourself.

Reflecting on Leadership: Lessons from Emperor Li Shimin's Legacy

Explore key leadership insights from Emperor Li Shimin, applicable for modern management and avoiding common pitfalls in authority.

The Most Practical Superpower: A Philosophical Exploration

A deep dive into the practicalities of superpowers and their implications on free will and personal boundaries.