【问题标题】:Decorators and IDisposable装饰器和 IDisposable
【发布时间】:2013-07-30 09:55:45
【问题描述】:

我有一个DbContext 的子类

public class MyContext : DbContext { }

我在MyContext 周围有一个IUnitOfWork 抽象,它实现了IDisposable,以确保在适当的时间处理MyContext 等引用

public interface IUnitOfWork : IDisposable { }

public class UnitOfWork : IUnitOfWork 
{
    private readonly MyContext _context;

    public UnitOfWork()
    {
        _context = new MyContext();
    }

    ~UnitOfWork()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private bool _disposed;

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;

        if (disposing)
        {
            if (_context != null) _context.Dispose();
        }

        _disposed = true;
    }
}

我的UnitOfWork 注册了每个(网络)请求的生命周期范围。我有IUnitOfWork 的装饰器可以注册为瞬态或生命周期范围,我的问题是他们应该如何实现IDisposable - 特别是他们应该还是不应该将调用传递给Dispose()

public class UnitOfWorkDecorator : IUnitOfWork
{
    private readonly IUnitOfWork _decorated;

    public UnitOfWorkDecorator(IUnitOfWork decorated)
    {
        _decorated = decorated;
    }

    public void Dispose()
    {
        //do we pass on the call?
        _decorated.Dispose();
    }
}    

我看到了 2 个选项(我猜选项 2 是正确答案):

  1. 希望每个装饰器都知道它是瞬态的还是生命周期的。如果装饰器是瞬态的,那么它不应该在被装饰的实例上调用Dispose()。如果它是生命周期范围的,它应该。
  2. 每个装饰器应该只关心自己的处理,并且应该从不将调用传递给被装饰的实例。容器将在适当的时候为调用链中的每个对象管理对Dispose() 的调用。一个对象应该只封装Dispose() 个实例,而装饰不是封装。

【问题讨论】:

  • 很好的问题。此外,您应该在 Dispose 方法中将 _context 变量设置为 null,就在您处理完该上下文之后。
  • 我同意@Maarten:一个非常棒且非常有趣的问题。

标签: c# decorator ioc-container simple-injector


【解决方案1】:

[装饰者] 在实现 IDisposable 方面应该做什么

这又回到了所有权的一般原则。问问自己:“谁拥有这种一次性类型?”。这个问题的答案是:拥有该类型的人负责处置它。

由于一次性类型是从外部传递给装饰器的,因此装饰器没有创建该类型,通常不应该负责清理它。装饰器无法知道是否应该处理该类型(因为它不控制其生命周期),这在您的情况下非常清楚,因为装饰器可以注册为瞬态,而被装饰者的生命周期要长得多.在你的情况下,如果你从装饰器中处理被装饰者,你的系统就会崩溃。

所以装饰者永远不应该处置被装饰者,仅仅因为它不拥有被装饰者。您的Composition Root 有责任处置该装饰者。在这种情况下,我们谈论装饰器并不重要。它仍然归结为所有权的一般原则。

每个装饰器都应该只关心处理自己,并且应该 永远不要将调用传递给装饰实例。

正确。装饰器应该处理它拥有的所有东西,但由于您使用依赖注入,它通常不会自己创建太多东西,因此不拥有这些东西。

另一方面,您的 UnitOfWork 创建了一个新的 MyContext 类,因此拥有该实例的所有权,它应该处置它。

这条规则也有例外,但它仍然归结为所有权。有时您确实会将类型的所有权传递给其他人。例如,当使用工厂方法时,按照惯例,工厂方法将创建对象的所有权传递给调用者。有时所有权会传递给创建的对象,例如 .NET 的 StreamReader 类。 API 文档对此很清楚,但由于设计如此不直观,开发人员一直在为这种行为绊倒。 .NET 框架中的大多数类型都不是这样工作的。例如,SqlCommand 类不会释放 SqlConnection,如果它确实释放连接会很烦人。

看待这个问题的另一种方式是从SOLID principles 的角度。通过让IUnitOfWork 实现IDisposable 你违反了Dependency Inversion Principle,因为“抽象不应该依赖于细节;细节应该依赖于抽象”。通过实现IDisposable,您将实现细节泄漏到IUnitOfWork 接口中。实现IDisposable 意味着该类具有需要处置的非托管资源,例如文件句柄和连接字符串。这些是实现细节,因为这种接口的每个实现几乎都不需要处理。你只需要为你的单元测试创​​建一个假的或模拟的实现,并且你有一个不需要处理的实现的证据。

因此,当您通过从 IUnitOfWork 中删除 IDisposable 接口并将其移至实现来修复此 DIP 违规时,装饰器将无法处置被装饰者,因为它无法知道是否不是装饰者实现IDisposable。这很好,因为根据 DIP,装饰者不应该知道 - 而且 - 我们已经确定装饰者不应该处置被装饰者。

【讨论】:

  • 您一如既往地提供了如此好的答案。我想我爱上了你:)。
  • 有粉丝总是很高兴:-)
  • @Steven 那么我如何确保在UnitOfWork 超出范围时处理诸如IDbConnection 之类的东西(可能是蹩脚的例子,但它仍然是一个例子)?
  • @qujck:UnitOfWork 实现可以(并且应该)仍然实现IDisposable。创建该实例的人可以(并且应该)处置它。但是IUnitOfWork 接口不应该泄露这些实现细节(根据 DIP)。
  • Steven,当您无法从被装饰的接口中删除IDisposable 接口实现时,您有什么建议?假设您正在从框架或某个库中装饰本机接口,并且该接口还实现了IDisposable。使用 Scrutor,我怀疑有人 需要 在装饰器中实现 Dispose 并传递给内部实例,因为装饰器注册在容器上被覆盖,唯一剩下的实例是装饰器(因此容器不可能处置内部对象)。
【解决方案2】:

不是答案,但您的UnitOfWork 可以简化很多。

  • 由于类本身没有任何本地资源,因此不需要终结器。因此可以删除终结器。
  • IDisposable 接口的合约规定Dispose 被多次调用是有效的。这不应导致异常或任何其他可观察到的行为。因此,您可以删除 _disposed 标志和 if (_disposed) 检查。
  • _context 字段总是在构造函数成功时被初始化,而Dispose 永远不能在构造函数抛出异常时被调用。因此,if (_context != null) 检查是多余的。由于DbContext 可以安全地多次处理,因此无需将其无效。
  • 仅当打算继承类型时才需要实现 Dispose 模式(使用受保护的Dispose(bool) 方法)。该模式对于作为可重用框架一部分的类型特别有用,因为无法控制谁从该类型继承。如果您将此类型设为sealed,则可以安全地删除受保护的Dispose(bool) 方法并将其逻辑移至公共Dispose() 方法中。
  • 由于该类型不包含终结器且无法被继承,您可以移除对GC.SuppressFinalize 的调用。

按照这些步骤,这是UnitOfWork 类型的剩余部分:

public sealed class UnitOfWork : IUnitOfWork, IDisposable
{
    private readonly MyContext _context;

    public UnitOfWork()
    {
        _context = new MyContext();
    }

    public void Dispose()
    {
        _context.Dispose();
    }
}

如果您通过将UnitOfWork 注入UnitOfWorkMyContext 的创建移出UnitOfWork,您甚至可以将UnitOfWork 简化为以下内容:

public sealed class UnitOfWork : IUnitOfWork 
{
    private readonly MyContext _context;

    public UnitOfWork(MyContext context)
    {
        _context = context;
    }
}

由于UnitOfWork 接受MyContext 它没有所有权,因此不允许处置MyContext(因为即使在UnitOfWork 超出范围后,其他消费者可能仍需要使用它) .这意味着UnitOfWork 不需要处理任何东西,因此不需要实现IDisposable

这当然意味着我们将处理MyContext 的责任转移到“其他人”身上。这个“某人”通常与控制UnitOfWork 的创建和处置的人相同。通常这是Composition Root

【讨论】:

  • 文章模式的实现几乎直接来自框架设计指南,但有细微的差异,并且在第二版中改变了一些指导。虽然应用模式很重要,但更重要的是理解它们。 dispose 模式是为继承层次结构设计的。如果您不需要这样的层次结构,事情就会变得容易得多。如果您没有原生资源,事情也会变得更容易。
  • 也看看 cmets。我所说的似乎有很多共识:-)
【解决方案3】:

就个人而言,我怀疑您需要根据具体情况进行处理。一些装饰器可能有充分的理由来理解作用域;对于大多数人来说,简单地传递它可能是一个很好的默认设置。很少有人应该明确处置链 - 我见过的主要时间是专门用来抵消另一个装饰器应该考虑作用​​域的情况:没有(总是被丢弃)。

作为一个相关的例子——考虑像GZipStream这样的事情——对于大多数人来说,他们只处理一个逻辑块——所以默认“处理流”就可以了;但是这个决定可以通过constructor overload 获得,它可以让你告诉它如何表现。在带有可选参数的最新版本的 C# 中,这可以在单个构造函数中完成。

选项 2 是有问题的,因为它要求您(或容器)跟踪所有中间对象;如果您的容器方便地做到这一点,那很好 - 但还要注意它们必须以正确的顺序放置(从外到内)。因为在装饰器链中,可能存在待处理的操作 - 计划在请求时向下游刷新,或者(作为最后手段)在处置期间。

【讨论】:

  • 我几乎从不反对你的 cmets,但这次我真的同意了。但我仍然需要给你+1,因为在阅读了你的答案后,我意识到 Simple Injector 中有一个错误。 S.I. 以错误的顺序处理实例。正如您所说,实例必须从外部到内部进行处理,但这不是 v2.3 中发生的情况。
  • @Steven 出于好奇,你不同意哪一点?
  • 你看我的回答了吗?您将GZipStream 作为一个类的示例,该类处理它从外部获取的资源,但是这种设计违背了基本的框架设计准则,并且与大多数实现.NET 的工作方式相反。如果没有容器,您将使用一系列 using 语句来处理嵌套图,并且很少有实现实际上应该处理链,因为这是用户通常不会期望的行为并且不允许重用输出对象。内部对象根本不知道它们的父对象是否应该被重用。
  • @Steven 正如您在回答中所说的那样,关键点是:我们何时(如果有的话)认为所有权已经转移。事实上,BCL 的某些部分确实对此做出了假设,这正是我想要强调的。
  • 确实如此。有时我们确实(需要)转让所有权,但 IMO 应该很少见。即使在《框架设计指南》一书中,评论者之间也讨论了这种模式(尽管 Brad Abrams 确实描述了这种“包装到拥有”模式here)。尽管如此,这种与规范的偏差,我个人一直被这种“设计怪癖”绊倒。 BCL 团队实际上在使这些类型在他们的常见用例中真正易于使用和让更有经验的开发人员感到困惑之间进行了权衡。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-11-24
  • 2011-06-06
  • 2015-03-04
  • 2017-03-07
  • 2014-03-07
  • 2017-02-14
  • 2012-12-25
相关资源
最近更新 更多