【问题标题】:Using IoC and Dependency Injection, how to wrap code with a new layer of implementation without violating the Open-Closed principle?使用 IoC 和依赖注入,如何在不违反 Open-Closed 原则的情况下,用新的实现层包装代码?
【发布时间】:2011-01-29 19:53:08
【问题描述】:

我正试图弄清楚这在实践中是如何做到的,以免违反开放封闭原则。

假设我有一个名为 HttpFileDownloader 的类,它有一个函数,它接受一个 url 并下载一个将 html 作为字符串返回的文件。这个类实现了一个只有一个函数的 IFileDownloader 接口。因此,在我的整个代码中,我都引用了 IFileDownloader 接口,并且每当 IFileDownloader 被解析时,我的 IoC 容器都会返回一个 HttpFileDownloader 实例。

然后经过一段时间的使用,发现有时候服务器太忙了,会抛出异常。我决定要解决这个问题,如果遇到异常,我将自动重试 3 次,并在每次重试之间等待 5 秒。

所以我创建了 HttpFileDownloaderRetrier,它有一个函数在一个 for 循环中使用 HttpFileDownloader,最多 3 个循环,每个循环之间等待 5 秒。为了测试 HttpFileDownloadRetrier 的“重试”和“等待”能力,我通过让 HttpFileDownloaderRetrier 构造函数采用 IFileDownloader 来注入 HttpFileDownloader 依赖项。

所以现在我希望 IFileDownloader 的所有 Resolving 都返回 HttpFileDownloaderRetrier。但如果我这样做,那么 HttpFileDownloadRetrier 的 IFileDownloader 依赖项将获得一个自身的实例,而不是 HttpFileDownloader 的实例。

所以我可以看到我可以为 HttpFileDownloader 创建一个名为 IFileDownloaderNoRetry 的新接口,并更改 HttpFileDownloader 来实现它。但这意味着我正在更改违反 Open Closed 的 HttpFileDownloader。

或者我可以为 HttpFileDownloaderRetrier 实现一个名为 IFileDownloaderRetrier 的新接口,然后将我的所有其他代码更改为引用该接口而不是 IFileDownloader。但是,我现在在所有其他代码中都违反了 Open Closed。

那么我在这里错过了什么?如何在不更改现有代码的情况下用新的实现层(重试和等待)包装现有实现(下载)?

如果有帮助,这里有一些代码:

public interface IFileDownloader
{
  string Download(string url);
}

public class HttpFileDownloader : IFileDownloader
{
  public string Download(string url)
  {
    //Cut for brevity - downloads file here returns as string
    return html;
  }
}

public class HttpFileDownloaderRetrier : IFileDownloader
{
  IFileDownloader fileDownloader;

  public HttpFileDownloaderRetrier(IFileDownloader fileDownloader)
  {
    this.fileDownloader = fileDownloader;
  }

  public string Download(string url)
  {
    Exception lastException = null;
    //try 3 shots of pulling a bad URL.  And wait 5 seconds after each failed attempt.
    for (int i = 0; i < 3; i++)
    {
      try { fileDownloader.Download(url); }
      catch (Exception ex) { lastException = ex; }
      Utilities.WaitForXSeconds(5);
    }
    throw lastException;
  }
}

【问题讨论】:

    标签: c# dependency-injection ioc-container open-closed-principle


    【解决方案1】:

    直接从HttpFileDownloader派生怎么样:

    public class HttpFileDownloader : IFileDownloader
    {
        public virtual string Download(string url)
        {
            //Cut for brevity - downloads file here returns as string
            return html;
        }
    }
    
    public class HttpFileDownloaderWithRetries : HttpFileDownloader
    {
        private readonly int _retries;
        private readonly int _secondsBetweenRetries;
    
        public HttpFileDownloaderWithRetries(int retries, int secondsBetweenRetries)
        {
            _retries = retries;
            _secondsBetweenRetries = secondsBetweenRetries;
        }
    
        public override string Download(string url)
        {
            Exception lastException = null;
            for (int i = 0; i < _retries; i++)
            {
                try 
                { 
                    return base.Download(url); 
                }
                catch (Exception ex) 
                { 
                    lastException = ex; 
                }
                Utilities.WaitForXSeconds(_secondsBetweenRetries);
            }
            throw lastException;
        }
    }
    

    【讨论】:

    • 继承是我正常的默认操作模式,所以这确实是我的第一个直觉反应,但我正在尝试学习使用组合而不是继承。所以在这种情况下,从 HttpFileDownloader 继承意味着我不能在单元测试中存根/模拟 HttpFileDownloader 并仅测试循环和等待功能。
    【解决方案2】:

    您或多或少地实现了 Circuit Breaker 设计模式。与往常一样,在使用 DI 实现横切关注点时,关键是应用Decorator 模式。

    像这样写一个 CircuitBreakingFileDownloader:

    public class CircuitBreakingFileDownloader : IFileDownloader
    { 
        private readonly IFileDownloader fileDownloader;
    
        public CircuitBreakingFileDownloader(IFileDownloader fileDownloader)
        {
            if (fileDownloader == null)
            {
                throw new ArgumentNullException("fileDownloader");
            }
    
            this.fileDownloader = fileDownloader;
        }
    
        public string Download(string url)
        {
            // Apply Circuit Breaker implementation around a call to
            this.fileDownloader.Download(url)
            // here...
        }
    } 
    

    这种方法遵循开放/封闭原则组合优于继承。它还满足单一责任原则,因为断路器只处理该方面,而装饰的 IFileDownloader 则专注于自己的责任。

    大多数正确的 DI 容器都了解装饰器模式,因此您现在可以配置容器以通过返回包含真实 HttpFileDownloader 的 CircuitBreakingFileDownloader 来解析对 IFileDownloader 的请求。

    事实上,这种方法可以泛化很多,您可以研究一个通用的断路器拦截器Here's an example that uses Castle Windsor.

    【讨论】:

    • 谢谢。查看 DI Container 的建议解决了这个问题。我使用的是 Castle Windsor,所以我只需要将 HttpFileDownloaderRetrier IFileDownloader 参数配置为配置文件中的 HttpFileDownloader。奇怪的是,这个关于使用 Castle Windsor 进行控制反转和依赖注入的页面几乎正是我所需要的。 dotnetslackers.com/articles/designpatterns/…
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-09-15
    • 1970-01-01
    • 1970-01-01
    • 2011-10-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多