Skip to content

Latest commit

 

History

History
261 lines (175 loc) · 8.48 KB

chapter6-design-patterns-with-first-class-functions.md

File metadata and controls

261 lines (175 loc) · 8.48 KB

Chapter6. Design Patterns with First-Class Functions

[TOC]

当我们引入了 函数作为第一公民 的概念后,Peter Norvig 建议我们重新思考的几个设计模式为:

English Chinese
Strategy Pattern (policy pattern) 策略模式
Command Pattern 命令模式
Template Method Pattern 模板方法模式
Visitor Pattern 访问者模式

在以上四种设计模式中,function 可以被用来替代某些类的实例。

Design Patterns

函数作为第一公民后,其在某种程度上,就可以替代 class,函数和 class 同样重要

Classic Strategy Pattern

Introduction

策略模式

当一个事情根据不同的情况,有多种方案时,例如:

  1. 超市的折扣针对不同的 Context,有不同的打折方案
  2. 携程曾经针对不同的用户,有不同的付款策略,对于老顾客杀熟
  3. 饿了吗定外卖的时候,有多种不同的支付方式

那么就可以联想到 Strategy Pattern。Strategy Pattern 的本质:具体问题具体分析

Classic Implement

经典的 Strategy Pattern 实现。从这个示例代码中,我们可以学到很多知识。

# -*- coding: utf-8 -*-

from collections import namedtuple
from abc import ABC, abstractmethod

Customer = namedtuple("Customer", "name fidelity") # 没有成员方法,可以直接用 namedtuple 来包装


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: # the Context
    """订单"""
    def __init__(self, customer, cart, promotion=None): # promotion 是可选参数,默认为 None
        self.customer = customer
        self.cart = 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): # print(order)
        return "<Order tota:{:.2f} due:{:.2f}>".format(self.total(), self.due())


class Promotion(ABC): # The Strategy: an abstract base class,抽象基类
    """促销基类,定下契约"""
    @abstractmethod
    def discount(self, order):
        """折扣"""
        pass


class FidelityPromotion(Promotion): # concrete strategy
    """5% discount for customers with 1000 or more fidelity points"""
    def discount(self, order):
        return 0 if order.customer.fidelity < 1000 else order.total() * .05


class BulkItemPromotion(Promotion): # concrete strategy
    """10% discount for each LineItem with 20 or more units"""
    def discount(self, order):
        discount = 0
        for item in order.cart:
            if item.quantity >= 20:
                discount += item.total() * .1 # Python 的小数可以不写 0
        return discount


class LargeOrderPromotion(Promotion): # concrete strategy
    """7% discount for orders with 10 or more distinct items"""
    def discount(self, order):
        discount = 0
        distinct_items = {item.product for item in order.cart}
        if len(distinct_items) >= 10:
            discount = order.total() * .07
        return discount


if __name__ == "__main__":

    joe = Customer("John Doe", 0)
    ann = Customer("Ann Smith", 1100)
    cart = [
        LineItem("banana", 4, .5),
        LineItem("apple", 10, 1.5),
        LineItem("orange", 5, 5.0)
    ]
    order1 = Order(ann, cart, promotion=FidelityPromotion()) # 传入一个策略:类的实例
    print(order1)  # <Order tota:42.00 due:39.90>

Function-Oriented Strategy

现在分析一下上面的面向对象的代码,对于每一种策略,我们都要用一个类将其封装,这个步骤是不是很多余?是不是没有突出重点?我们只是想要创建一个轻量的策略哎

为什么不能直接传入一个策略呢?Python 的函数和 class 一样,都是一等公民啊

重构原理

因为 first-class object 的特性,function 可以和对象一样进行赋值。在很多时候,我们为了调用某个方法,需要实例化包装该方法的类,这是不必要的,有了 first-class function 后,我们不需要将功能性的方法包装在类中再调用,而是可以直接将 function 作为参数传入,A function is more light-weight than an instance of user-defined class。当我们需要实现某个接口的唯一方法时,我们可以用 function 来取代该 Class。

实际上,应用 Frist-class function 这一特性,function 可以重构很多设计模式在 Python 中的实现。Peter Norvig 说过,23 个设计模式中,有 16 个设计模式可以在动态语言中被重构,甚至已经被设计到了编程语言中。

Function-Oriented Strategy Pattern 实现。

# -*- coding: utf-8 -*-

from collections import namedtuple
from operator import itemgetter

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: # the Context
    """订单"""
    def __init__(self, customer, cart, promotion=None): # promotion 是可选参数,默认为 None
        self.customer = customer
        self.cart = 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):
        return "<Order tota:{:.2f} due:{:.2f}>".format(self.total(), self.due())


def fidelity_promotion(order): # concrete strategy
    """5% discount for customers with 1000 or more fidelity points"""
    return 0 if order.customer.fidelity < 1000 else order.total() * .05


def bulk_item_promotion(order): # concrate strategy
    """10% discount for each LineItem with 20 or more units"""
    discount = 0
    for item in order.cart:
        if item.quantity >= 20:
            discount += item.total() * .1
    return discount


def large_order_promotion(order): # concrate strategy
    """7% discount for orders with 10 or more distinct items"""
    discount = 0
    distinct_items = {item.product for item in order.cart}
    if len(distinct_items) >= 10:
        discount = order.total() * .07
    return discount


def get_best_promotion(order, promotions):
    """获取最大的 promotion"""
    promotion2discount = [(promotion, promotion(order)) for promotion in promotions]
    sorted_promotion2discount = sorted(promotion2discount, key=itemgetter(1))
    return sorted_promotion2discount[-1][0]


if __name__ == "__main__":

    joe = Customer("John Doe", 0)
    ann = Customer("Ann Smith", 1100)
    cart = [
        LineItem("banana", 4, .5),
        LineItem("apple", 40, 1.5),
        LineItem("orange", 5, 5.0),
    ]

    existed_promotions = [globals()[name] for name in globals()
                  if name.endswith("_promotion") and not name.endswith("best_promotion")] # 遍历获取所有策略,进而兼容新加入的策略
    order1 = Order(ann, cart)
    order1.promotion = get_best_promotion(order1, existed_promotions)
    print(order1.total())
    print(order1.due())  # <Order tota:42.00 due:39.90>

Further Reading