# coding: utf-8
# # Srategy Pattern
# 策略模式定义一系列算法并封装它们,这些算法可以互换。
# 策略模式还根据不同的对象使用不同的算法。
# ## class implement
# In[10]:
import abc
import collections
Customer = collections.namedtuple('Customer', 'name fidelity')
class LineItem(object):
    def __init__(self, product, quantity, price):
        self.product = product
        self.quantity = quantity
        self.price = price
    
    def total(self):
        return self.quantity * self.price
    
class Order(object):
    def __init__(self, customer, cart, promotion=None):
        self.customer = customer
        self.cart = cart
        self.promotion = promotion
    
    def total(self):
        if not hasattr(self, '__total'):
            self.__total = sum(lineitem.total() for lineitem in self.cart)
        return self.__total
    
    def due(self):
        discount = 0 if self.promotion is None else self.promotion.discount(self)
        return self.total() - discount
    
    def __repr__(self):
        return "<Order total:{:.2f} due:{:.2f}>".format(self.total(), self.due())
class Promotion(abc.ABC):
    """abstract class of Promotion"""
    @abc.abstractmethod
    def discount(self, Order):
        """return the discount vary different Order """
        pass
    
class FidelityPromo(Promotion):
    """if fidelity >= 1000 Give 5% discount"""
    
    def discount(self, order):
        """ return 0.05 of the total price of Order """
        return 0.05*order.total() if order.customer.fidelity >= 100 else 0.0
class BulkItemPromo(Promotion):
    """if the lineitem's quantity >= 20 give the lineitem'total price 10% discount"""
    def discount(self, order):
        discount = sum((0.1*lineitem.total() if lineitem.quantity >= 20 else 0.0 for lineitem in order.cart))
        return discount
        
class LargeOrderPromo(Promotion):
    """7% discount for the kind of lineitems in cart >10 """
    def discount(self, order):
        kinds = {lineitem.product for lineitem in order.cart}
        return 0.07*order.total() if len(kinds)>=10 else 0.0
    
        
    
# In[3]:
joe = Customer('John Doe', 0)
# In[4]:
ann = Customer('Ann Smith', 1100)
# In[5]:
cart = [LineItem('banana', 4, .5),LineItem('apple', 10, 1.5),LineItem('watermellon', 5, 5.0)]
# In[11]:
Order(joe, cart, FidelityPromo()) # joe's fidelity < 1000
# Out[11]: <Order total:42.00 due:42.00>
# In[12]:
Order(ann, cart, FidelityPromo()) # ann's fidelity > 1000
# Out[12]: <Order total:42.00 due:39.90>
# In[13]:
banana_cart = [LineItem('banana', 30, .5),LineItem('apple', 10, 1.5)]
# In[14]:
Order(joe, banana_cart, BulkItemPromo()) # number of 'banana' > 20
# Out[14]: <Order total:30.00 due:28.50>
# In[15]:
long_order = [LineItem(str(item_code), 1, 1.0) for item_code in range(10)]
# In[16]:
Order(joe, long_order, LargeOrderPromo()) # number of line items >=10
# Out[16]: <Order total:10.00 due:9.30>
# In[17]:
Order(joe, cart, LargeOrderPromo())
# Out[17]: <Order total:42.00 due:42.00>
# ## function implement
# function is the first class in python.
# considering the subclasses of Promotion are very simple, i think implementation them as function is better.
# the defination of Customer, LineItem and Oder is no need to change.
# but change the due method in Oder is nesscery.
# In[18]:
import collections
Customer = collections.namedtuple('Customer', 'name fidelity')
class LineItem(object):
    def __init__(self, product, quantity, price):
        self.product = product
        self.quantity = quantity
        self.price = price
    
    def total(self):
        return self.quantity * self.price
    
class Order(object):
    def __init__(self, customer, cart, promotion=None):
        self.customer = customer
        self.cart = cart
        self.promotion = promotion
    
    def total(self):
        if not hasattr(self, '__total'):
            self.__total = sum(lineitem.total() for lineitem in self.cart)
        return self.__total
    
    def due(self):
        discount = 0 if self.promotion is None else self.promotion(self)
        return self.total() - discount
    
    def __repr__(self):
        return "<Order total:{:.2f} due:{:.2f}>".format(self.total(), self.due())
def fidelity_promo(order):
    return 0.05*order.total() if order.customer.fidelity >= 100 else 0.0
def bulk_promo(order):
    return sum((0.1*lineitem.total() if lineitem.quantity >= 20 else 0.0 for lineitem in order.cart))
def large_order_promo(order):
    kinds = {lineitem.product for lineitem in order.cart}
    return 0.07*order.total() if len(kinds)>=10 else 0.0
# In[19]:
joe = Customer('John Doe', 0)
ann = Customer('Ann Smith', 1100)
cart = [LineItem('banana', 4, .5),LineItem('apple', 10, 1.5),LineItem('watermellon', 5, 5.0)]
# In[20]:
Order(joe, cart, fidelity_promo) # joe's fidelity < 1000
# Out[20]: <Order total:42.00 due:42.00>
# In[21]:
Order(ann, cart, fidelity_promo) # ann's fidelity > 1000
# Out[21]:<Order total:42.00 due:39.90>
# In[22]:
banana_cart = [LineItem('banana', 30, .5),LineItem('apple', 10, 1.5)]
# In[24]:
Order(joe, banana_cart, bulk_promo) # number of 'banana' > 20
# Out[24]: <Order total:30.00 due:28.50>
# In[25]:
long_order = [LineItem(str(item_code), 1, 1.0) for item_code in range(10)]
# In[27]:
Order(joe, long_order, large_order_promo) # number of line items >=10
# Out[27]:<Order total:10.00 due:9.30>
# In[28]:
Order(joe, cart, large_order_promo)
# Out[28]: <Order total:42.00 due:42.00>
# ##  what is the best promotion
# In[31]:
promos = [fidelity_promo, bulk_promo, large_order_promo]
def best_promo(order):
    return max(promo(order) for promo in promos)
# In[32]:
Order(joe, long_order, best_promo)
# Out[32]:<Order total:10.00 due:9.30>
# In[33]:
Order(joe, banana_cart, best_promo)
# Out[33]: <Order total:30.00 due:28.50>
# In[34]:
Order(ann, cart, best_promo)
# Out[34]:<Order total:42.00 due:39.90>
# there is another way to find the all promotions in a module.
# use globals()
# globals() return a dict of all parameters: variable, function , class in current module.
# In[35]:
promos = [globals()[name] for name in globals() if name.endswith('_promo') and name != 'best_promo']
# also inspect module can inpect a module.
# for example, there is a module named promotions:
# In[ ]:
promos = [func for name, func in inspect.getmembers(promotions, inspect.isfunction)]