多接口装饰模式

5

我一直在为装饰器和接口而苦恼。假设我有以下“行为”接口:

interface IFlyable { void Fly();}
interface ISwimmable { void Swim();}

一个主要界面
interface IMainComponent { void DoSomethingA(); void DoSomethingB();}

主接口上的装饰器

    public class Decorator : IMainComponent
    {
        private readonly IMainComponent decorated;
        [..]

        public virtual void DoSomethingA()
        {
            decorated.DoSomethingA();
        }

        public virtual void DoSomethingB()
        {
            decorated.DoSomethingB();
        }
    }

我的问题是如何将装饰对象实现的所有接口转发给装饰器。解决方案是使装饰器实现这些接口:
    public class Decorator : IMainComponent, IFlyable, ISwimmable
    {
        [..]

        public virtual void Fly()
        {
            ((IFlyable)decorated).Fly();
        }

        public virtual void Swim()
        {
            ((ISwimmable)decorated).Swim();
        }

但我不喜欢它,因为:

  1. 这可能看起来像“装饰器”实现了一个接口,但实际上并非如此(在运行时会发生强制转换异常)
  2. 这不可扩展,每次添加新接口时都需要进行更改(不能忘记此添加)

另一种解决方案是添加“手动转换”,使其在装饰树中传播:

    public class Decorator : IMainComponent
    {
        public T GetAs<T>()
            where T : class
        {
            //1. Am I a T ?
            if (this is T)
            {
                return (T)this;
            }

            //2. Maybe am I a Decorator and thus I can try to resolve to be a T
            if (decorated is Decorator)
            {
                return ((Decorator)decorated).GetAs<T>();
            }

            //3. Last chance
            return this.decorated as T;
        }

但问题在于:
  1. 调用者可以在调用GetAs()后操纵包装对象。
  2. 如果在GetAs()之后使用IMainComponent的方法(例如((IMainComponent)GetAs()).DoSomethingB();),这可能会导致混淆/不希望的行为,因为这可能调用包装对象的实现,而不是完整的装饰。
  3. 必须调用GetAs()方法,使用转换/常规"As"退出代码将无法工作。
如何解决这个问题?是否有解决此问题的模式?
注:我的问题是针对C#最终实现的,但解决方案可能更加广泛。

2
你想要实现什么?有现实生活中的例子吗? - devdigital
1
decorated 的数据类型是什么?它在哪里声明的?你的问题不太清楚。而且 decorator 和 decorated 之间存在混淆。 - Chetan
为什么在装饰器中要实现接口?MainComponent 应该实现所有接口,而 Decorator 扩展了 MainComponent 对象。因此 Decorator: MainComponent { override Fly... } - Renatas M.
@devdigital:我遇到过这个问题好几次了。最近的一个实际情况是创建一个组件,它可以在不同的触发器(ITimedComponent、IFileChangedComponent、IFrequencyComponent等)上执行其工作。装饰器用于记录日志,在执行/存储指标之前/之后执行数据冻结等操作。 - Toto
@ChetanRanpariya decorated 是一个 IMainComponent 实例。这段代码没有完整,我进行了修改。 - Toto
@Reniuz:我的问题比较广泛,并且IMainComponent可能与接口没有任何关系。在我的具体用例中,我们通常实际上是相反的(每个接口都是一个IMainComponent(IFlyable:IMainComponent,ISwimmable))。 - Toto
3个回答

3
您需要为每个接口创建单独的装饰器。另一种选择是使用通用服务接口和通用装饰器。例如:
public interface ICommandService<TCommand>
{
   Task Execute(TCommand command);
}

public class LoggingCommandService<TCommand> : ICommandService<TCommand>
{
  public LoggingCommandService(ICommandService<TCommand> command, ILogger logger)
  {
    ...
  }

  public async Task Execute(TCommand command)
  {
    // log
    await this.command.Execute(command);
    // log
  }
}

1
我认为你正在朝着服务定位器模式的方向前进,这是一种严重的反模式。如果您依赖一个类似许愿井的接口,您可以请求其中任何内容,则您拥有服务定位器。我认为这正是您的 GetAs 带您到的地方。
服务定位器被认为是一种反模式,因为它隐藏了一个类所具有的依赖关系。相反,您只看到服务定位器作为单个依赖项,但您不会立即看到将要调用哪些依赖项。
如果您要求实现,则建议使用依赖注入框架。市场上有很多这样的框架,如MEF、Ninject、Unity、Windsor、DryIoC、SimpleInject、LightInjector、Grace、Stashbox等等,这仅是我能想到的一些。
装饰器的作用完全不同。如果您不仅转发接口调用,而且还添加了一些额外的逻辑(例如重试行为),则使用装饰器。在这种情况下,您仍将限制自己使用原始接口的方法。

0

装饰器模式并不适用于向被装饰对象添加新方法。这正是您试图做的事情,在静态类型语言中优雅地完成这个任务是不可能的。

装饰器的有用之处在于,装饰器和被装饰组件的接口相同,并且装饰器在将请求沿着装饰器链传递之前或之后为方法添加了一些额外的功能。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接