简单聊聊Python设计模式:适配器模式 | 八月更文挑战

2,018 阅读7分钟

这是我参与8月更文挑战的第25天,活动详情查看:8月更文挑战

📖前言

框架中用到了设计模式吗?

框架中不仅用到设计模式还用了很多,而且有些时候根本不是一个模式的单独使用,而是多种设计模式的综合运用。与大部分小伙伴平时开发的 CRUD 可就不一样了,如果都是if语句从上到下,也就算得不上什么框架了。就像你到Spring的源码中搜关键字Adapter,就会出现很多实现类,例如;UserCredentialsDataSourceAdapter。而这种设计模式就是我们本文要介绍的适配器模式。

适配器在生活里随处可见


🐱‍🏍结构型设计模式:

  其主要用来处理一个系统中不同实体(比如类和对象)之间关系,关注的是提供一种简单的对象组合方式来创造新的功能。


🚀适配器模式

适配器模式的主要作用就是把原本不兼容的接口,通过适配修改做到统一。使得用户方便使用,就像我们提到的万能充、数据线、MAC笔记本的转换头、出国旅游买个插座等等,他们都是为了适配各种不同的,做的兼容。

当我们希望把一个老组件用于一个新组系统或者把一个新组件应用于老系统中,同时在代码无法修改的,或者说无法访问这些代码时(在实际开发中,旧系统的代码修改后牵一而动全身,很容易引起系统的崩溃。)。这时候,我们可以编写一个额外的代码层,该代码层包含让这两个接口之间能够通信需要进行的所有修改。

除了我们生活中出现的各种适配的场景,那么在业务开发中呢?

在业务开发中我们会经常的需要做不同接口的兼容,尤其是中台服务,中台需要把各个业务线的各种类型服务做统一包装,再对外提供接口进行使用。而这在我们平常的开发中也是非常常见的。

通俗的说就是设计 接口/API,以保证程序符合 开放/封闭 原则,同时保证不修改其他地方接口的调用方式,保持新老代码间的兼容性。


😎什么时候使用 Adapter 模式

很多时候,我们并非从0开始编程,特别是当现有的类已经被充分测试过了,Bug 很少,而且已经被用于其他软件之中时,我们更愿意将这些类作为组件重复利用。

Adapter 模式会对现有的类进行适配,生成新的类。通过该模式可以很方便地创建我们需要的方法群。当出现 Bug 时,由于我们明确知道 Bug 不在现有的类(Adaptee角色)中,所以只需调查 Adapter 角色的类即可。

  1. 没有现成的代码 让现有的类适配新的接口(API)时,使用 Adapter 模式似乎是理所应当的。在 Adapter 模式中,并非一定需要现成的代码。只要知道现有类的功能,就可以。

  2. 版本升级与兼容性 软件的生命周期总是伴随着版本的升级,而很多时候需要与旧版本兼容。这个时候可以让新版本扮演 Adaptee 角色,旧版本扮演 Target 角色。接着编写一个 Adapter 角色的类,让它使用新版本的类来实现旧版本的类中的功能。

  3. 功能完全不同的类 当 Adaptee 角色与 Target 角色的功能完全不同时,Adapter 模式是无法使用的。就如同我们无法用交流100伏特电压让自来水管出水一样。


🙌软件的例子

Grok 是一个 Python 框架,运行在 Zope3 之上,专注于敏捷开发。Grok 框架使用适配器,让已有对象无需变更就能符合指定 API的标准

Python 第三方包 Traits 也使用了适配器模式,将没有实现某个指定接口(或一组接口)的对象转换成实现了接口的对象。

🚀应用案例

在某个产品制造出来之后,需要应对新的需求之时,如果希望其仍然有效,使用适配器是一种更好的方式,原因如下:

  1. 不要求访问他方接口的源代码
  2. 不违反开放/封闭原则

假设有这样一个场景:

  1. 我们的应用有一个 Computer 类,用来显示一台计算机的基本信息。
class Computer:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return 'the {} computer'.format(self.name)

    def execute(self):
        return 'executes a program'

execute 方法可以执行的主要动作,由客户端代码调用。

  1. 下面有需求,需要为该应用丰富更多的功能,而有了接下来的两个类:SynthesizerHumanSynthesizer 主要动作由 play() 方法执行。 Human 主要动作由 speak() 方法执行。
class Synthesizer:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return 'the {} synthesizer'.format(self.name)

    def play(self):
        return 'is playing an electronic song'

class Human:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return '{} the human'.format(self.name)

    def speak(self):
        return 'says hello'
  1. 问题是:对于原来的老系统来说,所有动作函数均使用 Obj.execute() 来执行。即对于调用者来说,新系统的组件 Synthesizer.play()Human.speak() 是不存在的,必须像调用 Computer.execute() 一样使用 Synthesizer.execute()Human.execute() 来调用原系统中对象的执行函数。

在不改变 SynthesizerHuman 类的前提下,为了让新组件去适应(兼容)旧系统的情况。我们可以使用适配器模式来解决!我们创建一个通用的 Adapter 类,将一些带不同接口的对象适配到一个统一接口中。__init__ 方法的 obj参数 是我们想要适配的对象,adapted_methods 是一个字典,键值对中的键是客户端要调用的方法,值是应该被调用的方法。而其中在 __dict__ 魔法方法,将 新组件对象的方法 添加到 适配器对象的属性字典 中。这样就可以使用适配器对象即可调用新组件的方法。

class Adapter:
    def __init__(self, obj, adapted_methods):
        self.obj = obj
        self.__dict__.update(adapted_methods)

    def __str__(self):
        return str(self.obj)
  1. 下面看看使用适配器模式的方法。列表 objects 容纳着所有对象。属于 Computer 类的可兼容对象不需要适配。可以直接将它们添加到列表中。不兼容的对象则不能直接添加。使用 Adapter 类来适配它们。结果是,对于所有对象,客户端代码都可以始终调用已知的 execute() 方法,而无需关心被使用的类之间的任何接口差别。
def main():
    objects = [Computer('Asus')]
    synth = Synthesizer('moog')
    objects.append(Adapter(synth, dict(execute=synth.play)))
    human = Human('Bob')
    objects.append(Adapter(human, dict(execute=human.speak)))

    for i in objects:
        print('{} {}'.format(str(i), i.execute()))
        print('type is {}'.format(type(i)))

输出结果如下:

the Asus computer executes a program
type is <class '__main__.Computer'>
the moog synthesizer is playing an electronic song
type is <class '__main__.Adapter'>
Bob the human says hello
type is <class '__main__.Adapter'>

🎉总结

  • 适配器模式包含一下三个角色:

    • 目标抽象类:目标抽象类定义客户所需的接口,可以是一个抽象类或接口,也可以是具体类。在类适配器中,由于C#语言不支持多重继承,所以它只能是接口。
    • 适配器类:它可以调用另一个接口,作为一个转换器,对 AdapteeTarget 进行适配。它是适配器模式的核心。
    • 适配者类:适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类包好了客户希望的业务方法。
  • 更多参考精彩博文请看这里:《陈永佳的博客》

  • 喜欢博主的小伙伴可以加个关注、点个赞哦,持续更新嘿嘿!