18

我有一个子类DbContext

public class MyContext : DbContext { }

我有一个围绕该实现的IUnitOfWork抽象,以确保在适当的时间处理诸如此类的引用MyContextIDisposableMyContext

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

MyUnitOfWork在每个(网络)请求的生命周期范围内注册。我有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()包含它封装的实例,而装饰不是封装。
4

3 回答 3

15

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

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

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

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

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

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

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

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

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

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

于 2013-07-30T10:59:18.597 回答
7

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

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

如果您通过将其注入来将其创建MyContext移出,您甚至可以简化为以下内容:UnitOfWorkUnitOfWorkUnitOfWork

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

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

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

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

于 2013-07-30T13:32:41.067 回答
4

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

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

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

于 2013-07-30T10:10:22.377 回答