Disposable без границ
В своей предыдущей статье я рассказал, как объект может просто и надежно нести ответственность за свои ресурсы.
Но есть множество вариантов владения, которые не являются персональной ответственностью объекта:
- Ресурсы, которыми владеют зависимости. При использовании 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, беглое гугление не выявило похожих методов расширения.
Надеюсь, статья и применение ее материалов на практике доставит читателям не меньшее удовольствие, чем автору.
Любые дополнения и критика приветствуются.