24

我已经阅读了来自 Wikipedia 的装饰器设计模式,以及来自这个站点的代码示例。

我认为传统继承遵循“is-a”模式,而装饰器遵循“has-a”模式。装饰器的调用约定看起来像是在“皮肤”之上的“皮肤”……在“核心”之上。例如

I* anXYZ = new Z( new Y( new X( new A ) ) );

如上面的代码示例链接所示。

但是,我仍然有几个问题我不明白:

  1. wiki 所说的“装饰器模式可用于在运行时扩展(装饰)某个对象的功能”是什么意思?'new ...(new... (new...))' 是一个运行时调用,很好,但是一个 'AwithXYZ anXYZ;' 是编译时的继承并且不好?

  2. 从代码示例链接中,我可以看到两个实现中类定义的数量几乎相同。我记得在其他一些设计模式书籍中,比如“Head first design patterns”。他们以 starbuzz coffee 为例,说传统继承会导致“类爆炸”,因为对于咖啡的每种组合,你都会为它想出一个类。

    但是在这种情况下,装饰器不一样吗?如果装饰器类可以采用任何抽象类并对其进行装饰,那么我想它确实可以防止爆炸,但是从代码示例中,您有确切的类定义#,不少于......

有人会解释吗?

4

5 回答 5

71

让我们以一些抽象流为例,假设您想为它们提供加密和压缩服务。

使用装饰器(伪代码):

Stream plain = Stream();
Stream encrypted = EncryptedStream(Stream());
Stream zipped = ZippedStream(Stream());
Stream zippedEncrypted = ZippedStream(EncryptedStream(Stream());
Stream encryptedZipped = EncryptedStream(ZippedStream(Stream());

通过继承,您可以:

class Stream() {...}
class EncryptedStream() : Stream {...}
class ZippedStream() : Stream {...}
class ZippedEncryptedStream() : EncryptedStream {...}
class EncryptedZippedStream() : ZippedStream {...}

1)使用装饰器,您可以根据需要在运行时组合功能。每个类只处理一个方面的功能(压缩、加密……)

2) 在这个简单的例子中,我们有 3 个带有装饰器的类,5 个带有继承的类。现在让我们添加更多服务,例如过滤和剪辑。使用装饰器,您只需要另外 2 个类来支持所有可能的场景,例如过滤 -> 剪辑 -> 压缩 -> 加密。使用继承,您需要为每个组合提供一个类,因此您最终会得到数十个类。

于 2012-09-12T01:02:54.110 回答
8

以相反的顺序:

2) 比如说,有 10 个不同的独立扩展,在运行时可能需要它们的任意组合,10 个装饰器类就可以完成这项工作。要通过继承涵盖所有可能性,您需要1024个子类。而且没有办法绕过大量的代码冗余。

1) 假设您在运行时有 1024 个子类可供选择。尝试勾勒出需要的代码。请记住,您可能无法指定选择或拒绝选项的顺序。还要记住,在扩展实例之前,您可能需要使用一段时间。来吧,试试。相比之下,用装饰器做这件事是微不足道的。

于 2012-09-12T01:05:03.813 回答
3

您说得对,它们有时可能非常相似。任一解决方案的适用性和优势将取决于您的具体情况。

其他人击败了我,对你的第二个问题给出了足够的答案。简而言之,您可以组合装饰器来实现更多的组合,而继承是无法做到的。

因此,我专注于第一个:

您不能严格地说编译时不好而运行时好,只是灵活性不同。在运行时更改内容的能力对于某些项目来说可能很重要,因为它允许在不重新编译的情况下进行更改,这可能很慢,并且需要您处于可以编译的环境中。

不能使用继承的一个例子是当你想向实例化对象添加功能时。假设为您提供了一个实现日志接口的对象实例:

public interface ILog{
    //Writes string to log
    public void Write( string message );
}

现在假设您开始一项涉及许多对象的复杂任务,并且每个对象都进行日志记录,因此您传递了日志记录对象。但是,您希望任务中的每条消息都以任务名称和任务 ID 为前缀。您可以传递一个函数,或者传递 Name 和 Id 并相信每个调用者都遵循预先挂起该信息的规则,或者您可以在传递它之前装饰日志对象,而不必担心其他对象会这样做对的

public class PrependLogDecorator : ILog{

     ILog decorated;

     public PrependLogDecorator( ILog toDecorate, string messagePrefix ){
        this.decorated = toDecorate;
        this.prefix = messagePrefix;
     }

     public void Write( string message ){
        decorated.Write( prefix + message );
     }
}

对 C# 代码感到抱歉,但我认为它仍会将这些想法传达给了解 C++ 的人

于 2012-09-12T01:18:30.517 回答
1

要解决您问题的第二部分(这可能反过来解决您的第一部分),使用装饰器方法您可以访问相同数量的组合,但不必编写它们。如果你有 3 层装饰器,每层有 5 个选项,你就有5*5*5可能使用继承定义类。使用装饰器方法你需要 15.

于 2012-09-12T00:53:29.433 回答
1

首先,我是一个 C# 人,有一段时间没有接触过 C++,但希望你能明白我的出发点。

想到的一个很好的例子是 aDbRepository和 a CachingDbRepository

public interface IRepository {
  object GetStuff();
}

public class DbRepository : IRepository {

  public object GetStuff() {
    //do something against the database
  }
}

public class CachingDbRepository : IRepository {
  public CachingDbRepository(IRepository repo){ }

  public object GetStuff() {
    //check the cache first
    if(its_not_there) {
      repo.GetStuff();
    }
}

所以,如果我只是使用继承,我会有 aDbRepository和 a CachingDbRepository。从数据库中DbRepository查询;将CachingDbRepository检查其缓存,如果数据不存在,它将查询数据库。所以这里有一个可能的重复实现。

通过使用装饰器模式,我仍然拥有相同数量的类,但是如果它不在缓存中,我CachingDbRepository会接收IRepository并调用它以从底层存储库中获取数据。GetStuff()

所以类的数量是相同的,但是类的使用是相关的。 CachingDbRepo调用传递给它的回购......所以它更像是组合而不是继承。

我发现何时决定何时使用继承而不是装饰是主观的。

我希望这有帮助。祝你好运!

于 2012-09-12T01:01:24.110 回答