Disposable без границ

f457e960271e49f5aea9cc2e19c38f1c.jpg
В своей предыдущей статье я рассказал, как объект может просто и надежно нести ответственность за свои ресурсы.

Но есть множество вариантов владения, которые не являются персональной ответственностью объекта:

  • Ресурсы, которыми владеют зависимости. При использовании Dependency Injection объект класса не только не должен отвечать за жизненный цикл своих зависимостей, он просто физически не может это делать: зависимость может разделяться между несколькими клиентами, зависимость может реализовать IDisposable, а может не реализовать, но при этом у нее могут быть свои зависимости и так далее. Кстати, этот довод сразу ставит крест на любых бизнес-интерфейсах, расширяющих IDisposable: такой интерфейс требует от своих реализаций невозможного — отвечать за себя и за того парня (зависимости)
  • Ресурсы, которые при некоторых условиях не надо очищать. Это, к примеру, дурная привычка StreamReader закрывать нижележащий Stream при вызове Dispose
  • Ресурсы, которые являются внешними по отношению к зависимости, но требуются клиенту в процессе ее использования. Самый простой пример — подписка на события объекта при присвоении его свойству.


Среди стандартных классов и интерфейсов .NET готового решения нет. Но, к счастью, этот велосипед очень просто собрать самому и он сможет дать убедительный ответ на все требования по части освобождения ресурсов.

Новый IDisposable<T>: теперь с обобщением

    public interface IDisposable : IDisposable
    {
        T Value { get; }
    }


Семантика обобщенного IDisposable отличается от обычного примерно так же как «можете быть свободны» от «немедленно освободите помещение». Теперь очистка ресурсов отделена от реализации основной функциональности и может определяться как поставщиком зависимости, так и ее потребителем.

Реализация проста как мычание:

    public class Disposable : IDisposable
    {
        public Disposable(T value, IDisposable lifetime)
        {
            _lifetime = lifetime;
            Value = value;
        }
   
        public void Dispose()
        {
            _lifetime.Dispose();
        }

        public T Value { get; }

        private readonly IDisposable _lifetime;
    }

Используем стероиды


А теперь я покажу, как с помощью нового велосипеда и нескольких однострочных кусочков синтаксического сахара можно просто, чисто и элегантно решить все рассмотренные варианты освобождения ресурсов.

Для начала избавим себя от вызова конструктора с явным указанием типа с помощью метода расширения:

        public static IDisposable ToDisposable(this T value, IDisposable lifetime)
        {
            return new Disposable(value, lifetime);
        }


Для использования достаточно просто написать:

        var disposableResource = resource.ToDisposable(disposable);


Типы компилятор в львиной доле случаев успешно выведет сам.

Если объект уже наследует IDisposable и эта реализация нас устраивает, то можно и без аргументов:

        public static IDisposable ToSelfDisposable(this T value) where T : IDisposable
        {
            return value.ToDisposable(value);
        }


Если ничего удалять не надо, но от нас ждут, что мы умеем (помните про вредный StreamReader?):

        public static IDisposable ToEmptyDisposable(this T value) where T : IDisposable
        {
            return value.ToDisposable(Disposable.Empty);
        }


Если хочется автоматически отписаться от событий объекта при расставании:

        public static IDisposable ToDisposable(this T value, Func lifetimeFactory)
        {
            return value.ToDisposable(lifetimeFactory(value));
        }


… и применять вот так:

        var disposableResource = new Resource().ToDisposable(r => r.Changed.Subscribe(Handler));


Если очистка требует выполнения специального кода, то и здесь на помощь придет однострочник:

        public static IDisposable ToDisposable(this T value, Action dispose)
        {
            return value.ToDisposable(value, Disposable.Create(() => dispose(value)));
        }


И даже если специальный код также нужен для инициализации:

        public static IDisposable ToDisposable(this T value, Func disposeFactory)
        {
            return new Disposable(value, Disposable.Create(disposeFactory(resource)));
        }


Использовать еще проще чем рассказывать:

        var disposableViewModel = new ViewModel().ToDisposable(vm => 
        {
            observableCollection.Add(vm);
            return () => observableCollection.Remove(vm);
        });


А что если у нас уже есть готовая обертка, но надо добавить к ней еще немного ответственности за очистку ресурсов?
Нет проблем:

        public static IDisposable Add(this IDisposable disposable, IDisposable lifetime)
        {
            return disposable.Value.ToDisposable(Disposable.Create(disposable, lifetime));
        }

Итоги


Наткнувшись на эту идею прямо по ходу решения бизнес-задачи, сразу написал и с чувством глубокого удовлетворения применил все рассмотренные однострочники.

Что удивительно, несмотря на наличие как минимум одного полного аналога IDisposable<T> в лице Owned<T> из Autofac, беглое гугление не выявило похожих методов расширения.

Надеюсь, статья и применение ее материалов на практике доставит читателям не меньшее удовольствие, чем автору.
Любые дополнения и критика приветствуются.

© Habrahabr.ru