专业编程基础技术教程

网站首页 > 基础教程 正文

经典结构型设计模式:装饰器模式

ccvgpt 2024-08-04 12:16:31 基础教程 10 ℃

意图

装饰器是一种结构型设计模式,它通过将对象放置在包含行为的特殊包装对象中,使您能够为对象附加新的行为。

问题

请想象一下,您正在开发一个通知库,让其他程序能够向其用户通知重要事件。

经典结构型设计模式:装饰器模式

初始版本的库基于Notifier类,该类仅具有一些字段、构造函数和一个send方法。该方法可以接受来自客户端的消息参数,并将消息发送到通过构造函数传递给通知器的电子邮件列表中。第三方应用程序作为客户端,应该创建和配置通知器对象一次,并在每次发生重要事件时使用它。

在某个时候,你意识到库的用户希望不仅仅是电子邮件通知。他们中的许多人希望收到关键问题的短信通知。其他人希望通过Facebook收到通知,当然,企业用户希望收到Slack的通知。

这样做有多难呢?你扩展了Notifier类,并将额外的通知方法放在新的子类中。现在客户端应该实例化所需的通知类,并在以后的通知中使用它。

但是,有人合理地问道:“为什么不能同时使用多种通知方式?如果你的房子着火了,你可能希望通过每个渠道都得到通知。”

为了解决这个问题,你尝试创建了特殊的子类,将多个通知方法组合在一个类中。然而,很快就明显,这种方法会使代码变得庞大,不仅是库代码,还包括客户端代码。

你必须找到其他的方法来组织通知类,以免它们的数量意外地打破吉尼斯纪录。

解决方法

当需要改变对象的行为时,继承是首先想到的方法。然而,继承有几个严重的注意事项需要注意:

继承是静态的。无法在运行时修改现有对象的行为。只能用来用另一个从不同子类创建的对象替换整个对象。

子类只能有一个父类。在大多数语言中,继承无法同时继承多个类的行为。

克服这些问题的一种方法是使用聚合或组合,而不是继承。这两种替代方法的工作方式几乎相同:一个对象持有对另一个对象的引用,并将一些工作委托给它,而继承中,对象本身能够执行该工作,从其超类继承行为。

通过这种新方法,可以轻松地在运行时用另一个链接的“辅助”对象替换它,从而改变容器的行为。一个对象可以使用多个类的行为,持有对多个对象的引用,并委托给它们各种工作。聚合/组合是许多设计模式的关键原则,包括装饰器模式。在此之上,让我们回到模式讨论。

"包装器"是装饰器模式的另一个昵称,它清晰地表达了该模式的主要思想。包装器是一个可以与某个目标对象关联的对象。包装器包含与目标对象相同的一组方法,并将它所接收到的所有请求委托给目标对象处理。然而,包装器可以通过在将请求传递给目标对象之前或之后执行某些操作来改变结果。

那么,何时一个简单的包装器变成了真正的装饰器呢?正如我之前提到的,包装器实现了与被包装对象相同的接口。这就是为什么从客户端的角度来看,这些对象是相同的。将包装器的引用字段接受任何遵循该接口的对象,这样就可以将多个包装器覆盖在一个对象上,将所有包装器的组合行为添加到该对象中。

在我们的通知示例中,让我们将简单的电子邮件通知行为保留在基础的 Notifier 类中,而将所有其他通知方法转换为装饰器。

客户端代码需要将基本通知对象包装在与客户端偏好相匹配的一组装饰器中。生成的对象将以堆栈的形式结构化。

堆栈中的最后一个装饰器将成为客户端实际操作的对象。由于所有装饰器都实现了与基础通知器相同的接口,客户端代码的其余部分不需要关心它是与“纯净”的通知器对象还是装饰后的对象进行交互。

我们可以将相同的方法应用于其他行为,比如消息格式化或收件人列表的组合。只要这些自定义装饰器遵循相同的接口,客户端可以对对象进行任意装饰。

真实世界类比

穿衣服是使用装饰器的一个例子。当感到寒冷时,你会穿上一件毛衣。如果穿了毛衣还是寒冷,你可以在上面穿一件外套。如果下雨了,你可以穿上雨衣。所有这些服装都“扩展”了你的基本行为,但并非你身体的一部分,而且你可以随时脱掉任何一件不需要的衣物。

结构

1、组件(Component)声明了包装器(Wrapper)和被包装对象(Wrapped Object)的共同接口。

2、具体组件(Concrete Component)是被包装的对象的类。它定义了基本行为,可以被装饰器(Decorator)修改。

3、基础装饰器(Base Decorator)类有一个用于引用被包装对象的字段。该字段的类型应声明为组件接口,以便可以包含具体组件和装饰器。基础装饰器将所有操作委托给被包装对象。

4、具体装饰器(Concrete Decorators)定义了可以动态添加到组件上的额外行为。具体装饰器重写基础装饰器的方法,在调用父方法之前或之后执行自己的行为。

5、客户端(Client)可以将组件包装在多层装饰器中,只要通过组件接口与所有对象进行交互。

伪代码

在这个例子中,装饰器模式允许你独立于实际使用数据的代码来压缩和加密敏感数据。

应用程序使用一对装饰器将数据源对象进行封装。这两个包装器改变了数据写入和从磁盘读取的方式:

  • 在数据写入磁盘之前,装饰器对其进行加密和压缩。原始类将加密和保护后的数据写入文件,而不知道这个变化。
  • 在从磁盘读取数据后,数据通过相同的装饰器进行解压缩和解码。

装饰器和数据源类实现了相同的接口,使它们在客户端代码中可以互换使用。

// The component interface defines operations that can be
// altered by decorators.
interface DataSource is
    method writeData(data)
    method readData():data

// Concrete components provide default implementations for the
// operations. There might be several variations of these
// classes in a program.
class FileDataSource implements DataSource is
    constructor FileDataSource(filename) { ... }

    method writeData(data) is
        // Write data to file.

    method readData():data is
        // Read data from file.

// The base decorator class follows the same interface as the
// other components. The primary purpose of this class is to
// define the wrapping interface for all concrete decorators.
// The default implementation of the wrapping code might include
// a field for storing a wrapped component and the means to
// initialize it.
class DataSourceDecorator implements DataSource is
    protected field wrappee: DataSource

    constructor DataSourceDecorator(source: DataSource) is
        wrappee = source

    // The base decorator simply delegates all work to the
    // wrapped component. Extra behaviors can be added in
    // concrete decorators.
    method writeData(data) is
        wrappee.writeData(data)

    // Concrete decorators may call the parent implementation of
    // the operation instead of calling the wrapped object
    // directly. This approach simplifies extension of decorator
    // classes.
    method readData():data is
        return wrappee.readData()

// Concrete decorators must call methods on the wrapped object,
// but may add something of their own to the result. Decorators
// can execute the added behavior either before or after the
// call to a wrapped object.
class EncryptionDecorator extends DataSourceDecorator is
    method writeData(data) is
        // 1. Encrypt passed data.
        // 2. Pass encrypted data to the wrappee's writeData
        // method.

    method readData():data is
        // 1. Get data from the wrappee's readData method.
        // 2. Try to decrypt it if it's encrypted.
        // 3. Return the result.

// You can wrap objects in several layers of decorators.
class CompressionDecorator extends DataSourceDecorator is
    method writeData(data) is
        // 1. Compress passed data.
        // 2. Pass compressed data to the wrappee's writeData
        // method.

    method readData():data is
        // 1. Get data from the wrappee's readData method.
        // 2. Try to decompress it if it's compressed.
        // 3. Return the result.


// Option 1. A simple example of a decorator assembly.
class Application is
    method dumbUsageExample() is
        source = new FileDataSource("somefile.dat")
        source.writeData(salaryRecords)
        // The target file has been written with plain data.

        source = new CompressionDecorator(source)
        source.writeData(salaryRecords)
        // The target file has been written with compressed
        // data.

        source = new EncryptionDecorator(source)
        // The source variable now contains this:
        // Encryption > Compression > FileDataSource
        source.writeData(salaryRecords)
        // The file has been written with compressed and
        // encrypted data.


// Option 2. Client code that uses an external data source.
// SalaryManager objects neither know nor care about data
// storage specifics. They work with a pre-configured data
// source received from the app configurator.
class SalaryManager is
    field source: DataSource

    constructor SalaryManager(source: DataSource) { ... }

    method load() is
        return source.readData()

    method save() is
        source.writeData(salaryRecords)
    // ...Other useful methods...


// The app can assemble different stacks of decorators at
// runtime, depending on the configuration or environment.
class ApplicationConfigurator is
    method configurationExample() is
        source = new FileDataSource("salary.dat")
        if (enabledEncryption)
            source = new EncryptionDecorator(source)
        if (enabledCompression)
            source = new CompressionDecorator(source)

        logger = new SalaryManager(source)
        salary = logger.load()
    // ...

适用性

使用装饰器模式的时候,当你需要在运行时为对象分配额外的行为,而不破坏使用这些对象的代码时,可以使用装饰器模式。

装饰器模式可以将业务逻辑分为多个层次,并为每个层次创建一个装饰器,在运行时以不同的逻辑组合对象。客户端代码可以以相同的方式处理所有这些对象,因为它们都遵循一个共同的接口。

当使用继承方式扩展对象的行为变得笨拙或不可行时,可以使用装饰器模式。

许多编程语言都有final关键字,用于防止类的进一步扩展。对于final类,唯一的重用现有行为的方式就是使用装饰器模式,将类包装在自己的包装器中。

如何实现

1、确保您的业务领域可以表示为一个主要组件,该组件上方有多个可选的层。

2、找出主要组件和可选层都具有的共同方法。创建一个组件接口,并在其中声明这些方法。

3、创建一个具体组件类,并在其中定义基本行为。

4、创建一个基础装饰器类。它应该有一个字段用于存储对包装对象的引用。该字段应该以组件接口类型声明,以允许链接到具体组件和装饰器。基础装饰器必须将所有工作委托给包装对象。

5、确保所有类都实现了组件接口。

6、通过从基础装饰器扩展它们来创建具体装饰器。具体装饰器必须在调用父方法之前或之后执行其行为(该方法始终委托给包装对象)。

7、客户端代码必须负责创建装饰器并以客户端所需的方式组合它们。

Python示例

class Component():
    """
    The base Component interface defines operations that can be altered by
    decorators.
    """

    def operation(self) -> str:
        pass


class ConcreteComponent(Component):
    """
    Concrete Components provide default implementations of the operations. There
    might be several variations of these classes.
    """

    def operation(self) -> str:
        return "ConcreteComponent"


class Decorator(Component):
    """
    The base Decorator class follows the same interface as the other components.
    The primary purpose of this class is to define the wrapping interface for
    all concrete decorators. The default implementation of the wrapping code
    might include a field for storing a wrapped component and the means to
    initialize it.
    """

    _component: Component = None

    def __init__(self, component: Component) -> None:
        self._component = component

    @property
    def component(self) -> Component:
        """
        The Decorator delegates all work to the wrapped component.
        """

        return self._component

    def operation(self) -> str:
        return self._component.operation()


class ConcreteDecoratorA(Decorator):
    """
    Concrete Decorators call the wrapped object and alter its result in some
    way.
    """

    def operation(self) -> str:
        """
        Decorators may call parent implementation of the operation, instead of
        calling the wrapped object directly. This approach simplifies extension
        of decorator classes.
        """
        return f"ConcreteDecoratorA({self.component.operation()})"


class ConcreteDecoratorB(Decorator):
    """
    Decorators can execute their behavior either before or after the call to a
    wrapped object.
    """

    def operation(self) -> str:
        return f"ConcreteDecoratorB({self.component.operation()})"


def client_code(component: Component) -> None:
    """
    The client code works with all objects using the Component interface. This
    way it can stay independent of the concrete classes of components it works
    with.
    """

    # ...

    print(f"RESULT: {component.operation()}", end="")

    # ...


if __name__ == "__main__":
    # This way the client code can support both simple components...
    simple = ConcreteComponent()
    print("Client: I've got a simple component:")
    client_code(simple)
    print("\n")

    # ...as well as decorated ones.
    #
    # Note how decorators can wrap not only simple components but the other
    # decorators as well.
    decorator1 = ConcreteDecoratorA(simple)
    decorator2 = ConcreteDecoratorB(decorator1)
    print("Client: Now I've got a decorated component:")
    client_code(decorator2)

最近发表
标签列表