使用装饰器模式有条件地替换行为而不是扩展

时间:2018-09-19 15:41:38

标签: c# oop design-patterns polymorphism decorator

最初我具有以下结构:

interface IFileBackup
{
    void Backup();
}

class BackUpMechanism1 : IFileBackup
{
    void Backup()
    {
        //Back it up
    }
}

class BackUpMechanism2 : IFileBackup
{
    void Backup()
    {
        //Back it up in another way
    }
}

class Client 
{
    //Instantiation of both mechanisms
    //

    try
    {
        backUpMechanism1.Backup();
    }
    catch(Exception ex)
    {
        backupMechanism2.Backup();
    }
}

有人告诉我这不是一个很干净的设计,请使用装饰器模式重新设计它。客户端不应该知道这两种备份机制,而只是调用备份,然后第一种机制应该尝试备份文件,如果失败,则使用机制2。但是我不理解如何使用装饰器模式,因为我的理解是,它可以扩展功能,但不能替代功能-这就是我想要的...如何存档?我尝试了以下方法:

interface IFileBackup
{
    void Backup();
}

class BackupMechanism1 : IFileBackup
{
    public void Backup()
    {
        try
        {
            Console.WriteLine("Trying to back up to the cloud...");
            throw new Exception();
        }
        catch(Exception ex)
        {
            Console.WriteLine("Oops that failed. We need to back up locally instead...");
        }

    }
}

class BackupMechanism2 : IFileBackup
{
    IFileBackup _fileBackup;
    public BackupMechanism2(IFileBackup fileBackup)
    {
        _filebackup = fileBackup;
    }

    public void Backup()
    {
        //All examples I have seen does this. But doesn't make sense in my case?
        _fileBackup.Backup();

        Console.WriteLine("Backing up locally");
    }
}

//The client does not care about how the backup is done
class Client
{
    static void Main()
    {
        //This is not right, but not sure what I should do in the client.
        BackupMechanism2 localBackup = new BackupMechanism2(new BackupMechanism1());
        localBackup.Backup();

        Console.Read();
    }
}

因此,我基本上想要实现的是拥有两种备份机制。让客户说我不关心如何备份。让第一种机制尝试它的备份方法(如果失败),然后尝试第二种方法。我正在尝试使用装饰器模式来扩展(替换)第一种机制的备份行为(如果失败)。我正在努力提出一个有意义的设计。

3 个答案:

答案 0 :(得分:1)

在这种情况下,装饰器模式可用于提供后备实现。您可以在.Net流实现中找到很多明显的示例。

因此请记住,您的代码应如下所示:

class abstract BaseFileBackup
{
  internal BaseFileBackup Fallback;
  internal BaseFileBackup(BaseFileBackup fallback) { Fallback = fallback; }
  internal BaseFileBackup() { }

  internal abstract void DoBackupWork();

  internal void Backup()
  {
    try { DoBackupWork(); }
    catch { if(Fallback != null) Fallback.Backup(); else throw; }
  }
}

class BackUpMechanism1 : BaseFileBackup
{
    internal BackUpMechanism1 (BaseFileBackup fallback): base(fallback) {}
    internal BackUpMechanism1 (): base() {}

    internal void DoBackupWork()
    {
        //Back it up
    }
}

class BackUpMechanism2 : BaseFileBackup
{
    internal BackUpMechanism2 (BaseFileBackup fallback): base(fallback) {}
    internal BackUpMechanism2 (): base() {}

    internal void DoBackupWork()
    {
        //Back it up in another way
    }
}

// and to call it
class Client
{
    static void Main()=>
        new BackupMechanism2(new BackupMechanism1()).Backup();
}

答案 1 :(得分:1)

一种非常干净的实现方法是添加一个IFileBackup对象数组,组成一个IFileBackup复合对象,并逐个尝试它们,直到找到可行的解决方案为止:

class CompositeBackup {
    private readonly IFileBackup[] chain;
    public CompositeBackup(params IFileBackup[] chain) {
        this.chain = chain.ToArray();
    }
    public void Backup() {
        foreach (var backup in chain) {
            try {
                backup.Backup();
                return;
            } catch {
                continue;
            }
        }
        throw new InvalidOperationException();
    }
}

现在,客户只需这样做:

IFileBackup backup = new CompositeBackup(
    new BackupMechanism1()
,   new BackupMechanism2()
);
backup.Backup();

如果您以后决定添加BackupMechanism3BackupMechanism4,则用户将需要向备份链中添加另一个对象。其余代码将保持不变。此外,备份机制本身将不会意识到其他机制的存在,这也简化了代码。

答案 2 :(得分:1)

在这种情况下,装饰器模式是错误的选择。

您在这里处理的问题是

  • 在条件x下调用一个方法
  • 在有条件的情况下调用其他方法
  • ...

这是制定策略模式的前提,而您最初的解决方案已非常接近。我心中的问题是,您正在使用异常来确定程序流,这是一件很糟糕的事情:异常会占用堆栈空间,并且仅应在异常情况下抛出它们。鉴于您的情况,预计给定策略将不起作用

IFileBackupStrategy 
{
    bool Backup(File fileToBackup);
}

IFileBackupContext
{
    File ForBackup { set; }
    bool Backup();
}

class CloudBackUp : IFileBackupStrategy 
{
    private bool _success;

    public bool Backup(File fileToBackup) 
    {
        // code to do backup omitted 
        // it will set the value of _success to false if it was unsuccessful

        return _success;
    }
}   

class LocalBackUp : IFileBackupStrategy 
{
    private bool _success;

    public bool Backup(File fileToBackup) 
    {
        // code to do backup omitted
        // it will set the value of _success to false if it was unsuccessful

        return _success;
    }
}

public class FileBackupContext : IFileBackupContext
{
    private IEnumerable<IFileBackupStrategy> _backupStrategies
    public Context(IEnumerable<IFileBackupStrategy> backupStrategies)
        => _backupStrategies = backupStrategies;

    public File ForBackup { set; private get; }

    public bool Backup()
    {
        bool successFlag;

        foreach(var strategy in _backupStrategies)
        {
            successFlag = strategy.Backup(ForBackup);
            if(successFlag) break;
        }

        return successFlag;
    }
}

在这种情况下,客户端需要了解的只是IFileBackupContext,而不是用于保存的策略。

public class MyBackupClient
{
    private IFileBackupContext _context;

    public MyBackupClient(IFileBackupContext context) => _context = context;

    void SomeMethodThatInvokesBackingUp()
    {
        _context.ForBackup = new File(/* */);

        if(!_context.Backup())
        {
            Console.WriteLine("Failed to backup  the file");
        }
    }
}

此设计的优点在于,您可以添加更多IFileBackupStrategy实现,并在您的DI容器中注册它们,并且可以立即将其提供给客户端,而无需更改任何代码或重新编译(尽管最终取决于关于如何填充DI容器)

装饰器模式是一种遵循SOLID中O原理的方法:

  

为扩展打开,为修改而关闭

这意味着您将使用装饰器模式装饰一个现有类,该类不应更改,但不会表现出所需的行为。提示是该模式的名称: Decorator (装饰器) 添加某些内容,它不会更改任何内容。

装饰器模式是一种结构模式,而策略模式以及您正在寻找的是行为模式

当然可以扩展此示例以报告最终采用的策略,以及(如果需要)不采用替代策略的任何原因。

  

已编辑:针对下面Blindy的评论。这是装饰器模式的范例,该模式应证明它不是解决此问题的正确模式:

 class Image 
 {
    void Render() { /*  */ }
 }

 class FramedImage : Image 
 { 
    private Image _originalImage;
    public FramedImage(Image original) => _originalImage = original;

    new public void Render()
    {
        /* code to render a frame */
        _originalImage.Render();
    }
 }

Image originalImage = new Image();
Image framedImage = new FramedImage(originalImage);

Image toRender = originalImage;
toRender.Render() // Renders the original image

toRender = framedImage;
toRender.Render(); // Renders the original image in a frame

应该注意,没有 来分配每个Image到toRender变量,这样做仅仅是为了证明装饰器 装饰的。

从该示例中可以看到,装饰器模式添加了行为,,并且它还调用了装饰项目的行为

  

已编辑:针对下面DSF提出的问题。这是一个控制台应用程序的完整清单,展示了如何使用Unity 5.8.6实现此目标。

该代码利用了C#7.0中的新元组。 我刚刚使用了一些随机数生成方法来确定每种策略实现是否都能成功执行其任务。

using System;
using System.Collections.Generic;
using System.IO;
using Unity;
using Unity.Injection;

namespace StrategyPattern
{
   public interface IFileBackupContext
   {
      FileStream ForBackup { set; }
      (bool success, string strategy) Backup();
   }

   public interface IFileBackupStrategy 
   {
      (bool success, string name) Backup(FileStream fileToBackup);
   }

   internal class LocalBackUp : IFileBackupStrategy
   {
      private bool _success = false;

      public (bool success, string name) Backup(FileStream fileToBackup)
      {
         // code to do backup omitted
         var random = new Random(DateTime.Now.Millisecond);
         _success = (random.Next() % 3) == 0;
         if(_success) fileToBackup.Close();
         // it will set the value of _success to false if it was unsuccessful
         return (_success, "LocalBackUp");
      }
   }

   internal class CloudBackUp : IFileBackupStrategy
   {
      private bool _success = false;

      public (bool success, string name) Backup(FileStream fileToBackup)
      {
         // code to do backup omitted 
         var random = new Random(DateTime.Now.Millisecond);
         _success = (random.Next() % 3) == 0;
         if (_success) fileToBackup.Close();
         // it will set the value of _success to false if it was unsuccessful

         fileToBackup.Close();
         return (_success, "CloudBackUp");
      }
   }

   public class FileBackupContext : IFileBackupContext
   {
      private readonly IEnumerable<IFileBackupStrategy> _backupStrategies;

      public FileBackupContext(IEnumerable<IFileBackupStrategy> backupStrategies)
         => _backupStrategies = backupStrategies;

      public FileStream ForBackup { set; private get; }

      public (bool success, string strategy) Backup()
      {
         foreach (var strategy in _backupStrategies)
         {
            var (success, name) = strategy.Backup(ForBackup);
            if (success) return (true, name);
         }

         return (false, "");
      }
   }

   public class MyBackupClient
   {
      private IFileBackupContext _context;

      public MyBackupClient(IFileBackupContext context) => _context = context;

      public void BackgUpMyFile()
      {
         _context.ForBackup = new FileStream("d:\\myfile", FileMode.OpenOrCreate);

         (bool success, string strategy) = _context.Backup();

         if (!success)
         {
            Console.WriteLine("Failed to backup  the file");
            return;
         }

         Console.WriteLine($"File backed up using [{strategy}] strategy");
      }
   }

   public class Bootstrap
   {
      private readonly IUnityContainer _container;
      public Bootstrap()
      {
         _container = new UnityContainer();


         _container.RegisterType<IFileBackupContext, FileBackupContext>();
         _container.RegisterType<IFileBackupStrategy, LocalBackUp>("local");
         _container.RegisterType<IFileBackupStrategy, CloudBackUp>("cloud");
         _container.RegisterType<MyBackupClient>();

         _container.RegisterType<Func<IEnumerable<IFileBackupStrategy>>>(new InjectionFactory(c =>
            new Func<IEnumerable<IFileBackupStrategy>>(() =>
               new[]
               {
                  c.Resolve<IFileBackupStrategy>("local"),
                  c.Resolve<IFileBackupStrategy>("cloud")
               }
            )));
      }

      public MyBackupClient GetClient() => _container.Resolve<MyBackupClient>();
   }

   class Program
   {
      static void Main(string[] args)
      {
         Console.WriteLine("Press ESC to quit ...");
         Console.WriteLine("Press any other key to try again.");
         Console.WriteLine();
         var client = new Bootstrap().GetClient();
         do
         {
            client.BackgUpMyFile();
         } while (Console.ReadKey().Key != ConsoleKey.Escape);
      }
   }
}