Design Patterns with First-Class Functions

Although design patterns are language-independent, that does not mean every pattern applies to every language.   

If we assumed procedural languages, we might have included design patterns called "Inheritance", "Encapsulation" and "Polymorphism". 

In particular, in the context of languages with first-class functions, Norvig suggests rethinking the Strategy, Command, Template Method, and Visitor patterns. The general idea is: you can replace instances of some participant class in these patterns with simple functions, reducing a lot of boilerplate code.

 

1. Case Study: Refactoring Strategy.

The Strategy pattern is summarized like this in Design Patterns:

# Define a family of algorithms, encapsulate each one, and make them interchangeable.  Strategy lets the algorithm vary independently from clients that use it.

The participants of Strategy pattern are:

(1).  Context; (2). Strategy ; (3). Concrete Strategy

 

1.1 Classic Strategy

Example 6-1. Implementation Order class with pluggable discount strategies.

"""
1. Customers with 1000 or more fidelity points get a a global 5% discount per order.
2. A 10% discount is applied to each line item with 20 or more units in the same order.
3. Orders with at least 10 distinct items get a 7% global discount.
"""

from abc import ABC, abstractmethod
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:  # the Context

    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 = '<Order total: {:.2f} due: {:.2f}>'
        return fmt.format(self.total(), self.due())


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

    @abstractmethod
    def discount(self, order):
        """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):
        return order.total() * .05 if order.customer.fidelity >= 1000 else 0


class BulkItemPromo(Promotion):  # second 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
        return discount


class LargeOrderPromo(Promotion):
    """7% discount for orders with 10 or more distinct items"""

    def discount(self, order):
        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, .5),
...         LineItem('apple', 10, 1.5),
...         LineItem('watermelon', 5, 5.0)]
>>> 
>>> Order(joe, cart, FidelityPromo())
<Order total: 42.00 due: 42.00>
>>> Order(ann, cart, FidelityPromo())
<Order total: 42.00 due: 39.90>
>>> banana_cart = [LineItem('banana', 30, .5),
...                LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, BulkItemPromo())
<Order total: 30.00 due: 28.50>
>>> long_order = [LineItem(str(item_code), 1, 1.0)
...               for item_code in range(10)]
>>> Order(joe, long_order, LargeOrderPromo())
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, LargeOrderPromo())
<Order total: 42.00 due: 42.00>
"""

"""
This example 6-1 works perfectly well, but the same functionality can be implemented with less code in Python by using
functions as objects.
"""

 

1.2 Function-Oriented Strategy

Each concrete strategy in Example 6-1 is a class with a single method, discount. Furthermore, the strategy instances have no state(no instance attributes). You could say they look a lot like plain functions, and you would be right. Example 6-3 is a refactoring of Example 6-1, replacing the concrete strategies with simple functions and removing the Promo abstract class.

Example 6-3. Order class with discount strategies implemented as functions.

"""
This example is a refactoring of Example 6-1, replacing the concrete strategies with simple functions and removing the
Promo abstract class.
"""

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:  # the Context

    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)  # To compute a discount, just call the self.promotion() function.
        return self.total() - discount

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


def fidelity_promo(order):  # Each strategy is a function.
    """5% discount for customers with 1000 or more fidelity points"""

    return order.total() * .05 if order.customer.fidelity >= 1000 else 0


def bulk_item_promo(order):
    """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_promo(order):
    """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() * .07
    return 0


# 运行结果:
"""
>>> joe = Customer('John Doe', 0)
>>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, .5),
...         LineItem('apple', 10, 1.5),
...         LineItem('watermelon', 5, 5.0)]
>>> 
>>> Order(joe, cart, fidelity_promo)
<Order total: 42.00 due: 42.00>
>>> Order(ann, cart, fidelity_promo)
<Order total: 42.00 due: 39.90>
>>> banana_cart = [LineItem('banana', 30, .5),
...                LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo)
<Order total: 30.00 due: 28.50>
>>> long_order = [LineItem(str(item_code), 1, 1.0)
...               for item_code in range(10)]
>>> Order(joe, long_order, large_order_promo)
<Order total: 10.00 due: 9.30>
>>> Order(joe, cart, large_order_promo)
<Order total: 42.00 due: 42.00>
"""

 

1.3 Choosing the Best Strategy: Simple Approach

Example 6-6. best_promo finds the maximum discount iterating over a list of functions.

from ex6_3_order_class_with_discount_strategies_as_functions import *

promos = [fidelity_promo, bulk_item_promo, large_order_promo]  # promos: list of the strategies implemented as functions


def best_promo(order):  # best_promo takes an instance of Order as argument, as do the *_promo functions.
    """Select best discount available
    """
    return max(promo(order) for promo in promos)
    # Using a generator expression, we apply each of the functions from promos to the order,
    # and return the maximum discount computed


# 运行结果:
"""
>>> joe = Customer('John Doe', 0)
>>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, .5),
...         LineItem('apple', 10, 1.5),
...         LineItem('watermelon', 5, 5.0)]
>>> long_order = [LineItem(str(item_code), 1, 1.0)
...               for item_code in range(10)]
>>> banana_cart = [LineItem('banana', 30, .5),
...                LineItem('apple', 10, 1.5)]
>>> Order(joe, long_order, best_promo)
<Order total: 10.00 due: 9.30>
>>> Order(joe, banana_cart, best_promo)
<Order total: 30.00 due: 28.50>
>>> Order(ann, cart, best_promo)
<Order total: 42.00 due: 39.90>
"""

 

Example 6-7. The promos list is built by introspection of the module global namespace.

"""
This example is a somewhat hackish way of using globals to help best_promo automatically find the other available
*_promo functions.
"""

"""
Modules in Python are also first-class objects, and the standard library provides several functions to handle them.
The built-in globals() is described as follows in the Python docs:
globals()
    Return a dictionary representing the current global symbol table. This is always the dictionary of the current
    module(inside a function or method, this the module where it is defined,not the module from which it is called.)
"""

from ex6_3_order_class_with_discount_strategies_as_functions import *


promos = [globals()[name] for name in globals()     # Iterate over each name in the dictionary returned by globals()
          if name.endswith('_promo')    # Select only names that end with the _promo suffix
          and name != 'best_promo']     # Filter out best_promo itself, to avoid an infinite recursion.


def best_promo(order):
    """Select best discount available
    """
    return max(promo(order) for promo in promos)

注: Example 6-7 例子中我是用导入的方式把 *_promo 函数导入到了 best_promo函数所在的py文件中, 但书上的愿意是上述代码 和 *_promo 函数同属一个py文件中。

 

Example 6-8. The promos list is built by introspection of a new promotions module.

"""
Another way of collecting the available promotions would be to create a module and put all the strategies functions
here, except for the best_promo
"""

import inspect
import promotions

promos = [func for name, func  # built by introspection of a separate module called promotions.
          in inspect.getmembers(promotions, inspect.isfunction)]


def best_promo(order):
    """Select best discount available
    """
    return max(promo(order) for promo in promos)


print(promos)
# 打印结果:
# [<function bulk_item_promo at 0x1035efd08>, <function fidelity_promo at 0x1035efc80>, <function large_order_promo at 0x1035efd90>]

"""
The function inspect.getmembers returns the attributes of an object -- in this case, the promotions module -- optionally
filtered by a predicate(a boolean function). We use inspect.isfunction to get only the functions from teh module.
"""

 

2. Command

Example 6-9. Each instance of MacroCommand has an internal list of commands.

class MarcoCommand:
    """A command that execute a list of commands"""

    def __init__(self, commands):
        self.commands = list(commands)
        # Building a list from the commands arguments ensures that is iterable and keeps a local copy of the command
        # references in each MacroCommand instance.

    def __call__(self):
        for command in self.commands:
            command()


"""
The goal of Command is to decouple an object that invokes an operation (the Invoker) from the provider object that 
implements it (the Receiver).
The idea is to put a Command object between the two(Invoker and Receiver), implementing an interface with a single
method, execute, which calls some method in the Receiver to perform the desired operation. That way the Invoker does 
not need to know the interface of the Receiver, and different receivers can be adapted through different Command
subclass. The Invoker is configured with a concrete command and calls its execute method to operate it.

"Commands are an object-oriented replacement for callbacks."

Instead of giving the Invoker a Command instance, we can simply give it a function. Instead of calling 
command.execute(), the Invoker can just call command()
"""

 

Sometimes you may encounter a design pattern or an  API that requires taht components implement an interface with a single method, and that method has a generic-sounding name such as "execute", "run" or "doIt". Such patterns or APIs often can be implemented with less boilerplate code in Python using first-class functions or other callables.

posted @ 2020-02-02 02:16  neozheng  阅读(168)  评论(0编辑  收藏  举报