14 December 2015

Disposable без границ

.NETDesigning and refactoringC#ООP

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

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


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

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


    public interface IDisposable<out T> : IDisposable
    {
        T Value { get; }
    }

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

Реализация проста как мычание:
    public class Disposable<T> : IDisposable<T>
    {
        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<T> ToDisposable<T>(this T value, IDisposable lifetime)
        {
            return new Disposable<T>(value, lifetime);
        }

Для использования достаточно просто написать:
        var disposableResource = resource.ToDisposable(disposable);

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

Если объект уже наследует IDisposable и эта реализация нас устраивает, то можно и без аргументов:
        public static IDisposable<T> ToSelfDisposable<T>(this T value) where T : IDisposable
        {
            return value.ToDisposable(value);
        }

Если ничего удалять не надо, но от нас ждут, что мы умеем (помните про вредный StreamReader?):
        public static IDisposable<T> ToEmptyDisposable<T>(this T value) where T : IDisposable
        {
            return value.ToDisposable(Disposable.Empty);
        }

Если хочется автоматически отписаться от событий объекта при расставании:
        public static IDisposable<T> ToDisposable<T>(this T value, Func<T, IDisposable> lifetimeFactory)
        {
            return value.ToDisposable(lifetimeFactory(value));
        }

… и применять вот так:
        var disposableResource = new Resource().ToDisposable(r => r.Changed.Subscribe(Handler));

Если очистка требует выполнения специального кода, то и здесь на помощь придет однострочник:
        public static IDisposable<T> ToDisposable<T>(this T value, Action<T> dispose)
        {
            return value.ToDisposable(value, Disposable.Create(() => dispose(value)));
        }

И даже если специальный код также нужен для инициализации:
        public static IDisposable<T> ToDisposable<T>(this T value, Func<T, Action> disposeFactory)
        {
            return new Disposable<T>(value, Disposable.Create(disposeFactory(resource)));
        }

Использовать еще проще чем рассказывать:
        var disposableViewModel = new ViewModel().ToDisposable(vm => 
        {
            observableCollection.Add(vm);
            return () => observableCollection.Remove(vm);
        });

А что если у нас уже есть готовая обертка, но надо добавить к ней еще немного ответственности за очистку ресурсов?
Нет проблем:
        public static IDisposable<T> Add<T>(this IDisposable<T> disposable, IDisposable lifetime)
        {
            return disposable.Value.ToDisposable(Disposable.Create(disposable, lifetime));
        }


Итоги


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

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

Надеюсь, статья и применение ее материалов на практике доставит читателям не меньшее удовольствие, чем автору.
Любые дополнения и критика приветствуются.
Tags:disposableвелосипедостроениесинтаксический сахарlinq
Hubs: .NET Designing and refactoring C# ООP
+9
11.4k 73
Comments 110
Popular right now
.NET C#/Blazor Developer
from 3,000 to 4,000 $Hand2NoteRemote job
SharePoint Developer C# .NET
from 80,000 to 100,000 ₽Витро СофтRemote job
Senior Backend Developer (C#, .net)
from 200,000 ₽Wärtsilä Digital TechnologiesСанкт-Петербург
Middle Backend developer (C#)
from 60,000 ₽Email SoldiersRemote job
Разработчик C#
from 100,000 to 160,000 ₽ТакскомМосква
Top of the last 24 hours