第4章 装饰模式

4.1 从生活中领悟装饰模式

4.1.1 故事剧情—你想怎么搭就怎么搭

Tony因为换工作而搬了一次家!这是一个4室1厅1卫1厨的户型,住了4户人家。恰巧这里住的都是年轻人,有男孩也有女孩,而 Tony就是在这里遇上了自己喜欢的人,她叫Jenny。Tony和Jenny每天低头不见抬头见,但Tony是一个程序员,天生不善言辞,不懂着装,老被Jenny嫌弃:满脸猥琐,一副邋遢样!

被嫌弃后,Tony痛定思痛:一定要改善一下自己的形象!于是叫上自己的死党Henry一起去了五彩城……

Tony在这个大商城中兜兜转转,被各个商家教化着该怎样搭配衣服:衬衫要套在腰带里面,风衣不要系纽扣,领子要立起来……

在反复试穿了一个晚上的衣服之后,Tony 终于找到一套还算凑合的着装:下面是一条卡其色休闲裤配一双深色休闲皮鞋,加一条银色针扣头的黑色腰带;上面是一件紫红色针织毛衣,内套一件白色衬衫;头上戴一副方形黑框眼镜。整体着装虽不潮流,却透露出一种工作人士的成熟、稳健和大气!

4.1.2 用程序来模拟生活

服装店里的衣服品类齐全,款式多样,但不同品味的人会搭配出完全不同的风格。Tony是一个程序员,给自己搭配了一套着装,但类似的着装也可以穿在其他人身上,比如一个老师也可以这样穿。下面我们就用程序来模拟这样一个情景。

源码示例4-1 模拟故事剧情

测试代码:

上面的测试代码中decorateTeacher=GlassesDecorator(WhiteShirtDecorator(LeatherShoesDecorator (Teacher("wells","教授"))))这个写法,大家不要觉得奇怪,它其实就是将多个对象的创建过程合在了一起,是一种优雅的写法。创建的Teacher对象通过参数传给LeatherShoesDecorator的构造函数,而创建的LeatherShoesDecorator对象又通过参数传给WhiteShirtDecorator的构造函数,依此类推……

输出结果:

4.2 从剧情中思考装饰模式

4.2.1 什么是装饰模式

Attach additional responsibilities to an object dynamically.Decorators provide a flexible alternative to subclassing for extending functionality.

动态地给一个对象增加一些额外的职责,就拓展对象功能来说,装饰模式比生成子类的方式更为灵活。

就故事剧情中这个示例来说,由结构庞大的子类继承关系(如图4-1所示)转换成了结构紧凑的装饰关系(如图4-2所示)。

图4-1 继承关系

图4-2 装饰关系

4.2.2 装饰模式设计思想

在故事剧情中,Tony为了改善自己的形象,换了整体着装,改变了自己的气质,使自己看起来不再是那个猥琐的邋遢样。俗话说一个人帅不帅,三分看长相,七分看打扮。同一个人,不一样的着装,会给人完全不一样的感觉。我们可以任意搭配不同的衣服、围巾、裤子、鞋子、眼镜、帽子以达到不同的效果。

在这个追求个性与自由的时代,穿着的风格可谓是开放到了极致,真是你想怎么搭就怎么搭!如果你去参加一个正式会议或演讲,可以穿一套标配西服;如果你去大草原,想骑着骏马驰骋天地,便该穿上马服、马裤、马鞋;如果你是漫迷,去参加动漫节,亦可穿上cosplay的衣服,让自己成为那个内心向往的主角……

这样一个时时刻刻发生在我们生活中的着装问题,就是程序中装饰模式的典型样例。在程序中,我们希望动态地给一个类增加额外的功能,而不改动原有的代码,就可用装饰模式来进行拓展。

4.3 装饰模式的模型抽象

4.3.1 类图

装饰模式的类图如图4-3所示。

图4-3 装饰模式的类图

图4-3中的Component是一个抽象类,代表具有某种功能(function)的组件,ComponentImplA和ComponentImplB分别是其具体的实现子类。Decorator是Component的装饰器,里面有一个Component的对象decorated,这就是被装饰的对象,装饰器可为被装饰对象添加额外的功能或行为(addBehavior)。DecoratorImplA和DecoratorImplB分别是两个具体的装饰器(实现子类)。

这样一种模式很好地将装饰器与被装饰的对象进行了解耦。

4.3.2 Python中的装饰器

在Python中一切都是对象:一个实例是一个对象,一个函数也是一个对象,甚至类本身也是一个对象。在Python中,可以将一个函数作为参数传递给另一个函数,也可以将一个类作为参数传递给一个函数。

1.Python中函数的特殊功能

在 Python 中,函数可以作为一个参数传递给另一个函数,也可以在函数中返回一个函数,还可以在函数内部再定义函数。这是Python和很多静态语言不同的地方,这一特性给它带来了很多新奇的功能。

源码示例4-2 函数的特殊功能

输出结果如下:

上面的调用代码等同于:

2.装饰器修饰函数

装饰器的作用:包装一个函数,并改变(拓展)它的行为。

我们以一个场景为例,看一下Python中装饰器是如何实现的。假设有这样一个需求:我们希望每一个函数在被调用之前和被调用之后,记录一条日志。

源码示例4-3 定义装饰器

输出结果:

我们在loggingDecorator中定义了一个内部函数wrapperLogging,用于在传入的函数中执行前后记录日志,一般称这个函数为包装函数,并在最后返回这个函数。我们称loggingDecorator为装饰器,定义这个装饰器函数之后,就可以将其应用于所有希望记录日志的函数,比如下面这样一个函数:

输出结果:

有没有发现,我们每次调用一个函数,都要写两行代码。这是非常繁琐的,Python有没有更简单的方式,让我们的代码更简洁一些呢?答案是肯定的,那就是@decorator语法,如下所示:

@loggingDecorator 表示用loggingDecorator装饰器来修饰showMin函数,它的功能与下面代码的作用是相同的,但调用时,只需要写一行代码,和调用一般函数是一样的。

3.装饰器修饰类

装饰器可以是一个函数,也可以是一个类(必须要实现__call__方法,使其是callable的)。同时装饰器不仅可以修改一个函数,还可以修饰一个类,示例如下。

源码示例4-4 修饰类的装饰器

输出结果:

这里 ClassDecorator 是类装饰器,记录一个类被实例化的次数。其修饰一个类和修饰一个函数的用法是一样的,只需在定义类时 @ClassDecorator即可。

4.3.3 模型说明

1.设计要点

(1)可灵活地给一个对象增加职责或拓展功能。你可任意地穿上自己想穿的衣服。不管穿上什么衣服,你还是那个你,但穿上不同的衣服你就会有不同的外表。

(2)可增加任意多个装饰 你可以只穿一件衣服,也可以只穿一条裤子,也可以衣服和裤子搭配着穿,随你意!

(3)装饰的顺序不同,可能产生不同的效果。在上面的示例中,Tony把针织毛衣穿在外面,白色衬衫穿在里面。当然,如果你愿意(或因为怕冷),也可以把针织毛衣穿在里面,白色衬衫穿在外面。但两种着装穿出来的效果、给人的感觉肯定是完全不一样的。

使用装饰模式时,想要改变装饰的顺序,也是非常简单的。只要把测试代码稍微改动一下即可,如下所示:

输出结果如下:

2.装饰模式的优缺点

优点:

(1)使用装饰模式来实现扩展比使用继承更加灵活,它可以在不创造更多子类的情况下,将对象的功能加以扩展。

(2)可以动态地给一个对象附加更多的功能。

(3)可以用不同的装饰器进行多重装饰,装饰的顺序不同,可能产生不同的效果。

(4)装饰类和被装饰类可以独立发展,不会相互耦合;装饰模式相当于继承的一个替代模式。

缺点:

与继承相比,用装饰的方式拓展功能容易出错,排错也更困难。对于多次装饰的对象,调试寻找错误时可能需要逐级排查,较为烦琐。

3.Python装饰器与装饰模式的区别与联系

在“4.3.2 Python中的装饰器”一节中讲了Python中装饰器的原理和用法,它与我们在这一章讲的装饰模式的设计模式有什么区别呢?二者的区别如表4-1所示。

表4-1 Python装饰器与装饰模式的区别

二者的联系是,设计的思想相似,即要达到的目标是相似的:更好的拓展性,以及在不需要做太多代码变动的前提下,增加额外的功能。

4.4 应用场景

(1)有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长时。

(2)需要动态地增加或撤销功能时。

(3)不能采用生成子类的方法进行扩充时,类的定义不能用于生成子类(如Java中的final类)。

装饰模式的应用场景非常广泛。如在实际项目开发中经常看到的过滤器,便可用装饰模式的方式实现。如果你是Java程序员,那么你对I/O中的FilterInputStream和FilterOutputStream一定不陌生,它的实现其实就是一个装饰模式。FilterInputStream(FilterOutputStream)就是一个装饰器,而InputStream(OutputStream)就是被装饰的对象。我们看一下创建对象的过程:

这个写法与上面Demo中的decorateTeacher=GlassesDecorator(WhiteShirtDecorator(LeatherShoesDecorator(Teacher("wells","教授"))))是不是很相似?它们都是用一个对象套一个对象的方式进行创建的。