Python装饰器模式

标签: python|设计模式|装饰器模式


引子

对于装饰器模式我正在一点一点的理解........
使用对象组合的方式,做到在运行时装饰对象,使用这种方式,将能够在不修改任何底层代码的情况下,给对象赋予新的职责

想当然的方法

以星巴兹咖啡举例子,星巴兹有几种固定种类的咖啡,种类及价格见下表

种类 价格
HouseBlend 1.99
DarkRoast 1.79
Decaf 2.99
Espreso 1.39

他们共同继承自一个Beverage的抽象类

UML1.PNG-22.8kB
UML1.PNG-22.8kB

这种时候,可以使用下面的代码将他处理的很好,毕竟也就是几种咖啡罢了

#父类
class Beverage(object):
    def __init__(self):
        self.description = "Unknown description"
    def getDescription(self):
        return self.description
    def cost(self):
        pass
#子类
class HouseBlend(Beverage):
    def __init__(self):
        super(HouseBlend, self).__init__()
        self.description = "HouseBlend coffee"
    def cost(self):
        return 1.99
#子类
class DarkRoast(Beverage):
    def __init__(self):
        super(DarkRoast, self).__init__()
        self.description = "DarkRoast coffee"
    def cost(self):
        return 1.79
#子类
class Decaf(Beverage):
    def __init__(self):
        super(Decaf, self).__init__()
        self.description = "Decaf coffee"
    def cost(self):
        return 2.99
#子类
class Espresso(Beverage):
    def __init__(self):
        super(Espresso, self).__init__()
        self.description = "Espresso coffee"
    def cost(self):
        return 1.39
#实例
for item in [HouseBlend(), DarkRoast(), Decaf(), Espresso()]:
    print("{0}: {1}".format(item.getDescription(), item.cost()))

一共是五个类,打印的结果如下

HouseBlend coffee: 1.99
DarkRoast coffee: 1.79
Decaf coffee: 2.99
Espresso coffee: 1.39

当购买咖啡时,顾客要求添加各种各样的调料,例如:奶,糖,豆浆,摩卡,奶泡........
星巴兹会根据添加调料的不同,重新计算最终的价格,所以订单系统需要考虑到这些调料部分,这个时候怎么办,继续增加类,无穷无尽的类,哪天牛奶要是价格涨价了,那我就需要将所有涉及牛奶的子类价格全部更新一遍,那这一天就是粘贴复制了


UML2.PNG-35.6kB
UML2.PNG-35.6kB
class Beverage(object):
    def __init__(self):
        self.description = "Unknow description"
    def getDescription(self):
        return self.description
    def cost(self):
        pass
class HouseBlend(Beverage):
    def __init__(self):
        super(HouseBlend, self).__init__()
        self.description = "HouseBlend coffee"
    def cost(self):
        return 1.99
#我需要在下面定义无穷无尽的子类
......
......
......

这样弄下类,也许会有成百上千各类。

怎么办

可以将所有的调料放到父类里面,Beverage里面计算每种调料的价格,子类最后将自己的价格加到这些增加的调料价格上,这样的话好像还是五个类

class Beverage(object):
    #定义两个类属性,作为初始值
    condiment = 0.0
    description = ""
    #定义一个dict,用来存放调料
    def __init__(self, **condiments):
        for k, v in condiments.items():
            setattr(self, k , v)
            #下面是判断哪些调料使用了,之后加上这种调料的价格和描述
            if condiments[k] == "milk":
                Beverage.condiment += 0.99
                Beverage.description += " add milk"
            if condiments[k] == "soy":
                Beverage.condiment += 0.89
                Beverage.description += " add soy"
            #下面可以不停的增加调料种类
    def getDescription(self):
        return self.description + Beverage.description
#这里只写了一种咖啡种类,其余三种格式全部一致,这里省略了
class HouseBlend(Beverage):
    def __init__(self, **condiments):
        super(HouseBlend, self).__init__(**condiments)
        self.description = "HouseBlend Coffee "
    #cost现在就为调料与本身的价格之和
    def cost(self):
        return Beverage.condiment + 1.99
#这个实例里增加了milk调料,打印最终的价格和描述
hb = HouseBlend(condi1="milk", condi2="soy", condi3="soy")
print("{0}: {1}".format(hb.getDescription(), hb.cost()))

这么设计看似解决了类爆炸的问题,但是它违反了一个原则,这个原则就是

类应该对扩展开放,对修改关闭

此处,如果有新的调料加入,就必须去对Beverage类进行修改,这样做似乎违反了面向对象界的一些规则,但我个人觉得没有什么,我不是写代码的,不知道这个原则到了大型程序上是不是后果很严重,不过想想写出的类一增加功能,就需要去原来的基础上修改代码,确实不利于后续维护,扩展也许是最好的选择。

新的目标

这样看来有了新的目标,就是将上面的代码改成扩展的,而不是修改的,这样只需要每次写一个新的调料放上去就行。
答案就是装饰器模式,每一种调料都是一个个装饰材料,而四种咖啡是被装饰对象,需要增加哪种调料,就把这个调料的装饰花环套在咖啡的脖子上,直到咖啡已经喝不了为止。
定义

动态地将责任附加到对象上,若有扩展功能,装饰者提供了比继承更具有弹性的替代方案

先看Beverage类,他是所有类的基类,定义了getDescription和cost方法

class Beverage(object):
    def __init__(self):
        self.description = "Unknown Beverage"
    def getDescription(self):
        return self.description
    def cost(self):
        pass

定义四个咖啡组件,这就是要被装饰者装饰的被装饰者,在这里定义了两个,其余两个格式都是一致的,只要更改一下description的内容和价格即可

class HouseBlend(Beverage):
    def __init__(self):
        super(HouseBlend, self).__init__()
        self.description = "HouseBlend"
    def cost(self):
        return 1.99
class Espresso(Beverage):
    def __init__(self):
        super(Espresso, self).__init__()
        self.description = "Espresso"
    def cost(self):
        return 1.39
    def getDescription(self):
        return self.description

接下来定义装饰者类,他们用来装饰被装饰者,在这里自然就是一堆堆的调料了

#这个类是装饰者的类的基类,其实没有也无所谓,因为所有的类都是`Beverage`类型
class CondimentDecorator(Beverage):
    def getDescription(self):
        pass
#这是调料milk
class Milk(CondimentDecorator):
    #这里传入被装饰者,可以是咖啡组件,也可以是已经被mocha或其他调料装饰过的咖啡组件,主要
    #看装饰者在装饰过程中的位置
    def __init__(self, beverage):
        super(Milk, self).__init__()
        self.beverage = beverage
    def getDescription(self):
        return self.beverage.getDescription() + ", Milk"
    def cost(self):
        return self.beverage.cost() + .2
#这是调料mocha
class Mocha(CondimentDecorator):
    #这里传入被装饰者,可以是咖啡组件,也可以是已经被milk或其他调料装饰过的咖啡组件,主要
    #看装饰者在装饰过程中的位置
    def __init__(self, beverage):
        super(Mocha, self).__init__()
        self.beverage = beverage
    #这里自然是之前的组件描述加上这种调料的描述
    def getDescription(self):
        return self.beverage.getDescription() + ", Mocha"
    #那这里自然也就是之前的组件的价格和这种调料的价格了
    def cost(self):
        return self.beverage.cost() + .3
#这后面你可以增加无穷无尽的调料

实现

#先建立一个咖啡组件的实例
beverage = HouseBlend()
#先被milk装饰一遍
beverage = Milk(beverage)
#再被mocha装饰一遍
beverage = Mocha(beverage)
#后面可以接着写,被其他别的调料接着装饰

print("{0}: {1}".format(beverage.getDescription(), "%.3f" % beverage.cost()))

输出看看

HouseBlend, Milk, Mocha: 2.490

现在再来看看下面这张图,应该理解的深刻一些


2016-02-24_191915.png-57.5kB
2016-02-24_191915.png-57.5kB

回过头来看这些代码,好像变多了,但是解决了之前那个问题,如果有新的调料加入,不需要更改任何代码,只要增加就可以了,实现了扩展但不改变。

想来个大杯或者小杯怎么办

大杯和小杯的调料不能一个价格,当然我希望如此,那么接下来怎么改进代码,其实只要在基类里面增加一个跟size有关系的方法就行

class Beverage(object):
    def __init__(self):
        self.description = "Unknown Beverage"
    def getDescription(self):
        return self.description
    def cost(self):
        pass
    def getSize(self):
        return self.size
    #设置一下杯子尺寸
    def setSize(self, cupSize):
        self.size = cupSize

之后将装饰者和被装饰者里面cost方法上增加一些判断即可,毕竟对价格产生直接影响
被装饰者

class HouseBlend(Beverage):
    def __init__(self):
        super(HouseBlend, self).__init__()
        self.description = "HouseBlend"
    def cost(self):
        if self.getSize() == "TALL":
            return 1.99
        elif self.getSize() == "GRANDE":
            return 2.99
        elif self.getSize() == "VENTI":
            return 3.99

装饰者

class Milk(CondimentDecorator):
    def __init__(self, beverage):
        super(Milk, self).__init__()
        self.beverage = beverage
    def getDescription(self):
        return self.beverage.getDescription() + ", Milk"
    def cost(self):
        if self.beverage.getSize() == "TALL":
            return self.beverage.cost() + .2
        elif self.beverage.getSize() == "GRANDE":
            return self.beverage.cost() + .25
        elif self.beverage.getSize() == "VENTI":
            return self.beverage.cost() + .30

结尾

python有一个装饰器功能,作用和这个模式是一样的,用这种方法实现应该更直接一些,但我还不会。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容