装饰设计模式与继承

Decorator design pattern vs. inheritance?

本文关键字:继承 设计模式      更新时间:2023-10-16

我已经阅读了维基百科上的装饰器设计模式,以及这个网站上的代码示例。

我认为传统继承遵循的是is-a模式,而decorator遵循的是has-a模式。装饰师的称呼惯例看起来像是"皮肤"之上的"皮肤"。。超过"核心"。例如

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

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

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

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

  2. 从代码示例链接中,我可以看到类定义的数量在两种实现中几乎相同。我记得在其他一些设计模式书中,比如《头先设计模式》。他们以星巴克咖啡为例,并表示传统继承将导致"类爆炸",因为对于每种咖啡组合,你都会为其找到一个类。

    但在这种情况下,装饰师不是也一样吗?如果一个decorator类可以接受ANY抽象类并对其进行装饰,那么我想它确实可以防止爆炸,但从代码示例来看,您有确切的类定义#,不亚于。。。

有人能解释一下吗?

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

使用decorator(伪代码):

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) 使用decorator,您可以根据需要在运行时组合功能。每个类只处理功能的一个方面(压缩、加密…)

2) 在这个简单的例子中,我们有3个类带有decorator,5个类带有继承。现在让我们添加更多的服务,例如过滤和剪辑。使用decorator,您只需要再增加2个类就可以支持所有可能的场景,例如过滤->剪切->压缩->加密。对于继承,您需要为每个组合提供一个类,这样您最终会得到几十个类。

按相反顺序:

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

1) 假设您在运行时有1024个子类可供选择。试着勾画出所需的代码。请记住,您可能无法指定选择或拒绝选项的顺序。还要记住,在扩展实例之前,您可能需要使用一段时间。继续,尝试。相比之下,使用decorator进行操作是微不足道的。

你说得对,它们有时可能非常相似。这两种解决方案的适用性和好处将取决于您的情况。

对于你的第二个问题,其他人击败了我,使我得到了充分的答案。简而言之,您可以组合装饰器来实现继承无法实现的更多组合。

因此,我专注于第一个:

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

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

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

现在假设您开始一个复杂的任务,该任务涉及许多对象,并且每个对象都进行日志记录,因此您可以传递日志记录对象。但是,您希望任务中的每条消息都以任务名称和任务Id为前缀。您可以传递一个函数,或者传递名称和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++的人

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

首先,我是一个C#用户,已经有一段时间没有处理过C++了,但希望你能理解我的想法。

脑海中浮现的一个很好的例子是DbRepositoryCachingDbRepository:

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();
    }
}

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

通过使用decorator模式,我仍然有相同数量的类,但我的CachingDbRepository接收一个IRepository,并调用它的GetStuff(),以便在数据不在缓存中的情况下从底层repo获取数据。

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

我发现什么时候决定只使用继承而不是装饰是主观的。

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