Декораторы и IDisposable

У меня есть подкласс DbContext

public class MyContext : DbContext { }

и у меня есть абстракция IUnitOfWork вокруг MyContext, которая реализует 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 - правильный ответ):

  • Ожидается, что каждый Decorator будет знать, является ли он временным или охваченным временем. Если декоратор временно, то он не должен вызывать Dispose() на украшенном экземпляре. Если это срок действия, он должен.
  • Каждый декоратор должен заботиться только об утилизации себя и никогда передавать вызов украшенному экземпляру. Контейнер будет управлять вызовом Dispose() для каждого объекта в цепочке вызовов в соответствующее время. Объект должен только Dispose() экземпляров, которые он инкапсулирует и украшает, не является инкапсуляцией.

Ответы

Ответ 1

что должны делать [декораторы] в отношении реализации IDisposable

Это возвращается к общему принципу собственности. Спросите себя: "кто владеет этим одноразовым типом?". Ответ на этот вопрос: тот, кто владеет типом, несет ответственность за его удаление.

Так как одноразовый тип передается декоратору снаружи, декоратор не создает этот тип и обычно не несет ответственности за его очистку. Декоратор не имеет никакого способа узнать, должен ли быть выбран тип (поскольку он не контролирует его срок службы), и это очень ясно в вашем случае, поскольку декоратор может быть зарегистрирован как переходный, в то время как у декоратора намного больше времени жизни. В вашем случае ваша система просто сломается, если вы разместите декор изнутри декоратора.

Поэтому декоратор никогда не должен распоряжаться декоратором, просто потому, что он не владеет декоратором. Это обязанность вашего корня композиции распоряжаться этим украшением. Не имеет значения, что мы говорим об декораторах в этом случае; это все еще сводится к общему принципу собственности.

Каждый декоратор должен заботиться только об утилизации себя и должен   никогда не переходите к призыву к украшенному экземпляру.

Правильно. Декоратор должен распоряжаться всем, чем он владеет, но, поскольку вы используете инъекцию зависимостей, он обычно не создает много материала и, следовательно, не владеет этим материалом.

С другой стороны, ваш UnitOfWork создает новый класс MyContext и, следовательно, имеет право собственности на этот экземпляр, и он должен распоряжаться им.

Есть исключения из этого правила, но до сих пор он переходит в собственность. Иногда вы передаете право собственности на тип другим. Например, при использовании метода factory метод factory передает права собственности на созданный объект на вызывающего. Иногда право собственности передается на созданный объект, например .NET StreamReader. В документации API это ясно, но, поскольку дизайн такой неинтуитивный, разработчики продолжают спотыкаться об этом. Большинство типов в .NET Framework не работают таким образом. Например, класс SqlCommand не удаляет SqlConnection, и было бы очень неприятно, если бы оно установило соединение.

Другой способ взглянуть на эту проблему - с точки зрения принципов SOLID. Путем внедрения IUnitOfWork реализации IDisposable вы нарушаете Принцип инверсии зависимостей, потому что "Абстракции не должны зависеть от деталей, детали должны зависеть от абстракций". Внедряя IDisposable, вы просачиваете детали реализации в интерфейс IUnitOfWork. Реализация IDisposable означает, что у класса есть неуправляемые ресурсы, требующие удаления, такие как дескрипторы файлов и строки подключения. Это детали реализации, потому что вряд ли когда-либо будет так, что каждая реализация такого интерфейса фактически нуждается в утилизации вообще. Вам просто нужно создать одну фальшивую или макетную реализацию для ваших модульных тестов, и у вас есть доказательство реализации, которая не требует удаления.

Поэтому, когда вы исправляете это нарушение DIP, удаляя интерфейс IDisposable из IUnitOfWork и перемещая его в реализацию, для декоратора становится невозможным распоряжаться декоратором, поскольку он не знает, а не декоратор реализует IDisposable. И это хорошо, потому что, согласно DIP, декоратор не должен знать - и мы уже установили, что декоратор не должен распоряжаться декоратором.

Ответ 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();
    }
}

Если вы переместите создание MyContext из UnitOfWork, вставив его в 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. Обычно это Корень композиции.

Ответ 3

Лично я подозреваю, что вам нужно обрабатывать это в каждом конкретном случае. У некоторых декораторов могут быть веские причины понять область видимости; для большинства, это, вероятно, хороший дефолт, чтобы просто передать его. Очень немногие должны явно не использовать цепочку - основные моменты, которые я видел, что это было специально, чтобы противодействовать сценарию, в котором другой декоратор, который должен был рассмотреть область видимости: не был (всегда удален).

В качестве родственного примера - рассмотрите такие вещи, как GZipStream - для большинства людей, они имеют дело только с одним логическим блоком, поэтому по умолчанию "распоряжаться потоком" - это нормально; но это решение доступно с помощью перегрузки конструктора, который позволяет вам сказать, как вести себя. В последних версиях С# с дополнительными параметрами это можно было бы сделать в одном конструкторе.

Вариант 2 является проблематичным, так как он требует от вас (или контейнера) отслеживать все промежуточные объекты; если ваш контейнер делает это удобно, то хорошо, но также обратите внимание, что они должны быть расположены в правильном порядке (внешний для внутреннего). Потому что в цепочке декоратора могут быть ожидающие операции - запланированные для сброса по потоку по запросу или (в крайнем случае) во время удаления.