02 适配模式:身高不够鞋来凑

【故事剧情】

晚上九点半,Tony 上了地铁,准备回家,正巧还有一个空位,赶紧走向前坐下。工作一天后,疲惫不堪的他正准备坐着打个盹小睡一会儿。这时进来两个小姑娘,一个小巧可爱,一个身姿曼妙;嬉笑地聊着天走到了 Tony 的前面,Tony 犹豫了片刻后还是绅士般地给小女孩让了个座……

两个小姑娘道了声谢谢,便挤在一块坐下了,继续有说有笑地谈论着……

Amy:周末在商场里看到你和一个帅哥在一起。好你个 Nina,脱单了也不告诉姐姐我,太不够意思了!怎么……想金屋藏“娇”啊!

Nina:不是啦,也是最近刚有事,还没来得及告诉你呢。

Amy:那快说说呗!那小哥看着很高啊!

Nina:嗯,他1米85。

Amy:厉害了,你155 他185,这就是传说中的最萌身高组合啊!

Nina:嗯,走在大街上,别人都回头看我们,弄的我挺不好了意思的~

Amy:你这是身在福中不知福啊!别人就是因为想求也求不到呢!

Nina:也有很气的时候啦,有时生气想打他,结果粉拳一出去就被他的大手包了饺子。

Amy:哈哈哈哈,还有呢!

Nina:还有一件很囧的事,我一抬头总是看到他的鼻毛,他一低头总是看到我的头发屑!

Amy:哈哈哈!笑的我肚子痛了……所以你们在一起,你一定要天天洗头,他一定要天天修鼻毛咯~

Nina:是啊!可麻烦了~

Amy:看来还是我这 160 的身高最棒了!衣服可以随便挑,更重要的是我男友 175,穿上高跟鞋,我就可以挽着他的手肩并肩地走~

Nina:这就是所谓的身高不够鞋来凑吗?

Amy:不然怎么叫万能的高跟鞋呢……

Nina:好羡慕啊!在我这,高跟鞋也无能~

Amy:… …

正听的兴起时,地铁门开了。Tony 才反应过来,到站了,该下车了。Tony 赶忙往车门方向走,一不小心额头碰到了把手上,只好一手护着头往外跑,两个小姑娘相视一笑~

img

用程序来模拟生活

身材苗条、长像出众是每个人梦寐以求的,尤其是女孩子!但很多人却因为先天的原因并不能如意,这时就需要通过服装、化妆去弥补。所谓美女,三分靠长相七分靠打扮!比如身高不够,就可以通过穿高跟鞋来弥补;如果本身就比较高,那穿不穿高跟鞋就没那么重要了。这里的高跟鞋就起着一个适配的作用,能让你的形象增高四、五厘米,下面我们就用代码来模拟一下高跟鞋在生活中的场景吧!

源码示例:

class IHightPerson:
    "接口类,提供空实现的方法,由子类去实现"

    def getName(self):
        "获取姓名"
        pass

    def getHeight(self):
        "获取身高"
        pass

class HighPerson(IHightPerson):
    "个高的人"

    def __init__(self, name):
        self.__name = name

    def getName(self):
        return self.__name

    def getHeight(self):
        return 170

class ShortPerson:
    "个矮的人"

    def __init__(self, name):
        self.__name = name

    def getName(self):
        return self.__name

    def getRealHeight(self):
        return 160

    def getShoesHeight(self):
        return 6

class DecoratePerson(ShortPerson, IHightPerson):
    "有高跟鞋搭配的人"

    def getHeight(self):
        return super().getRealHeight() + super().getShoesHeight()

测试代码:

def canPlayReceptionist(person):
    """
    是否可以成为(高级酒店)接待员
    :param person: IHightPerson的对象
    :return: 是否符合做接待员的条件
    """
    return person.getHeight() >= 165;

def testPerson():
    lira = HighPerson("Lira")
    print(lira.getName() + "身高" + str(lira.getHeight()) + ",完美如你,天生的美女!" )
    print("是否适合做接待员:", "符合" if canPlayReceptionist(lira) else "不符合")
    print()
    demi = DecoratePerson("Demi");
    print(demi.getName() + "身高" + str(demi.getHeight()) + "在高跟鞋的适配下,你身高不输高圆圆,气质不输范冰冰!")
    print("是否适合做接待员:", "符合" if canPlayReceptionist(lira) else "不符合")

输出结果:

Lira身高170,完美如你,天生的美女!
是否适合做接待员: 符合

Demi身高166在高跟鞋的适配下,你身高不输高圆圆,气质不输范冰冰!
是否适合做接待员: 符合

从剧情中思考适配器模式

在上面的例子中,高跟鞋起着一个适配的作用,让其形象增高 5~7 厘米完全不在话下,而且效果立竿见影!使得一些女孩原本不符合接待员的真实身高,在鞋子的帮助下也能符合条件。如高跟鞋一样,使原本不匹配某种功能的对象变得匹配这种功能,这在程序中叫做适配器模式。

适配器模式

Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.

将一个类的接口变成客户端所期望的另一种接口,从而使原本因接口不匹配而无法一起工作的两个类能够在一起工作。

适配器模式的作用:

设计思想

适配器模式又叫变压器模式,也叫包装模式(Wrapper),它的核心思想是将一个对象经过包装或转换后使它符合指定的接口,使得调用方可以像使用这接口的一般对象一样使用它。这一思想,在我们生活中可谓是处处可见,比如变压器插座,能让你像使用国内电器一样使用美标(110V)电器;还有就是各种转接头,如 MiniDP 转 HDMI 转接头、HDMI 转 VGA 线转换器、Micro USB 转 Type-C 转接头等。

你们知道吗?“设计模式”一词最初是来源于建筑领域,而中国古建筑是世界建筑史的一大奇迹(如最具代表性的紫禁城),中国古建筑的灵魂是一种叫榫卯结构的建造理念。

榫卯(sǔn mǎo)是两个木构件上所采用的一种凹凸结合的连接方式。凸出部分叫榫(或榫头);凹进部分叫卯(或榫眼、榫槽)。它是古代中国建筑、家具及其他木制器械的主要结构方式。

榫卯结构的经典模型如下图:

img

榫卯是藏在木头里的灵魂!而随着时代的变化,其结构也发生着一些变化,现在很多建材生产商也在发明和生产新型的具有榫卯结构的木板。假设木板生产商有下面两块木板,木板 A 是榫,木板 B 是卯,A、B 两块木板就完全吻合。他们之间的榫卯接口是一种 T 字形的接口。

enter image description here

后来,随着业务的拓展,木板厂商增加了一种新木板 C。但 C 是 L 形的接口,不能与木板 A 对接。为了让木板 C 能与木板 A 进行对接,就需要增加一个衔接板 D 进行适配,而这个 D 就相当于适配器,如下图:

enter image description here

适配器模式通常用于对已有的系统拓展新功能时,尤其适用于在设计良好的系统框架下接入第三方的接口或第三方的 SDK 时。在系统的最初设计阶段,最好不要把适配器模式考虑进去,除非一些特殊的场景(如系统本身就是要去对接和适配多种类型的硬件接口)。

适配器模式的模型抽象

类图

适配器模式的类图如下:

enter image description here

Target 是一个接口类,是提供给用户调用的接口抽象,如上面示例中的 IHightPerson。Adaptee 是你要进行适配的对象类,如上面的 ShortPerson。Adapter 是一个适配器,是对 Adaptee 的适配,它将 Adaptee 的对象转换(或说包装)成符合 Target 接口的对象;如上面的 DecoratePerson,将 ShortPerson 的 getRealHeight 和 getShoesHeight 方法包装成 IHightPerson 的 getHeight 接口。

模型说明

设计要点

适配器模式中主要三个角色,在设计适配器模式时要找到并区分这些角色:

优缺点

适配器模式的优点
适配器模式的缺点

实战应用

有一个电子书阅读器的项目(Reader),研发之初,产品经理经过各方讨论,最后告诉我们只支持 TXT 和 Epub 格式的电子书。然后经过仔细思考、精心设计,采用了如图1的代码架构。在这个类图中,有一个阅读器的核心类 Reader,一个 TXT 文档的关键类 TxtBook(负责 TXT 格式文件的解析),和一个 Epub 文档的关键类 EpubBook(负责 Epub 格式文件的解析)。

enter image description here

图1:阅读器类图

产品上线半年后,市场响应良好,业务部门反映:有很多办公人员也在用我们的阅读器,他们希望这个阅读器能同时支持 PDF 格式,这样就不用在多个阅读器神之间来回切换了,此时程序就需要增加对 PDF 格式的支持,而 PDF 并不是核心业务,我们不会单独为其开发一套 PDF 解析内核,而会使用一些开源的 PDF 库(我们称它为第三方库),如 MuPDF、TCPDF 等。而开源库的接口和我们的接口并不相同(如图2),返回的内容也不是我们直接需要的,需要经过一些转换才能符合我们的要求。

enter image description here

图2:第三方 PDF 解析库的类图

这时,我们就需要对 PDF 的解析库 MuPDF 进行适配。经过上面的学习,你一定知道这时该用适配器模式了,于是有了如下图3的类图结构。

enter image description here

图3:兼容 PDF 的类图结构

代码实现如下:

class Page:
    "电子书一页的内容"
    def __init__(self, pageNum):
        self.__pageNum = pageNum

    def getContent(self):
        return "第 " + str(self.__pageNum) + " 页的内容..."

class Catalogue:
    "目录结构"

    def __init__(self, title):
        self.__title = title
        self.__chapters = []
        self.setChapter("第一章")
        self.setChapter("第二章")

    def setChapter(self, title):
        self.__chapters.append(title)

    def showInfo(self):
        print("标题:" + self.__title)
        for chapter in self.__chapters:
            print(chapter)

class IBook:
    "电子书文档的接口类"

    def parseFile(self, filePath):
        pass

    def getCatalogue(self):
        pass

    def getPageCount(self):
        pass

    def getPage(self, pageNum):
        pass

class TxtBook(IBook):
    "TXT解析类"

    def parseFile(self, filePath):
        # 模拟文档的解析
        print(filePath + " 文件解析成功")
        self.__pageCount = 500
        return True

    def getCatalogue(self):
        return Catalogue("TXT电子书")

    def getPageCount(self):
        return self.__pageCount

    def getPage(self, pageNum):
        return Page(pageNum)

class EpubBook(IBook):
    "TXT解析类"

    def parseFile(self, filePath):
        # 模拟文档的解析
        print(filePath + " 文件解析成功")
        self.__pageCount = 800
        return True

    def getCatalogue(self):
        return Catalogue("Epub电子书")

    def getPageCount(self):
        return self.__pageCount

    def getPage(self, pageNum):
        return Page(pageNum)

class Outline:
    "第三方PDF解析库的目录类"
    pass

class PdfPage:
    "PDF页"

    def __init__(self, pageNum):
        self.__pageNum = pageNum

    def getPageNum(self):
        return self.__pageNum

class ThirdPdf:
    "第三方PDF解析库"

    def __init__(self):
        self.__pageSize = 0

    def open(self, filePath):
        print("第三方解析PDF文件:" + filePath)
        self.__pageSize = 1000
        return True

    def getOutline(self):
        return Outline()

    def pageSize(self):
        return self.__pageSize

    def page(self, index):
        return PdfPage(index)

class PdfAdapterBook(ThirdPdf, IBook):
    "TXT解析类"

    def parseFile(self, filePath):
        # 模拟文档的解析
        rtn = super().open(filePath)
        if(rtn):
            print(filePath + "文件解析成功")
        return rtn

    def getCatalogue(self):
        outline = super().getOutline()
        print("将Outline结构的目录转换成Catalogue结构的目录")
        return Catalogue("PDF电子书")

    def getPageCount(self):
        return super().pageSize()

    def getPage(self, pageNum):
        page = self.page(pageNum)
        print("将PdfPage的面对象转换成Page的对象")
        return Page(page.getPageNum())

# 导入os库
import os

class Reader:
    "阅读器"

    def __init__(self, name):
        self.__name = name
        self.__filePath = ""
        self.__curBook = None
        self.__curPageNum = -1

    def __initBook(self, filePath):
        self.__filePath = filePath
        extName = os.path.splitext(filePath)[1]
        if(extName.lower() == ".epub"):
            self.__curBook = EpubBook()
        elif(extName.lower() == ".txt"):
            self.__curBook = TxtBook()
        elif(extName.lower() == ".pdf"):
            self.__curBook = PdfAdapterBook()
        else:
            self.__curBook = None

    def openFile(self, filePath):
        self.__initBook(filePath)
        if(self.__curBook is not None):
            rtn = self.__curBook.parseFile(filePath)
            if(rtn):
                self.__curPageNum = 1
            return rtn
        return False

    def closeFile(self):
        print("关闭 " + self.__filePath + " 文件")
        return True

    def showCatalogue(self):
        catalogue = self.__curBook.getCatalogue()
        catalogue.showInfo()

    def prePage(self):
        return self.gotoPage(self.__curPageNum - 1)

    def nextPage(self):
        return self.gotoPage(self.__curPageNum + 1)

    def gotoPage(self, pageNum):
        if(pageNum < 1 or pageNum > self.__curBook.getPageCount()):
            return None

        self.__curPageNum = pageNum
        print("显示第" + str(self.__curPageNum) + "页")
        page = self.__curBook.getPage(self.__curPageNum)
        page.getContent()
        return page

测试代码:

def testReader():
    reader = Reader("阅读器")
    if(not reader.openFile("平凡的世界.txt")):
        return
    reader.showCatalogue()
    reader.gotoPage(1)
    reader.nextPage()
    reader.closeFile()
    print()

    if (not reader.openFile("平凡的世界.epub")):
        return
    reader.showCatalogue()
    reader.gotoPage(5)
    reader.nextPage()
    reader.closeFile()
    print()

    if (not reader.openFile("平凡的世界.pdf")):
        return
    reader.showCatalogue()
    reader.gotoPage(10)
    reader.nextPage()
    reader.closeFile()

输出结果:

平凡的世界.txt 文件解析成功
标题:TXT电子书
第一章
第二章
显示第1页
显示第2页
关闭 平凡的世界.txt 文件

平凡的世界.epub 文件解析成功
标题:Epub电子书
第一章
第二章
显示第5页
显示第6页
关闭 平凡的世界.epub 文件

第三方解析PDF文件:平凡的世界.pdf
平凡的世界.pdf文件解析成功
将Outline结构的目录转换成Catalogue结构的目录
标题:PDF电子书
第一章
第二章
显示第10页
将PdfPage的面对象转换成Page的对象
显示第11页
将PdfPage的面对象转换成Page的对象
关闭 平凡的世界.pdf 文件

应用场景