《Python编程实战:运用设计模式、并发和程序库创建高质量程序》—— 2.1 适配器模式

本节书摘来自华章出版社《Python编程实战:运用设计模式、并发和程序库创建高质量程序》一 书中的第2章,第2.1节,作者:(美) Mark Summerfield,更多章节内容可以访问云栖社区“华章计算机”公众号查看。

2.1 适配器模式

“适配器模式”(Adapter Pattern)是一种接口适配技术,可通过某个类来使用另一个接口与之不兼容的类,运用此模式时,两个类的接口都无须改动。这项技术非常有用,比方说,我们想把某个类从其原先的应用场景中拿出来放在另一个环境下运行,而这个类又不能修改,那就可以考虑适配器模式。
假设有个简单的Page类用于渲染页面,它需要知道标题、正文段落以及“渲染器类”(renderer class)的实例。(本节代码均选自render1.py范例程序。)
《Python编程实战:运用设计模式、并发和程序库创建高质量程序》—— 2.1 适配器模式
《Python编程实战:运用设计模式、并发和程序库创建高质量程序》—— 2.1 适配器模式

Page类并不知道也无须关心传进来的渲染器类实例具体是什么,它只要知道渲染器提供了渲染页面所需的接口就好,也就是说,渲染器类应该有三个方法:header(str)、paragraph(str)、footer()。
在本例中,我们需要保证__init__()接收到的renderer参数确实是个Renderer实例。有一种简单但是很糟糕的办法,就是用assert isinstance(renderer, Renderer)语句来判断。这么做有两个缺陷。首先,它抛出的是AssertionError,而不是我们所期望的TypeError,后者更为具体。其次,假如运行程序时指定了-o选项(“optimize”,优化),那么assert语句就不会执行,而稍后执行render()方法时,将会导致AttributeError异常。范例代码中的if not isinstance(...)语句则没有这两个问题,它可以抛出TypeError异常,而且在加了-o选项后依然能正确运行。
但这种写法也有个明显的问题,那就是所有渲染器子类似乎都必须继承自Renderer基类。假如用C++语言来编程,那确实如此,而在Python语言里也是可以创建这种基类的。不过,Python的abc(abstract base class,抽象基类)模块提供了另一种做法,既能像抽象基类那样检查接口是否匹配,又能像“动态类型”(duck typing)那样非常灵活。这就是说,我们可以在无须继承特定基类的前提下,创建出符合某套接口(也就是具备特定API)的对象来。
《Python编程实战:运用设计模式、并发和程序库创建高质量程序》—— 2.1 适配器模式

Renderer类重新实现了__subclasshook__()这个“特殊方法”(special method)。Python语言内置的isinstance()函数要通过此方法来决定函数的首个参数是不是第二个参数的子类(如果第二个参数是由类所构成的元组,那就判断首个参数是不是元组中某个类的子类)。
上面这段代码有些棘手,它必须在Python 3.3及之后的版本上才能运行,因为其中用到了collections.ChainMap类。这段代码的原理稍后解释,但就算不明白也无关紧要,因为这些复杂的操作都可以通过范例代码中的@Qtrac.has_methods“类装饰器”(class decorator)来完成,2.2节将会演示其用法。
__subclasshook__()特殊方法首先通过Class参数判断该自己是不是在Renderer类上面调用的,如果不是,就返回NotImplemented。这么做意味着子类无法继承__subclasshook__()的行为。由于我们假定子类要在抽象基类的基础上添加新的标准而不是新的行为,所以才设计成这样。若想继承__subclasshook__()的行为也可以,只要在重新实现__subclasshook__()的时候调用Renderer.__subclasshook__()就可以了。
此方法如果返回True或False,那么isinstance()的判定流程就会在这个抽象基类处终止,并返回bool值。若返回NotImplemented,则会沿着继承体系按照通常的规则继续判定下去(判断Subclass是不是本类的子类、是不是“显式注册类”(explicitly registered class)的子类、是不是子类的子类)。
如果满足了if语句的判断条件,那就调用Subclass的__mro__()特殊方法,并遍历Subclass及所有超类(包括Subclass本身)的私有字典(也就是__dict__)。遍历好的字典会放在元组中,我们通过序列解包操作(*)将其传给collections.ChainMap()函数。此函数会创建一份Map视图,把它从参数中收到的所有映射表(比如字典就可以当作映射表传进去)都当成一张映射表看待。接下来,将待检测的方法放在另一个元组中。最后,遍历元组中的方法,判断它们是不是都在attributes映射表中,这张映射表的键是Subclass及其全部超类的所有方法名与属性名。如果methods中的每个方法都在attributes映射表里,那就返回True。
请注意,上面这段代码只检测了Subclass及其全部基类的所有attribute名称是不是涵盖了我们所需的那些方法,并没有详细判断attribute到底是属性还是方法。如果某属性恰好与所需方法同名,那它也能通过检测。假如检测时想排除属性名而只考虑方法名,那么可在method in attributes这行判断语句中加上and callable(method)。由于此问题在实际编程中很少遇到,所以没必要专门改写。
用__subclasshook__()来创建带有接口检查功能的类是一项非常有用的技术,但如果每个类都要写这十几行代码的话,那就显得重复了,因为这些类之间的差别可能不大,只是基类与所支持的方法不同而已。在下一节中,我们将通过类装饰器来避免重复代码,也就是说,有了类装饰器之后,每次只需编写一两行特殊代码,就能创建出具备接口检查功能的类。(下一节的render2.py范例程序演示了这种装饰器的用法。)
《Python编程实战:运用设计模式、并发和程序库创建高质量程序》—— 2.1 适配器模式

上面这个简单的类可以当成页面渲染器来用,因为它具备相关接口。
header()方法会根据指定的宽度把标题输出到正中位置,然后换一行,在标题的每个字母下面输出“=”字符。
《Python编程实战:运用设计模式、并发和程序库创建高质量程序》—— 2.1 适配器模式

paragraph()方法使用Python标准库的textwrap模块把文本段按照指定的宽度换行,并打印出来。使用self.previous这个Boolean变量是为了保证除第一段以外,后面每两段之间都有空行隔开。由于页面渲染器的接口定义了footer(),所以即便不打印页脚,也得写个什么事都不做的方法放在这里。
《Python编程实战:运用设计模式、并发和程序库创建高质量程序》—— 2.1 适配器模式

HtmlWriter类可用来写出简单的HTML页面,它用html.escape()函数处理转义字符(在Python 3.2及早前版本中,使用xml.sax.saxutil.escape()函数)。
尽管这个类也有header()及footer()方法,但其行为却和页面渲染器接口所定义的不同。所以,在构建Page实例时,我们可以传入TextRenderer对象,但却不能直接把HtmlWriter对象当成页面渲染器传进去。
一种解决办法是编写HtmlWriter的子类,并在子类中提供页面渲染器所需的接口方法。但这种方案很容易出错,因为子类会把HtmlWriter的方法同页面渲染器的接口方法混在一起。还有个更好的办法就是创建适配器,令其把我们要使用的HtmlWriter类“聚合”(aggregate)进来,并提供Renderer所定义的接口,然后将聚合进来的类与接口适配好。图2.1演示了如何引入适配器类。
《Python编程实战:运用设计模式、并发和程序库创建高质量程序》—— 2.1 适配器模式

上面列出的就是适配器类。在构造时,可把HtmlWriter对象当成htmlWriter参数传入,该类负责提供页面渲染器接口所需的方法。由于实际渲染任务都会委托给聚合进来的HtmlWriter对象,所以HtmlRenderer类只相当于在现有的HtmlWriter类的外围包了一层新的接口而已。
《Python编程实战:运用设计模式、并发和程序库创建高质量程序》—— 2.1 适配器模式
《Python编程实战:运用设计模式、并发和程序库创建高质量程序》—— 2.1 适配器模式

上面几行代码演示了如何用两种渲染器来创建Page类的实例。在构建TextRenderer时,我们将默认宽度设为22个字符。而在用HtmlRenderer来创建HtmlWriter适配器时,我们则把一份打开的文件传了进去(创建此文件所用的语句没有列出来),这样的话,HTML就不会渲染到默认的sys.stdout上面了。

上一篇:Js+MVC~公用API的设计,返回jsonp后使ajax的error属性生效!


下一篇:【Netty】NIO 缓冲区 ( Buffer ) ( 缓冲区读写类型 | 只读缓冲区 | 映射字节缓冲区 )(一)