组件:接口和适配器

面向对象编程语言允许程序员通过创建新的“类”对象来重用现有代码的部分,这些对象是另一个类的子类。当一个类是另一个类的子类时,它被称为继承所有行为。子类可以“覆盖”和“扩展”超类提供的行为。继承在许多情况下非常有用,但由于它使用起来非常方便,因此在大型软件系统中经常被滥用,尤其是在涉及多重继承时。一种解决方案是在适当的情况下使用委托而不是“继承”。委托仅仅是要求另一个对象为一个对象执行任务的行为。为了支持这种设计模式,它通常被称为组件模式,因为它涉及许多相互作用的小组件,接口适配器是由 Zope 3 团队创建的。

“接口”仅仅是标记,对象可以使用它们来表示“我实现了这个接口”。其他对象随后可以发出类似“请给我一个实现接口 X 的对象,用于对象类型 Y”的请求。实现另一个对象类型的接口的对象被称为“适配器”。

超类-子类关系被称为is-a关系。在设计对象层次结构时,对象建模人员在可以说明子类与超类相同的类时使用子类。例如

class Shape:
    sideLength = 0
    def getSideLength(self):
        return self.sideLength

    def setSideLength(self, sideLength):
        self.sideLength = sideLength

    def area(self):
        raise NotImplementedError("Subclasses must implement area")

class Triangle(Shape):
    def area(self):
        return (self.sideLength * self.sideLength) / 2

class Square(Shape):
    def area(self):
        return self.sideLength * self.sideLength

在上面的示例中,三角形一个形状,因此它是形状的子类,正方形一个形状,因此它也是形状的子类。

但是,子类化可能会变得很复杂,尤其是在多重继承出现时。多重继承允许一个类从多个基类继承。严重依赖继承的软件最终会拥有非常宽泛和非常深的继承树,这意味着一个类从系统中分布的许多超类继承。由于使用多重继承进行子类化意味着实现继承,因此定位方法的实际实现并确保实际调用了正确的方法成为一项挑战。例如

class Area:
    sideLength = 0
    def getSideLength(self):
        return self.sideLength

    def setSideLength(self, sideLength):
        self.sideLength = sideLength

    def area(self):
        raise NotImplementedError("Subclasses must implement area")

class Color:
    color = None
    def setColor(self, color):
      self.color = color

    def getColor(self):
      return self.color

class Square(Area, Color):
    def area(self):
        return self.sideLength * self.sideLength

程序员喜欢使用实现继承的原因是它使代码更易于阅读,因为 Area 的实现细节与 Color 的实现细节位于不同的位置。这很好,因为可以想象一个对象可以有颜色但没有面积,或者有面积但没有颜色。但是,问题是 Square 并不是真正的 Area 或 Color,而是具有面积和颜色。因此,我们应该真正使用另一种面向对象技术,称为组合,它依赖于委托而不是继承来将代码分解成可重用的小块。不过,让我们继续使用多重继承示例,因为它在实践中经常被使用。

如果 Color 和 Area 基类都定义了相同的方法,例如calculate?实现将来自哪里?Square().calculate() 的实现取决于方法解析顺序或 MRO,并且当程序员通过重构系统中其他部分的类来更改看似无关的事情时,可能会发生变化,从而导致难以理解的错误。我们首先想到的可能是更改 calculate 方法名称以避免名称冲突,例如calculateAreacalculateColor。虽然明确,但这种更改可能需要在整个系统中进行大量更改,并且容易出错,尤其是在尝试集成两个你没有编写的系统时。

让我们想象另一个例子。我们有一个电器,比如吹风机。吹风机是美式电压。我们有两个电源插座,一个是美式 120 伏插座,另一个是英国 240 伏插座。如果我们将吹风机插入 240 伏插座,它将期望 120 伏电流,并且会发生错误。返回并更改吹风机以支持plug120Voltplug240Volt 方法将很繁琐,如果我们决定需要将吹风机插入另一种类型的插座怎么办?例如

class HairDryer:
    def plug(self, socket):
        if socket.voltage() == 120:
            print("I was plugged in properly and am operating.")
        else:
            print("I was plugged in improperly and ")
            print("now you have no hair dryer any more.")

class AmericanSocket:
    def voltage(self):
        return 120

class UKSocket:
    def voltage(self):
        return 240

给定这些类,可以执行以下操作

>>> hd = HairDryer()
>>> am = AmericanSocket()
>>> hd.plug(am)
I was plugged in properly and am operating.
>>> uk = UKSocket()
>>> hd.plug(uk)
I was plugged in improperly and
now you have no hair dryer any more.

我们将尝试通过为UKSocket 编写一个适配器来解决这个问题,该适配器将电压转换为美式吹风机可以使用。适配器是一个类,它使用一个且仅一个参数(“被适配者”或“原始”对象)进行构造。在本例中,我们将为了清晰起见显示所有涉及的代码

class AdaptToAmericanSocket:
    def __init__(self, original):
        self.original = original

    def voltage(self):
        return self.original.voltage() / 2

现在,我们可以像这样使用它

>>> hd = HairDryer()
>>> uk = UKSocket()
>>> adapted = AdaptToAmericanSocket(uk)
>>> hd.plug(adapted)
I was plugged in properly and am operating.

因此,如您所见,适配器可以“覆盖”原始实现。它还可以通过提供原始对象没有的方法来“扩展”原始对象的接口。请注意,适配器必须明确地将它不想修改的任何方法调用委托给原始对象,否则适配器不能在需要原始对象的地方使用。通常这不是问题,因为适配器是为使对象符合特定接口而创建的,然后被丢弃。

Twisted 代码中的接口和组件

适配器是使用多个类将代码分解成离散块的一种有用方法。但是,如果没有更多基础设施,它们就没有什么意义。如果每个希望使用适配对象的代码块都必须显式地构建适配器本身,那么组件之间的耦合就会过于紧密。我们希望实现“松耦合”,这就是 twisted.python.components 的作用。

首先,我们需要更详细地讨论接口。正如我们之前提到的,接口不过是一个用作标记的类。接口应该是 zope.interface.Interface 的子类,对于不习惯它们的 Python 程序员来说,它们看起来很奇怪。

from zope.interface import Interface

class IAmericanSocket(Interface):
    def voltage():
      """
      Return the voltage produced by this socket object, as an integer.
      """

注意它看起来就像一个普通的类定义,除了继承自 Interface 吗?但是,类块中的方法定义没有方法体!由于 Python 没有像 Java 那样对接口提供任何本机语言级支持,因此这就是区分接口定义和类的关键。

现在我们已经定义了接口,我们可以使用以下术语来谈论对象:“AmericanSocket 类实现了 IAmericanSocket 接口” 和“请给我一个将 UKSocket 适配到 IAmericanSocket 接口的对象”。我们可以对某个类实现哪些接口进行声明,并且可以请求为特定类实现某个接口的适配器。

让我们看看如何声明一个类实现了接口。

from zope.interface import implementer

@implementer(IAmericanSocket)
class AmericanSocket:
    def voltage(self):
        return 120

因此,要声明一个类实现了接口,我们只需用 zope.interface.implementer 装饰它。

现在,假设我们要将 AdaptToAmericanSocket 类重写为一个真正的适配器。在这种情况下,我们也将其指定为实现了 IAmericanSocket

from zope.interface import implementer

@implementer(IAmericanSocket)
class AdaptToAmericanSocket:
    def __init__(self, original):
        """
        Pass the original UKSocket object as original
        """
        self.original = original

    def voltage(self):
        return self.original.voltage() / 2

注意我们如何在该适配器类上放置了实现声明。到目前为止,除了要求我们键入更多内容之外,我们还没有通过使用组件实现任何东西。为了使组件有用,我们必须使用组件注册表。由于 AdaptToAmericanSocket 实现了 IAmericanSocket 并调节 UKSocket 对象的电压,我们可以将 AdaptToAmericanSocket 注册为 UKSocket 类的 IAmericanSocket 适配器。在代码中查看如何完成此操作比描述它更容易。

from zope.interface import Interface, implementer
from twisted.python import components

class IAmericanSocket(Interface):
    def voltage():
      """
      Return the voltage produced by this socket object, as an integer.
      """

@implementer(IAmericanSocket)
class AmericanSocket:
    def voltage(self):
        return 120

class UKSocket:
    def voltage(self):
        return 240

@implementer(IAmericanSocket)
class AdaptToAmericanSocket:
    def __init__(self, original):
        self.original = original

    def voltage(self):
        return self.original.voltage() / 2

components.registerAdapter(
    AdaptToAmericanSocket,
    UKSocket,
    IAmericanSocket)

现在,如果我们在交互式解释器中运行此脚本,我们可以更多地了解如何使用组件。我们可以做的第一件事是发现一个对象是否实现了接口。

>>> IAmericanSocket.implementedBy(AmericanSocket)
True
>>> IAmericanSocket.implementedBy(UKSocket)
False
>>> am = AmericanSocket()
>>> uk = UKSocket()
>>> IAmericanSocket.providedBy(am)
True
>>> IAmericanSocket.providedBy(uk)
False

如您所见,AmericanSocket 实例声称实现了 IAmericanSocket,但 UKSocket 没有。如果我们想将 HairDryerAmericanSocket 一起使用,我们可以通过检查它是否实现了 IAmericanSocket 来知道这样做是安全的。但是,如果我们决定要将 HairDryerUKSocket 实例一起使用,我们必须在这样做之前将其适配IAmericanSocket。我们使用接口对象来完成此操作。

>>> IAmericanSocket(uk)
<__main__.AdaptToAmericanSocket instance at 0x1a5120>

当用对象作为参数调用接口时,接口会在适配器注册表中查找一个为给定实例的类实现该接口的适配器。如果找到一个,它会构造一个适配器类的实例,将原始实例传递给构造函数,并将其返回。现在,HairDryer 可以安全地与适配后的 UKSocket 一起使用。但是,如果我们尝试适配一个已经实现了 IAmericanSocket 的对象会发生什么?我们只是得到原始实例。

>>> IAmericanSocket(am)
<__main__.AmericanSocket instance at 0x36bff0>

因此,我们可以编写一个新的“智能”HairDryer,它会自动查找您尝试将其插入的插座的适配器。

class HairDryer:
    def plug(self, socket):
        adapted = IAmericanSocket(socket)
        assert adapted.voltage() == 120, "BOOM"
        print("I was plugged in properly and am operating")

现在,如果我们创建一个新的“智能”HairDryer 实例并尝试将其插入各种插座,HairDryer 会根据它插入的插座类型自动进行适配。

>>> am = AmericanSocket()
>>> uk = UKSocket()
>>> hd = HairDryer()
>>> hd.plug(am)
I was plugged in properly and am operating
>>> hd.plug(uk)
I was plugged in properly and am operating

瞧;组件的魔力。

组件和继承

如果您继承自一个实现了某些接口的类,并且您的新子类声明它实现了另一个接口,那么实现将默认情况下被继承。

例如,pb.Root 是一个实现了 IPBRoot 的类。此接口指示一个对象具有可远程调用的方法,并且可以用作新 Broker 实例提供的初始对象。它有一个类似于 implements 的设置。

from zope.interface import implementer

@implementer(IPBRoot)
class Root(Referenceable):
    pass

假设您有自己的类,它实现了您的 IMyInterface 接口。

from zope.interface import implementer, Interface

class IMyInterface(Interface):
    pass

@implementer(IMyInterface)
class MyThing:
    pass

现在,如果您想让此类继承自 pb.Root,接口代码会自动确定它也实现了 IPBRoot

from twisted.spread import pb
from zope.interface import implementer, Interface

class IMyInterface(Interface):
    pass

@implementer(IMyInterface)
class MyThing(pb.Root):
    pass
>>> from twisted.spread.flavors import IPBRoot
>>> IPBRoot.implementedBy(MyThing)
True

如果您想让 MyThing 继承自 pb.Rootpb.Root 那样实现 IPBRoot,请使用 @implementer_only

from twisted.spread import pb
from zope.interface import implementer_only, Interface

class IMyInterface(Interface):
    pass

@implementer_only(IMyInterface)
class MyThing(pb.Root):
    pass
>>> from twisted.spread.pb import IPBRoot
>>> IPBRoot.implementedBy(MyThing)
False