Design Patterns in Python: Strategic Patterns

Keywords: Python

Although design patterns have nothing to do with languages, this does not mean that every pattern can be used in every language. There are 23 patterns in Design Patterns: The Foundation of Reusable Object-Oriented Software, 16 of which are "missing or simplified" in dynamic languages.

1. Overview of Strategic Patterns

Policy patterns: Define a series of algorithms, encapsulate them one by one, and make them interchangeable. This pattern allows the algorithm to change without affecting the customers who use the algorithm.

In the field of e-commerce, there is a classic case of using the "strategy" model, that is, calculating discounts based on customer attributes or commodities in orders.

Suppose an online store has the following discount rules.

  • Customers with 1000 or more points will receive a 5% discount on each order.
  • In the same order, the quantity of a single commodity reaches 20 or more, with a 10% discount.
  • The discount is 7% for 10 or more different items in the order.

For simplicity, we assume that an order can only enjoy one discount at a time.

The UML class diagram is as follows:

The Promotion abstract class provides a common interface for different algorithms, and three subclasses, fidelityPromo,BulkPromo and LargeOrderPromo, implement specific "policies", which are chosen by the customers of the context class.

In this example, before instantiating an order (Order class), the system chooses a promotional discount strategy in some way and passes it on to the Order construction method. It is not within the scope of this model's responsibility to choose specific strategies. (Selection strategy can use factory mode.)

2. Strategic modes of traditional methods:

from abc import ABC, abstractmethod
from collections import namedtuple

Customer = namedtuple('Customer', 'name fidelity')


class LineItem:
    """Quantity and unit price of a single commodity in an order"""
    def __init__(self, product, quantity, price):
        self.product = product
        self.quantity = quantity
        self.price = price

    def total(self):
        return self.price * self.quantity


class Order:
    """Order"""
    def __init__(self, customer, cart, promotion=None):
        self.customer = customer
        self.cart = list(cart)
        self.promotion = promotion

    def total(self):
        if not hasattr(self, '__total'):
            self.__total = sum(item.total() for item in self.cart)
        return self.__total

    def due(self):
        if self.promotion is None:
            discount = 0
        else:
            discount = self.promotion.discount(self)
        return self.total() - discount

    def __repr__(self):
        fmt = '<Total Order Price: {:.2f} Actual payment: {:.2f}>'
        return fmt.format(self.total(), self.due())


class Promotion(ABC):  # Policy: abstract base class
    @abstractmethod
    def discount(self, order):
        """Return the discount amount (positive value)"""


class FidelityPromo(Promotion):  # The first specific strategy
    """Provide 5 for customers with a score of 1000 or more%Discount?"""
    def discount(self, order):
        return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0


class BulkItemPromo(Promotion):  # Second specific strategy
    """Provide 10 for 20 or more individual items%Discount?"""
    def discount(self, order):
        discount = 0
        for item in order.cart:
            if item.quantity >= 20:
                discount += item.total() * 0.1
        return discount


class LargeOrderPromo(Promotion):  # Third specific strategy
    """Provide 7 for 10 or more different items in the order%Discount?"""
    def discount(self, order):
        distinct_items = {item.product for item in order.cart}
        if len(distinct_items) >= 10:
            return order.total() * 0.07
        return 0


joe = Customer('John Doe', 0)
ann = Customer('Ann Smith', 1100)

cart = [LineItem('banana', 4, 0.5),
        LineItem('apple', 10, 1.5),
        LineItem('watermellon', 5, 5.0)]

print('Strategy 1: Provide 5 for customers with a score of 1000 or more%Discount?')
print(Order(joe, cart, FidelityPromo()))
print(Order(ann, cart, FidelityPromo()))

banana_cart = [LineItem('banana', 30, 0.5),
               LineItem('apple', 10, 1.5)]

print('Strategy 2: Provide 10 for 20 or more individual goods%Discount?')
print(Order(joe, banana_cart, BulkItemPromo()))

long_order = [LineItem(str(item_code), 1, 1.0) for item_code in range(10)]

print('Strategy 3: Provide 7 for 10 or more different items in the order%Discount?')
print(Order(joe, long_order, LargeOrderPromo()))
print(Order(joe, cart, LargeOrderPromo()))

Output:

Strategy 1: Provide a 5% discount for customers with a score of 1000 or more
 <Total Order Price: 42.00 Payment: 42.00>
<Total Order Price: 42.00 Payment: 39.90>
Strategy 2: 10% discount for 20 or more items
 <Total Order Price: 30.00 Payment: 28.50>
Strategy 3: 7% discount for 10 or more items in the order
 <Total Order Price: 10.00 Payment: 9.30>
<Total Order Price: 42.00 Payment: 42.00>

3. Implementing Policy Patterns with Functions

In the traditional policy pattern, each specific policy is a class, and only one method is defined. There are no other instance attributes. They look like ordinary functions. Indeed, in Python, we can replace specific policies with simple functions and remove abstract classes of policies.

from collections import namedtuple

Customer = namedtuple('Customer', 'name fidelity')


class LineItem:
    def __init__(self, product, quantity, price):
        self.product = product
        self.quantity = quantity
        self.price = price

    def total(self):
        return self.price * self.quantity


class Order:
    def __init__(self, customer, cart, promotion=None):
        self.customer = customer
        self.cart = list(cart)
        self.promotion = promotion

    def total(self):
        if not hasattr(self, '__total'):
            self.__total = sum(item.total() for item in self.cart)
        return self.__total

    def due(self):
        if self.promotion is None:
            discount = 0
        else:
            discount = self.promotion(self)
        return self.total() - discount

    def __repr__(self):
        fmt = '<Total Order Price: {:.2f} Actual payment: {:.2f}>'
        return fmt.format(self.total(), self.due())


def fidelity_promo(order):
    """Provide 5 for customers with a score of 1000 or more%Discount?"""
    return order.total() * .05 if order.customer.fidelity >= 1000 else 0


def bulk_item_promo(order):
    """Provide 10 for 20 or more individual items%Discount?"""
    discount = 0
    for item in order.cart:
        if item.quantity >= 20:
            discount += item.total() * .1
    return discount


def large_order_promo(order):
    """Provide 7 for 10 or more different items in the order%Discount?"""
    distinct_items = {item.product for item in order.cart}
    if len(distinct_items) >= 10:
        return order.total() * .07
    return 0


joe = Customer('John Doe', 0)
ann = Customer('Ann Smith', 1100)

cart = [LineItem('banana', 4, 0.5),
        LineItem('apple', 10, 1.5),
        LineItem('watermellon', 5, 5.0)]

print('Strategy 1: Provide 5 for customers with a score of 1000 or more%Discount?')
print(Order(joe, cart, fidelity_promo))
print(Order(ann, cart, fidelity_promo))

banana_cart = [LineItem('banana', 30, 0.5),
               LineItem('apple', 10, 1.5)]

print('Strategy 2: Provide 10 for 20 or more individual goods%Discount?')
print(Order(joe, banana_cart, bulk_item_promo))

long_order = [LineItem(str(item_code), 1, 1.0) for item_code in range(10)]

print('Strategy 3: Provide 7 for 10 or more different items in the order%Discount?')
print(Order(joe, long_order, large_order_promo))
print(Order(joe, cart, large_order_promo))

In fact, as long as it is a language that supports higher-order functions, it can be implemented in this way. For example, in C #, it can be implemented by delegation. This implementation only makes the code more complex and difficult to understand. In Python, functions can naturally be passed as parameters.

It is worth noting that the author of Design Patterns: The Foundation of Reusable Object-Oriented Software points out that "Strategic Objects are usually good hedons." Hedgehogs are shareable objects that can be used in multiple contexts at the same time. Sharing is recommended so that you don't have to constantly create specific policy objects in each new context (in this case, the Order instance) to reduce consumption. Therefore, in order to avoid running time consumption of [policy mode], it can be used together with [hedonic mode], but in this way, the number of lines of code and maintenance costs will continue to rise.

In complex situations, when specific strategies are needed to maintain internal state, it may be necessary to combine the "strategy" and "hedonic" modes. However, specific strategies generally do not have internal state, but deal with data in the context. At this point, we must use ordinary functions, do not write a class with only one method, and then implement a single function interface declared by another class. Functions are lighter than instances of user-defined classes and do not need to use the "hedonic" mode, because each policy function is created only once when Python compiles the module. Ordinary functions are also "shareable objects that can be used in multiple contexts at the same time".

Sweeping the code pays attention to my public number:

Python Road for Older Manon

Posted by freakyG on Sat, 11 May 2019 17:27:53 -0700