适配器模式和桥接模式 6/30 | Python 主题月
写在前面
本文正在参加「Python主题月」,详情查看活动链接
这个月是 Python 活动月,我决定尝试用 Python 来刷这 30 天的每日一题和随机一题。然后如果周末有精力,我想捣鼓捣鼓这个python-patterns
设计模式对我来说更多的是学习而不是我的个人经验总结,所以我很可能理解偏,如果有大佬见到了请及时指出,我之所以选择在掘金来写一些个人的东西是因为这里的文章质量更高,我不希望后来者看到了这些文章被误导。
适配器模式
适配器模式应该是一个 cv 程序员必会的模式,毕竟会抄代码也是一门学问,适配器模式就是抄代码的心法之一。适配器比如说电源适配器就是将家里的市电转换成家用电器使用的电压,适配器模式在“抄”代码中就体现为将一段写的很妙的代码嵌入到自己的辣鸡代码里还能让它完美无错无警告地运行起来。
当然也有另一种情况,没有可以借鉴的他人的好代码,我们只能自己抄自己。适配器模式实质上允许将一个现有类的接口作为另一个接口使用。
下面看例子
from typing import Callable, TypeVar
T = TypeVar("T")
class Dog:
def __init__(self) -> None:
self.name = "Dog"
def bark(self) -> str:
return "woof!"
class Cat:
def __init__(self) -> None:
self.name = "Cat"
def meow(self) -> str:
return "meow!"
class Human:
def __init__(self) -> None:
self.name = "Human"
def speak(self) -> str:
return "'hello'"
class Car:
def __init__(self) -> None:
self.name = "Car"
def make_noise(self, octane_level: int) -> str:
return f"vroom{'!' * octane_level}"
class Adapter:
"""Adapts an object by replacing methods.
Usage
------
dog = Dog()
dog = Adapter(dog, make_noise=dog.bark)
"""
def __init__(self, obj: T, **adapted_methods: Callable):
"""We set the adapted methods in the object's dict."""
self.obj = obj
self.__dict__.update(adapted_methods)
def __getattr__(self, attr):
"""All non-adapted calls are passed to the object."""
return getattr(self.obj, attr)
def original_dict(self):
"""Print original object dict."""
return self.obj.__dict__
def main():
"""
>>> objects = []
>>> dog = Dog()
>>> print(dog.__dict__)
{'name': 'Dog'}
>>> objects.append(Adapter(dog, make_noise=dog.bark))
>>> objects[0].__dict__['obj'], objects[0].__dict__['make_noise']
(<...Dog object at 0x...>, <bound method Dog.bark of <...Dog object at 0x...>>)
>>> print(objects[0].original_dict())
{'name': 'Dog'}
>>> cat = Cat()
>>> objects.append(Adapter(cat, make_noise=cat.meow))
>>> human = Human()
>>> objects.append(Adapter(human, make_noise=human.speak))
>>> car = Car()
>>> objects.append(Adapter(car, make_noise=lambda: car.make_noise(3)))
>>> for obj in objects:
... print("A {0} goes {1}".format(obj.name, obj.make_noise()))
A Dog goes woof!
A Cat goes meow!
A Human goes 'hello'
A Car goes vroom!!!
"""
if __name__ == "__main__":
import doctest
doctest.testmod(optionflags=doctest.ELLIPSIS)
复制代码
这段代码里有代表实体的类(狗,猫,人,小汽车),它们发出不同的声音。适配器类为发出声音的原始方法提供了一个统一的接口。因此,原来的接口(如 bark
和 meow
)可以用一个统一的名字:make_noise
。
这个例子好,和我说的常见情况刚好形成补充,把适配器类的发声接口适配到不同实体类的实际发声接口上了。这里是一般到特殊,我说的是特殊到特殊。
另外,Grok 框架使用适配器来使对象与特定的 API 协同工作,而无需修改对象本身。
桥接模式
桥接模式将一个抽象与它的实现解耦。当不希望抽象部分和行为有一种固定的绑定关系,而可以动态联系两者,桥接模式就派上用场了。如果一个抽象类或接口有多个具体实现(子类,concrete subclass),这些子类之间关系可能有以下两种情况:
-
这多个子类之间概念是并列的,如打桩,有两个 concrete class:方形桩和圆形桩。这两个形状上的桩是并列的,没有概念上的重复。
-
这多个子类之中有内容概念上重叠。那么需要我们把抽象共同部分和行为共同部分各自独立开来,原来是准备放在一个接口里,现在需要设计两个接口:抽象接口和行为接口,分别放置抽象和行为。
这第二种情况举的例子就有意思了,“鱼丸粗面”。比如说去买咖啡时人家会问你要中杯大杯超大杯,还问你要加奶还是不加奶。显然这里排列组合是 3 * 2 = 6
种咖啡选项。简单粗暴的实现咖啡点单的话就可以直接实现六个子类对应排列组合里的每一个具体组合。但是这些组合有一部分概念是重叠的,所以这里可以将杯子大小定义为抽象,将加不加其他饮品定义为行为。现在有三个抽象类“中杯”,“大杯”,“超大杯”,继承自“一杯饮料”类;“一杯饮料”类有一个接口“加不加副饮品”。有两个具体行为继承自“加不加副饮品”接口,分别是“加副饮品”和“不加副饮品”。这样就只用实现 3 + 2 = 5
个接口。这在排列组合选项特别多的时候尤其好用。
如果上面这个例子太绕了,就别管这个例子了,也许举得不好。看下面这个简单的例子。
# ConcreteImplementor 1/2
class DrawingAPI1:
def draw_circle(self, x, y, radius):
print(f"API1.circle at {x}:{y} radius {radius}")
# ConcreteImplementor 2/2
class DrawingAPI2:
def draw_circle(self, x, y, radius):
print(f"API2.circle at {x}:{y} radius {radius}")
# Refined Abstraction
class CircleShape:
def __init__(self, x, y, radius, drawing_api):
self._x = x
self._y = y
self._radius = radius
self._drawing_api = drawing_api
# low-level i.e. Implementation specific
def draw(self):
self._drawing_api.draw_circle(self._x, self._y, self._radius)
# high-level i.e. Abstraction specific
def scale(self, pct):
self._radius *= pct
def main():
"""
>>> shapes = (CircleShape(1, 2, 3, DrawingAPI1()), CircleShape(5, 7, 11, DrawingAPI2()))
>>> for shape in shapes:
... shape.scale(2.5)
... shape.draw()
API1.circle at 1:2 radius 7.5
API2.circle at 5:7 radius 27.5
"""
if __name__ == "__main__":
import doctest
doctest.testmod()
复制代码
例子里两个具体的画圆 API 和它们的抽象是分开的,这样如果一个具体实现比较重,可以换另一个轻的具体实现。(这里没体现出来)在一些通用第三方库对不同平台上相同功能接口的具体实现的场景下可以见到桥接模式。
小结
适配器模式实质上允许将一个现有类的接口作为另一个接口使用。桥接模式将一个抽象与它的实现解耦。