Python Explains Design Patterns: Strategic Patterns

Keywords: Programming Python

There are many ways to accomplish a task, which we call strategy.

For example, supermarket activities, if your shopping points are over 1000, you can exchange cash vouchers for 10 yuan, if you buy 10 pieces of the same commodity, you can discount 9 percent, if the purchase amount exceeds 500, you can enjoy the discount of 50 yuan. These are three different promotion strategies.

For example, contact friends and classmates, you can call, you can send text messages, you can send micro-letters, you can send e-mail, these are four different contact strategies.

For example, to travel, we can choose trains, buses, airplanes or self-driving. There are four different travel strategies.

These real scenarios all have the shadow of policy selection model, so we can consider using policy mode.

The classical strategy model consists of three parts

  • Context: Context environment class
  • Stragety: Policy base class
  • Concrete Stragety: Specific Strategy

Take the first supermarket activity scenario as an example.

  • Context: Order class, order information, including goods, prices and quantities, for buyers, etc.
  • Stragety: Promotion class, abstract base class, contains an abstract method (calculating discounts)
  • ContreteStragety: It is divided into three categories, Fidelity Promo, Bulk ItemPromo, LargeOrderPromo, to implement a specific discount calculation method.

First, the Order class:

class Item:
    def __init__(self, issue, price, quantity):
        self.issue = issue
        self.price = price
        self.quantity = quantity

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

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

    def add_to_cart(self, *items):
        for item in items:
            self.cart.append(item)

    def total(self):
        total = 0
        for item in self.cart:
            total += item.total()

        return total

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

Then is the strategy of converting cash vouchers into integrals. In order to ensure that our code has good scalability and maintainability, I will first write a strategy class, which is an abstract base class. Its subclasses are all specific strategies. We must implement the discount method, such as our strategy of converting cash into integrals.

from abc import ABC, abstractmethod

class Promotion(ABC):
    @abstractmethod
    def discount(self, order):
        pass

class FidelityPromo(Promotion):
    '''
    //If the score reaches 1000, you can exchange 10 yuan cash vouchers.
    '''
    def discount(self, order):
        return 10 if order.customer.fidelity >1000 else 0

Suppose Xiao Ming goes to the mall and buys a dress (600 yuan) and two pairs of shoes (200*2). His shopping score is 1500 points.

In normal times, shopping malls generally do not have activities, but for many years there are activities of cash vouchers for points.

>>> from collections import namedtuple

# Define two fields: name, shopping score
>>> Customer = namedtuple('Customer', 'name fidelity')
>>> xm = Customer('Xiao Ming', 1500)
>>> item1 = Item('shoes', 200, 3)
>>> item2 = Item('clothes', 600, 1)
>>> order = Order(xm, FidelityPromo())
>>> order.add_to_cart(item1, item2)

# The original price is 1200. If you use the integral, it's only 1190.
>>> order
<Order Total:1200 due:1190>

Looking at it, May Day is fast, and shopping malls are ready to launch promotions.

  • As long as you buy 10 items of a single item, you can get a 9% discount.
  • If the total amount of the order is greater than or equal to 500, it can be reduced by 50.

With the foundation we have laid by using the strategy pattern, we do not use hard-coded method to configure the strategy, so we do not need to change too much source code, as long as we directly define the two promotional strategy classes of May Day (also inherited from Promotion abstract base class), just like plug-in, plug-and-play.

class BulkItemPromo(Promotion):
    '''
    //If you buy 10 items of a single item, you can get a 9% discount.
    '''
    def discount(self, order):
        discount = 0
        for item in order.cart:
            if item.quantity >= 10:
                discount += item.total() * 0.1
        return discount

class LargeOrderPromo(Promotion):
    '''
    //If the total order amount is greater than or equal to 500, a reduction of 50 can be made.
    '''
    def discount(self, order):
        discount = 0
        if order.total() >= 500:
            discount = 50

        return discount

Seeing that the activity of the mall was so powerful, Xiao Ming's wallet also began to drum up and began to set up daily necessities.

If the first strategy is used, the original price is 600, and it only costs 580.

>>> from collections import namedtuple
>>> Customer = namedtuple('Customer', 'name fidelity')

>>> xm = Customer('Xiao Ming', 300)

>>> item1 = Item('Tissue', 20, 10)
>>> item2 = Item('Edible oil', 50, 4)
>>> item3 = Item('milk', 50, 4)

>>> order = Order(xm, BulkItemPromo())
>>> order.add_to_cart(item1, item2, item3)

>>> order
<Order Total:600 due:580.0>

If the second strategy is used, the original price is 600, and it only costs 550.

>>> from collections import namedtuple
>>> Customer = namedtuple('Customer', 'name fidelity')

>>> xm = Customer('Xiao Ming', 300)

>>> item1 = Item('Tissue', 20, 10)
>>> item2 = Item('Edible oil', 50, 4)
>>> item3 = Item('milk', 50, 4)

>>> order = Order(xm, LargeOrderPromo())
>>> order.add_to_cart(item1, item2, item3)

>>> order
<Order Total:600 due:550>

The two strategies are plug-and-play, only need to select the corresponding strategy when ordering in the front desk, and the original business logic need not be changed.

>>> order = Order(xm, BulkItemPromo())
>>> order = Order(xm, LargeOrderPromo())

But the problem soon came again. The mall was engaged in activities, but let the customers choose which preferential strategy to use manually. As a conscientious businessman, it should be able to automatically compare all strategies to get the best price to the customers. This requires background code to be able to find all the strategies currently available and compare discounts one by one.

# Find out all the promotional strategies
all_promotion = [globals()[name] for name in globals() if name.endswith('Promo') and name != 'BestPromo']

# Implementing an Optimal Policy Class
class BestPromo(Promotion):
    def discount(self, order):
        # Find out all the strategies in the current file
        all_promotion = [globals()[name] for name in globals() if name.endswith('Promo') and name != 'BestPromo']

        # Calculate the maximum discount
        return max([promo().discount(order) for promo in all_promotion])

When you place an order at the front desk, you will automatically calculate all the preferential policies and directly tell the customers the cheapest price.

# Direct selection of this optimal strategy
>>> order = Order(xm, BestPromo())
>>> order.add_to_cart(item1, item2, item3)

>>> order
<Order Total:600 due:550>

From the above examples, we can summarize the benefits of using policy patterns.

  1. Excellent expansibility, convenient transplantation and flexible use. It is easy to extend the strategy.
  2. Each strategy can be switched freely. This is also one of the benefits of relying on abstract classes to design interfaces.

But at the same time, the strategy model will bring some drawbacks.

  1. When the project is large, there may be many strategies, which are not easy to maintain.
  2. The use of strategies must know which strategies to use in order to decide which strategy to use, which is contrary to Dimiter's rule.

For the above examples, consider carefully, in fact, there are many places to optimize.

For example, in order to implement classical patterns, we first define an abstract base class and then implement specific policy classes. For such a simple discount pricing logic as the above, it can be implemented by functions, and then specify the policy function when instantiating the Order class, which is not necessary to move out of the class. In this way, we can avoid creating policy objects constantly while placing orders, and reduce excessive runtime consumption. I won't write the code here.

Therefore, learning design patterns, not only to know how to use such patterns to organize code, but also to understand its ideas, learn flexibly, flexible.

Above all, is today about the strategy model of some personal sharing, if there is no place to speak, but also please leave a message to correct!

Reference document

  • Fluent Python

Posted by ryans18 on Mon, 22 Apr 2019 09:57:33 -0700