В своей предыдущей статье я рассказал, как объект может просто и надежно нести ответственность за свои ресурсы.
Но есть множество вариантов владения, которые не являются персональной ответственностью объекта:
- Ресурсы, которыми владеют зависимости. При использовании Dependency Injection объект класса не только не должен отвечать за жизненный цикл своих зависимостей, он просто физически не может это делать: зависимость может разделяться между несколькими клиентами, зависимость может реализовать IDisposable, а может не реализовать, но при этом у нее могут быть свои зависимости и так далее. Кстати, этот довод сразу ставит крест на любых бизнес-интерфейсах, расширяющих IDisposable: такой интерфейс требует от своих реализаций невозможного — отвечать за себя и за того парня (зависимости)
- Ресурсы, которые при некоторых условиях не надо очищать. Это, к примеру, дурная привычка StreamReader закрывать нижележащий Stream при вызове Dispose
- Ресурсы, которые являются внешними по отношению к зависимости, но требуются клиенту в процессе ее использования. Самый простой пример — подписка на события объекта при присвоении его свойству.
Среди стандартных классов и интерфейсов .NET готового решения нет. Но, к счастью, этот велосипед очень просто собрать самому и он сможет дать убедительный ответ на все требования по части освобождения ресурсов.
Новый IDisposable<T>: теперь с обобщением
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<T> в лице Owned<T> из Autofac, беглое гугление не выявило похожих методов расширения.
Надеюсь, статья и применение ее материалов на практике доставит читателям не меньшее удовольствие, чем автору.
Любые дополнения и критика приветствуются.
Комментарии (110)
FiresShadow
14.12.2015 08:35
Тут можно было просто добавить событие\делегат в ViewModel, тогда cohesion было бы выше при том же coupling.var disposableViewModel = new ViewModel().ToDisposable(vm => { observableCollection.Add(vm); return () => observableCollection.Remove(vm); });
Bonart
14.12.2015 09:34Куда именно добавить? Из конструктора события вызывать бесполезно, IDisposable viewModel реализовывать не обязана, как и знать о своем нахождении в ObservableCollection.
FiresShadow
14.12.2015 09:52Не вижу никаких препятствий, мешающих ViewModel реализовать IDisposable
class ViewModel : IDisposable { public Action<ViewModel> DisposeStrategy; public void Dispose() { /*тут освобождение своих ресурсов*/ try { if (DisposeStrategy != null) DisposeStrategy(this); } finally { DisposeStrategy = null; } } }
var viewModel = new ViewModel() {DisposeStrategy = vm => observableCollection.Remove(vm)}; observableCollection.Add(viewModel);
Bonart
14.12.2015 10:09- Вы добавили внутрь ViewModel ответственность, которая ей самой не нужна
- Вы исключили сценарий дальнейшего переиспользования конкретного экземпляра ViewModel
- Вам потребуется более трудоемкий рефакторинг при замене типа ViewModel на другой класс
- Ваш код еще не делает того, что делает мой, но уже больше и сложнее
FiresShadow
14.12.2015 11:22+1Допустим, у вас в ViewModel «на одну ответственность меньше», а сколько тогда ответственностей у Disposable[ViewModel]? А если понадобится добавить функцию печати и импорта в Excel, вы напишите Printable<Excelable<Disposable[ViewModel]>>?
lair
14.12.2015 11:00+3зависимость может реализовать IDisposable, а может не реализовать
Эээ, если зависимость не реализуетIDisposable
, то в чем проблема-то? Не реализует — не диспозь.Bonart
14.12.2015 11:48-4Так у нее свои зависимости могут быть, вполне себе диспозабельные. И если исходная зависимость с какого-то времени нам не нужна, то надо об этом как-то сообщить, чтобы CompositionRoot смог очистить все, что уже не требуется никому.
Owned в Autofac именно для этого.lair
14.12.2015 13:38+2Кому сообщить? DI-контейнеру? Так давайте для этого использовать ISignalToContainer (он же Owned в автофаке), а не IDisposable, семантика-то разная совершенно.
Bonart
14.12.2015 14:21семантика-то разная совершенно.
Чем она разная-то? И там, и там «это мне больше не нужно».
Так давайте для этого использовать ISignalToContainer (он же Owned в автофаке)
… и пронесем зависимость от контейнера в обычный класс?lair
14.12.2015 14:32Чем она разная-то? И там, и там «это мне больше не нужно».
Да нет же. IDisposable означает «отпусти ресурсы». Owned означает «закрой lifetime scope». Упомянутый выше псевдоинтерфейс означает «передай контейнеру, что я все, пусть делает, что хочет».
… и пронесем зависимость от контейнера в обычный класс?
А так пронесем зависимость от вашегоIDisposable<T>
— оно чем-то лучше?
Ну и да, тема абстракции от DI-контейнера, конечно, интересная и плодотворная, но она совершенно не связана с тем, что вы в посте пишете.Bonart
14.12.2015 16:42А так пронесем зависимость от вашего IDisposable — оно чем-то лучше?
Тем, что нет никакой привязки к контейнеру и никаких лишних обязательств для реализации.
Ну и да, тема абстракции от DI-контейнера, конечно, интересная и плодотворная, но она совершенно не связана с тем, что вы в посте пишете.
Вы можете так считать. Я полагаю, что поддержка абстракции классов-клиентов от контейнеров — один из типовых вариантов использования для IDisposable<T> Просто в Autofac эта тема на мой взгляд раскрыта настолько полно, что я предпочел ограничиться ссылкой.lair
14.12.2015 16:49Тем, что нет никакой привязки к контейнеру и никаких лишних обязательств для реализации.
Вот именно, что нет обязательств — что означает, что мне никто не обещает, что поведет себя каким-то конкретным образом. И именно поэтому оно мне низачем не надо.Bonart
14.12.2015 17:06Вот именно, что нет обязательств — что означает, что мне никто не обещает, что поведет себя каким-то конкретным образом.
Не понял — CompositionRoot реализуете не вы, а кто-то другой? Если вы, то нет никакой проблемы реализовать любое нужное вам поведение.
Кроме того, а какие обещания вам нужны на стороне класса-клиента? Вам дают ресурс и просят сообщить, когда он он перестанет быть вам нужен. Чего не хватает?lair
14.12.2015 17:10CompositionRoot реализуете не вы, а кто-то другой?
Не я.
Вам дают ресурс и просят сообщить, когда он он перестанет быть вам нужен. Чего не хватает?
Э не, вы не поняли. Нет никакого «вам дают и просят», есть только «я запрашиваю». Так вот, я запрашиваю зависимость, и я хочу быть уверен, что (а) эта зависимость (вместе с деревом зависимостей) будет порезолвлена именно тогда, когда я попрошу, и (б) эта зависимость (вместе с деревом зависимостей) будет отпущена именно тогда, когда я попрошу.
А сообщать о том, что ресурс мне больше не нужен, мне, как разработчику класса-клиента, низачем не сдалось.Bonart
14.12.2015 18:30Так вот, я запрашиваю зависимость
Это у вас уже не Dependency Injection, а Service Locator какой-то
Так вот, я запрашиваю зависимость, и я хочу быть уверен, что (а) эта зависимость (вместе с деревом зависимостей) будет порезолвлена именно тогда, когда я попрошу, и (б) эта зависимость (вместе с деревом зависимостей) будет отпущена именно тогда, когда я попрошу.
Понятно, в рамках Dependency Injection это в терминах Марка Симана есть чистейший Control Freak. В таком варианте IDisposable<T> вам и в самом деле ни к чемуlair
14.12.2015 18:39+1Это у вас уже не Dependency Injection, а Service Locator какой-то
Нет. Параметр в конструкторе (в случае использования Dependency Injection) имеет семантику «для моей работы нужна вот такая зависимость — дай мне ее».
Понятно, в рамках Dependency Injection это в терминах Марка Симана есть чистейший Control Freak.
Не больше, чем вашIDisposable<T>
. Впрочем нет, в терминах Симана это не Control Freak, потому что «The CONTROL FREAK anti-pattern occurs every time we get an instance of a DEPENDENCY by directly or indirectly using the new keyword in any place other than a COMPOSITION ROOT». К явному управлению жизненным циклом это отношения не имеет.Bonart
15.12.2015 13:26К явному управлению жизненным циклом это отношения не имеет
В таком случае IDisposable<T> полностью соответствует заявленным вами требованиям.
Вызов Func<Disposable<T>> именно что создает зависимость для вашего объекта, а вызов IDisposable<T>.Dispose() ее отпускает. При этом создание и отпускание зависимости вовсе не означает обязательное конструирование или очистку объектов-реализаций.lair
15.12.2015 14:23Не соответствует. Я хочу управление lifetime scope. А поведение
IDisposable<T>.Dispose()
, как уже обсуждалось, не определено.Bonart
15.12.2015 15:22Поведение, что того, что другого определяется Compositon Root.
С вашей стороны есть только уведомление Composition Root о разрыве зависимости и не более того.
Реальное поведение зависит от реализации Composition Root (конфигурации контейнера) и никакая lifetimeScope определенности по факту не добавляет. Я могу сконфигурировать контейнер так, что при очистке lifetimeScope будет выполнен произвольный код.
Хотите узнать, что точно будет — смотрите код Composition Root и никак иначе.lair
15.12.2015 16:03Поведение, что того, что другого определяется Compositon Root.
Поведение Owned описано в документации на Autofac. Composition Root, который его переопределяет (я такого не знаю ни одного) — радикально не прав.
Bonart
15.12.2015 13:46>CompositionRoot реализуете не вы, а кто-то другой?
Не я.
Тогда у вас никаких гарантий по определению. Кстати, очистка lifetimeScope внутри Owned на самом деле ничего конкретного вам не гарантирует. В зависимости от настроек контейнера и конкретного набора объектов на данный момент что-то может быть освобождено, а может и ничего не освободиться.
эта зависимость (вместе с деревом зависимостей) будет отпущена именно тогда, когда я попрошу.
А сообщать о том, что ресурс мне больше не нужен
«Зависимость отпущена» — это и есть всего лишь информация для Composition Root, что вы от ресурса больше не зависите, т.е. он вам не нужен.
Будут ли при разрешении зависимости создаваться какие-то объекты, а при отпускании — очищаться, вы на уровне клиента не можете гарантировать никак. Это ответственность Composition Root, а клиент может только сообщить «Мне нужно X и получить желаемое» и «мне больше не нужно X».lair
15.12.2015 14:28Тогда у вас никаких гарантий по определению
… кроме тех, которые предоставляет контейнер.
Кстати, очистка lifetimeScope внутри Owned на самом деле ничего конкретного вам не гарантирует.
Почему же. Она мне гарантирует то, что я прошу: открытие/закрытие скоупа по открытию/закрытию owned.Bonart
15.12.2015 15:26… кроме тех, которые предоставляет контейнер.
А в контейнере все определяется его конфигурацией. Тут хоть совой об пень, хоть пнем об сову.
Почему же. Она мне гарантирует то, что я прошу: открытие/закрытие скоупа по открытию/закрытию owned.
Во-первых даже это не гарантировано — поведение по умолчанию вполне возможно переопределить, причем не хаками, а штатными средствами. Во-вторых результат создания-очистки скоупа также определяется конфигурацией контейнера. Контейнер — это всего лишь инструмент для реализации Composition Root он специально сделан максимально гибким.
lair
14.12.2015 11:08+4Это, к примеру, дурная привычка StreamReader закрывать нижележащий Stream при вызове Dispose
Вообще-то у StreamReader есть параметр, который это отключает.Bonart
14.12.2015 11:59Вы правы, вот только и то и другое поведение может быть недостаточно гибко.
С одной стороны вариант по умолчанию закрывает то, что можно переиспользовать.
С другой — вариант без автозакрытия ничего не сообщает о том, что Stream больше не используется и им можно свободно распоряжаться.
IDisposable<T> позволяет гибко настроить оба варианта, причем на уровне CompositionRoot, не трогая код, использующий StreamReader.creker
14.12.2015 12:17+5Такое ощущение, что вы боретесь не с причиной, а со следствием. А причина — изначально плохая архитектура. Один bool вариант в StreamReader/StreamWriter покрывает все, что необходимо от этого класса — закрыть или не закрыть стрим, который ему дается. Больше он ни о чем думать не должен. Код снаружи должен сообщать, может ли стрим использоваться где-то в другом месте или он еще занят. Уже изначально такая проблема намекает, что где-то что-то не так и IDisposable тут совсем не при чем.
Bonart
14.12.2015 12:33Один bool вариант в StreamReader/StreamWriter покрывает все, что необходимо от этого класса — закрыть или не закрыть стрим, который ему дается.
Вообще-то не покрывает. Закрыть или не закрыть Stream означает «переиспользовать или не переиспользовать» уровнем выше.
И если все-таки переиспользовать, то ответ на вопрос «с какого момента?» может оказаться очень важным.
Код снаружи тут не при делах: только тот, кто использует Reader может сообщить о том, что зависимости Reader'а больше не нужны и могут быть переиспользованы где-то еще.
Аналог — переиспользование соединений посредством пула: чтобы вернуть соединение в пул и сделать его доступным для повторного использования, необходимо знать, когда оно больше не нужно текущему клиенту.FiresShadow
14.12.2015 13:26Имхо, автор открыл (ну, или поспособствовал открытию) замечательный способ множественного наследования из мира ненормального программирования. Вместо того, чтобы класс реализовывал IPrintable, IExcelable, IDisposable можно написать три универсальных класса Printable, Excelable, Disposable, способных печатать и диспозить что угодно. Всю логику передаём в универсальные классы через делегаты. Поскольку в C#/Java нет множественного наследования, то делаем Printable[Excelable[Disposable[ViewModel]]].
Можно вообще сделать просто один класс Doer[T], делающий вообще всё что угодно. Пусть у класса 3 метода, тогда переменная будет иметь тип Doer[Doer[Doer[StreamReader]]](). Код вызова 1го метода: doer.Do(). Второго: doer.Value.Do(). Третьего: doer.Value.Value.Do(). Логику методов можно передавать через делегаты: new StreamReader.ToDoer(x => x.Dispose()).ToDoer(x => x.Value.ToString()).ToDoer(x => x.Value.Value.GetHashCode()). Чудесненько.
Имхо, в данном случае автор предлагает своеобразный способ, как «переопределить» Dispose в StreamReader, не реализуя наследника от StreamReader. Это незначительно экономит время: вместо объявления класса и метода нужно просто передать тело метода через делегат в ToDisposable(Action[T] delegate). Цена: использование нестандартного подхода, замена переменных типа StreamReader на Disposable[StreamReader], возможность переопределить только один метод (иначе приходим к Printable[Excelable[Disposable[StreamReader]]], а это уже явный перебор). Этот способ имеет смысл, когда нужно переопределить метод в запечатанном классе без публичных конструкторов. Однако автор предлагает использовать его повсеместно при любом освобождении ресурсов. Непонятно, почему бы просто не реализовать наследника StreamReader, раз уж так сильно хочется переопределить Dispose. Хотя не до конца понятно, а зачем его переопределять. Взяли из пула соединение, передали в функцию, функция отработала, вернули соединение в пул.
lair
14.12.2015 13:42IDisposable<T>
позволяет гибко настроить оба варианта
Не в данном случае.StreamReader
принимает на входStream
, а неIDisposable
.
Ну и да, отдельно хочу заметить, что — учитывая стандартные сценарии использованияStreamReader/Writer
, — я совершенно не понимаю, зачем куда-то о чем-то сигнализировать. Этот флаг покрывал все случавшиеся в моей практике варианты работы.
mird
14.12.2015 13:08Скажем честно, этот параметр — плохая архитектура.
lair
14.12.2015 13:43+3Я вот в этом совершенно не уверен. Ничего особо плохого в нем нет (ну кроме общей болезни флаговых параметров), а при этом типовые сценарии использования покрыты.
mird
14.12.2015 13:53Я бы сделал так, что если мы стрим передали снаружи — его не закрывать. Если создали внутри то закрывать. А иначе, поведение не очевидное.
lair
14.12.2015 14:12+2Овер 90% использования ридера — с переданным наружи стримом. И в ощутимой части из них его закрывать совершенно не надо.
Nanako
14.12.2015 15:00+1Внесу свои 5 копеек. У меня один процесс от другого получает данные в таких объемах, что приходится переводить GC в режим Sustained Low Latency. Внутри цикла, который иногда вызывается аж раз в 60 микросекунд, создаются и уничтожаются достаточно большие managed и unmanaged массивы, т.к. новые данные приходят через MemoryMap (ага, и StreamReader есть, точнее 8). Чтобы это не отъедало оперативную память со скоростью в 500мб в минуту у всего что там временно используется вызывается Dispose, чтобы наверняка. А еще чтобы не тормозило там куча unsafe и unchecked кода, доступ к коллекциям из локальных переменных, сознательное использование структур и их boxing, ни одной лямбды или замыкания, вобщем все грабли C# вплоть до ручного контроля Capacity коллекций. Короче, код где реально без нормального Dispose вообще никак, еще и с кучей unmanaged ресурсов. И ни разу мне не понадобилось ничего, кроме канонической реализации IDisposable и пары классов оберток. А в вашем варианте и множественное наследование не реализовать толком, и вызовы виртуальных функций и не sealed классы до сих пор такой пенальти по производительности дают, хоть стой хоть падай. В реальном коде который доводит систему до under memory pressure ваша реализация ИМХО гарантированно даст отрицательную эффективность. Я не говорю что ваниль спасет человечество, но такие велосипеды говорят о кривой архитектуре. Да, сознаюсь, я наверное в 100500 классов уже скопировал реализацию IDisposable, IComparable и т.д., но это особенность C#, и с ней не надо воевать, тем более есть тот же Решарпер. А все попытки добавить свой сахар и сделать «поудобнее» очень сильно пинают по производительности.
Bonart
14.12.2015 16:34Да, сознаюсь, я наверное в 100500 классов уже скопировал реализацию IDisposable, IComparable и т.д., но это особенность C#, и с ней не надо воевать, тем более есть тот же Решарпер. А все попытки добавить свой сахар и сделать «поудобнее» очень сильно пинают по производительности.
Полагаю, что ваши требования к производительности актуальны далеко не для всех.
Также полагаю, что даже там, где они актуальны, проще оптимизировать корректный код, чем корректировать оптимизированный.
А в вашем варианте и множественное наследование не реализовать толком, и вызовы виртуальных функций и не sealed классы до сих пор такой пенальти по производительности дают, хоть стой хоть падай.
Не могу понять где вы у меня нашли наследование классов, вызовы виртуальный функций и т.п.Nanako
14.12.2015 16:55+1Ну у вас как бы два варианта развития событий, код используете только вы, и там где код вызывается редко, можно все залить сахаром в виде LINQ, Rx и все такое. А второй вариант это если код использую другие люди, или он является ядром системы. И в итоге есть когда пара солюшенов, каждый на 500 файлов не считая тестов, и в одном что-то тормозит из-за косяков в другом начинаются проблемы, из серии «билд не может закончится т.к. таргет dll занята в мокапе для интегрейшн теста другого солюшена, а перезапускать сервер на каждый билд мы задолбались уже». И очень быстро приучаешься писать с минимумом наследования, абстракт классами и seal'ом всего и вся. И инициализация ресурсов, и финализация, и Dispose начинают прописываться вообще до бизнес логики. А после продакшена вообще оказывается что кастом методы Dispose прописанные для каждого класса это мана небесная, вроде «Так, а здесь для экономии ресурсов и увеличения скорости мы вообще откажемся от GC и лог замаршаллим прямо строками в heap», когда внезапно боттлнеком оказывается синхронное логгирование в одном из тредлупов из-за загруженности тома другим процессом на сервере.
И как раз «наследование классов, вызовы виртуальный функций»: у вас есть класс с достаточно базовой функциональностью, и нам нужно запилить наследника с еще какой-то базовой функциональностью и начинается пляска с кучей интерфейсов на разных этажах. А по изначальной логике C# есть набор базовых функций например IDisposable, IComparable и все такое, далее делается несколько абстракт классов с нужными наборами функциональности, и потом в идеальном случае один финальный класс где уже все функции определены и по максимуму финализированы. И этажерка virtual calls минимальная, и дженерики генерируются очень эффективно. А вы пытаетесь какой-то свой стиль привить, и в итоге что там в стеке MSIL получится мне страшно представить.Bonart
14.12.2015 17:11И как раз «наследование классов, вызовы виртуальный функций»: у вас есть класс с достаточно базовой функциональностью, и нам нужно запилить наследника с еще какой-то базовой функциональностью и начинается пляска с кучей интерфейсов на разных этажах.
Дело в том, что я не использую наследование классов и, соответственно, не использую ни виртуальные методы, ни абстрактные классы.Nanako
14.12.2015 17:20+1Тут дело в том что вы на функциональность которая идет «снизу» хотя бы в рамках GC и всего такого пытаетесь навесить инвершен и зайти «сверху». И архитектура трещит, и абсолютно реально появление конструкций «Printable<Excelable<Disposable[ViewModel]>>» если лепить концепт IoC на все подряд. Хотя тут старый добрый ООП отлично подошел бы, а чтобы не поощрять программиста устраивать вложенные virtual calls C# еще и по рукам бьет.
Bonart
15.12.2015 13:52Видите ли, то, что я делаю, это и есть старый добрый ООП.
Что значит «архитектура трещит»?
Те оптимизации, про которые вы говорите — всего лишь издержки конкретной реализации платформы и не надо выдавать нужду за добродетель.
Viacheslav01
14.12.2015 16:04+1Зачем, зачем все это? Если ты сам не знаешь, о используемых объектах, что, как, где, зачем и как долго, то никакие велосипеды тебя не спасут, только добавят сложности в твой код, больше, больше и еще больше!
IL_Agent
15.12.2015 13:10При использовании Dependency Injection объект класса не только не должен отвечать за жизненный цикл своих зависимостей, он просто физически не может это делать: зависимость может разделяться между несколькими клиентами
Несколько владельцев у одного disposable объекта? Просто не надо так делать.Bonart
15.12.2015 13:48-1Вы в курсе про паттерн Dependency Injection? Объект НЕ владеет своими зависимостями, владелец у них один — Composition Root.
IL_Agent
15.12.2015 13:56Несколько классов зависят от одного disposable класса? Просто не надо так делать.
Лучше? )Bonart
15.12.2015 14:09Не лучше.
Допустим, у вас есть есть очень дорогой объект. Он долго создается и жрет четверть всей доступной вам памяти за счет требующих очистки ресурсов. Реализуемый им интерфейс допускает параллельную работу без ограничений. Также у вас есть 10 дешевых объектов, которые зависят от интерфейса, реализованного дорогим.
Теперь попробуйте последовать собственному совету, подумайте, что будет и как исправить ситуацию.IL_Agent
15.12.2015 20:30Когда эти ресурсы требуют освобождения? Обычно такие объекты регистрируются как синглтон и умирают вместе с процессом. Соответственно, такой объект не должен быть idisposable.
Bonart
15.12.2015 20:38В подходящий момент. Вместе с процессом создавать и уничтожать нельзя.
Не стоит пытаться подогнать условия под ответ.
Кстати, даже если регистрировать объект как синглтон — он обязан быть Disposable, если имеет ресурсы, не очищаемые сборщиком мусора.lair
15.12.2015 20:55Кстати, даже если регистрировать объект как синглтон — он обязан быть Disposable, если имеет ресурсы, не очищаемые сборщиком мусора.
Это почему?Bonart
17.12.2015 11:10> Это почему?
Потому что иначе класс будет содержать утечки ресурсов by designlair
17.12.2015 11:45Потому что иначе класс будет содержать утечки ресурсов by design
И в какой момент будут утекать ресурсы, учитывая, что экземпляр класса ровно один на процесс?Bonart
17.12.2015 11:49А вот это на уровне класса неизвестно. И не должно быть известно.
lair
17.12.2015 11:51Вот еще. Если я проектирую и класс с ресурсами, и приложение, его использующее, зачем мне добавлять избыточный код?
Bonart
17.12.2015 12:41Вам, возможно, и незачем.
Я же предпочитаю не иметь такой сильной и неявной связности нигде и никогда.
Это сильно помогает при проектировании, изменениях, повторном использовании и рефакторинге. Экономия на спичках на мой взгляд здесь совершенно не нужна.lair
17.12.2015 12:47Понимаете ли, избыточный код — это тоже оверхед. Его надо осознавать, его надо поддерживать.
Ну и да, получается, что нет никакого «объект обязан быть Disposable, если он имеет ресурсы, не очищаемые сборщиком мусора», есть «я предпочитаю делать такие объекты Disposable, потому что я никогда не знаю, как они будут использованы».Bonart
20.12.2015 01:12Понимаете ли, избыточный код — это тоже оверхед. Его надо осознавать, его надо поддерживать.
Недостающий код — оверхед много больший. Класс, который сам по себе течет как слониха в красный день календаря, требует дополнительных усилий для поддержки и сопровождения, ибо надо знать, почему это так реализовано и каждый раз заново убеждаться, что причина выбора столь своеобразного метода очистки ресурсов до сих пор актуальна.
Ну и да, получается, что нет никакого «объект обязан быть Disposable, если он имеет ресурсы, не очищаемые сборщиком мусора»
Не получается. Какой-нибудь очередной ad hoc означает пренебрежение обязанностью, а не ее отсутствие.
«я предпочитаю делать такие объекты Disposable, потому что я никогда не знаю, как они будут использованы»
Как правило знаю. Но на уровне реализации знать не хочу, так это будет неявной зависимостью.lair
20.12.2015 18:34+1Недостающий код — оверхед много больший.
Как вы определяете, что его не достает?
Класс, который сам по себе течет как слониха в красный день календаря… причина выбора столь своеобразного метода очистки ресурсов до сих пор актуальна.
O, srsly? Давайте на примерах. Вот, значит, класс «с ресурсами»:
public class DisposableResourceHolder : IDisposable { private SafeHandle resource; // handle to a resource public DisposableResourceHolder(){ this.resource = ... // allocates the resource } public void Dispose(){ Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing){ if (disposing){ if (resource!= null) resource.Dispose(); } } }
А вот регистрация:
container.Register<DisposableResourceHolder>().AsSingleInstance();
Два вопроса:
— «течет» ли этот класс?
— в какой момент будет гарантированно вызванresource.Dispose
?
А теперь давайте сделаем вот так:
public class DisposableResourceHolder { private SafeHandle resource; // handle to a resource public DisposableResourceHolder(){ this.resource = ... // allocates the resource } } container.Register<DisposableResourceHolder>().AsSingleInstance();
Что изменилось в ответах на поставленные выше вопросы?
Не получается. Какой-нибудь очередной ad hoc означает пренебрежение обязанностью, а не ее отсутствие.
Так почему же объект, имеющий не-GC-ресурсы, обязан иметьIDisposable
? Что изменится, если он не будет его иметь?
Но на уровне реализации знать не хочу, так это будет неявной зависимостью.
А зря. Короткоживущие и долгоживущие объекты могут иметь сильно разные внутреннюю реализацию.
IL_Agent
15.12.2015 23:54+2Не стоит пытаться подогнать условия под ответ.
«Дорогой объект, который жрет четверть всей доступной вам памяти за счет требующих очистки ( в подходящий момент !) ресурсов». Это условие? )) В таком случае реализация будет зависеть от того, что является «подходящим моментом» или кто его определяет.
Кстати, даже если регистрировать объект как синглтон — он обязан быть Disposable, если имеет ресурсы, не очищаемые сборщиком мусора.
На самом деле IDisposable — это аналог RAII. Отличие в том, что там деструктор вызывается гарантировано и детерминировано, а в c# для этого используется связка using/dispose. Т.е. когда класс проектируется как IDisposable, то предполагается, что его инстанцирование будет производиться в using. Если он является частью агрегата, то корень этого агрегат должен вызывать его Dispose в своём Dispose (в случае RAII деструкторы частей агрегата вызываются при разрушении корня агрегата). В DI-контейнерах такие объекты регистрируются как transient и освобождаются явно, как конкретно — зависит от контейнера.Bonart
17.12.2015 11:08В таком случае реализация будет зависеть от того, что является «подходящим моментом» или кто его определяет.
Не будет. Класс, владеющий ресурсами, которые нельзя освободить автоматически, обязан дать возможность сделать это вручную. В какой именно момент освобождать — ответственность не объекта, а его владельца.
Т.е. когда класс проектируется как IDisposable, то предполагается, что его инстанцирование будет производиться в using.
Нет. using всего лишь удобный синтаксический сахар для использование IDisposable в пределах одного метода.
Если он является частью агрегата, то корень этого агрегат должен вызывать его Dispose в своём Dispose (в случае RAII деструкторы частей агрегата вызываются при разрушении корня агрегата). В DI-контейнерах такие объекты регистрируются как transient и освобождаются явно, как конкретно — зависит от контейнера.
Autofac позволяет не заморачиваться с явным освобождением. Вызов Dispose у LifetimeScope автоматически вызовет Dispose у всех созданных в ней объектов.
Так что вы будете делать в условиях задачи?IL_Agent
17.12.2015 16:50Не будет. Класс, владеющий ресурсами, которые нельзя освободить автоматически, обязан дать возможность сделать это вручную. В какой именно момент освобождать — ответственность не объекта, а его владельца.
То, как класс следует использовать, определяется при проектировании класса. И если класс реализует IDisposable, то это означает, что для его объектов должен быть обязательно вызван Dispose(). Через using, finally или ещё как-нибудь, но вызван. Это не значит, что Dispose нам дали, а мы решаем, дёргать его или нет.
Например, если я реализую класс SingleInstance, задача которого — держать открытым некий файл на протяжении работы процесса для контроля единственности запущенного экземпляра, я вызову Dispose() у файлового потока (он обязан быть вызван по задумке его разработчиков) в ~SingleInstance. А SingleInstance.IDisposable реализовывать, на всякий случай, я не буду. Потому что так класс задуман, такая у него задача.
Так что вы будете делать в условиях задачи?
Приведите условие конкретной задачи, как я привёл выше. «Сделайте класс, который может освобождать ресурсы в произвольный момент» = «Реализуйте IDisposable», да. Но необходимость освобождать ресурсы в произвольный момент должна быть чем-то вызвана. Она не безусловна, как вы утверждаете. И уж тем более такая необходимость крайне сомнительна, когда речь идёт об очень большом ресурсе, разделяемом между большим количеством потребителей.
Вызов Dispose у LifetimeScope автоматически вызовет Dispose у всех созданных в ней объектов.
Кто вызывает Dispose у LifetimeScope?
Bonart
17.12.2015 19:01То, как класс следует использовать, определяется при проектировании класса.
И является его контрактом.
Включать в контракт класса, владеющего ресурсами, реализацию IDisposable — это паттерн.
А вот включать в контракт класса контроль его единственности на процесс — это антипаттерн, нарушающий, для начала, принцип единственной ответственности.
Такая практика чревата размножением копипасты, непригодной для автономного тестирования и повторного использования.
Например, если я реализую класс SingleInstance, задача которого — держать открытым некий файл на протяжении работы процесса для контроля единственности запущенного экземпляра, я вызову Dispose() у файлового потока (он обязан быть вызван по задумке его разработчиков) в ~SingleInstance. А SingleInstance.IDisposable реализовывать, на всякий случай, я не буду. Потому что так класс задуман, такая у него задача.
Класс не должен контролировать количество своих экземпляров. Для этого есть коллекции, пулы, контейнеры и т.п.
Если мне надо держать файл открытым — это обеспечит один класс. А единственность его экземпляра в рамках процесса — другой класс.
Приведите условие конкретной задачи
В задаче достаточно информации для решения.
Но необходимость освобождать ресурсы в произвольный момент должна быть чем-то вызвана. Она не безусловна, как вы утверждаете. И уж тем более такая необходимость крайне сомнительна, когда речь идёт об очень большом ресурсе, разделяемом между большим количеством потребителей.
В рамках поставленной задачи эта необходимость безусловна. Ваш код должен работать без изменений вне зависимости от того, когда владелец захочет освободить ресурсы. Этот момент вы не контролируете.
Вы можете с этим справиться?
Кто вызывает Dispose у LifetimeScope?
Composition Root.IL_Agent
17.12.2015 23:58Включать в контракт класса, владеющего ресурсами, реализацию IDisposable — это паттерн.
Пруф?
А вот включать в контракт класса контроль его единственности на процесс — это антипаттерн
А где вы у меня это увидели? Впрочем, споры о том, чем является синглтон, паттерном ли, антипаттерном, не утихают по сей день :)
В задаче достаточно информации для решения.
В задаче недостаточно задачи :) Вы мне диаграмму классов словами описали, которая сама уже является решением некой абстрактной задачи. Причём сомнительным решением.
Но раз уж на то пошло, что предлагаете вы?
FiresShadow
Не совсем понял мотивацию для использования.
У класса есть ссылка на зависимость, по этой ссылке можно вызвать Dispose.Зависимостями зависимости может управлять сама зависимость. Логику освобождения ресурсов можно поместить в Dispose конкретного класса. Если логика освобождения ресурсов может различаться в разных ситуациях, можно использовать стратегию + фабрику.
В вашем примере логика освобождения ресурсов размазывается по нескольким классам. Часть логики в методе ToDisposable() одного класса (
return value.ToDisposable(Disposable.Empty);return value.ToDisposable(value);), часть логики — непосредственно в Dispose конкретного класса (к которому принадлежит value). Для чего это и в чём выигрыш?
mird
Не стоит делать как вы предлагаете. В этом случае следующий потребитель вашей зависимости может получить disposed зависимость.
Если вы хотите сами контролировать время жизни, лучше вбрасывать фабрику и использовать ее внутри конструкции using
FiresShadow
В случае, когда логика освобождения ресурсов может различаться в разных ситуациях, я предложил использовать стратегию. Нужно освободить разделяемый ресурс — освобождаем, не нужно — не освобождаем. Лучше увидеть внутри Dispose применение стратегии, чем увидеть внутри Dispose часть логики освобождения ресурсов (как предлагает автор статьи), а потом разбираться, почему освобождение ресурсов работает совсем не так, как описано в Dispose, и как оно работает на самом деле.
Не совсем понял, что вы предлагаете. Какую фабрику? Что она создаёт? Как инициализирует то, что создаёт?mird
Автор вообще пишет пургу на тему диспозабл. Предлогаемые им обертки ничего не упрощают, при этом усложняя код или компиляцию.
Наиболее правильно в случае использования DI контейнера переложить управление временем жизни зависимости на него (и задать явно при инициализации как этим временем жизни надо управлять). Если же вы хотите больше контроля за освбождением диспозабл зависимости, то имеет смысл вбрасывать не зависимость а фабрику от нее и использовать конструкцию using.
Если вы работаете с DI контейнером, можно сделать так:
Bonart
Вам же не составит труда привести примеры усложнения в сравнении с более простыми аналогами?
Нельзя сделать так.
Так что корректно определить сигнатуру фабрики можно вот так (Autofac)
… или вот так:
lair
Я, на всякий случай, замечу, что есть разница между
Owned<T>
и любым другим классом (если только вы не написали свое расширение к Autofac): когда вы сделаетеDispose
наOwned<T>
, Autofac закроет соответствующий LifetimeScope, и все зависимости, которые он создавал подT
, будут явно отпущены.Bonart
С точки зрения классов, реализующих функциональность, разница между Owned и IDisposable<T> только в том, что первый требует ссылку на сборку с Autofac. Семантика абсолютно одинаковая.
А с точки зрения реализации Composition Root вы правы: Autofac реализует именно такое поведение для Owned по умолчанию. Впрочем, его легко переопределить и чуть сложнее реализовать аналогичное для IDisposable<T>
lair
Да разве? А мне казалось, что семантика
IDisposable<T>
полностью определяется тем, кто его создает, и может не делать вообще ничего.Bonart
Для того, кто ресурсами пользуется, семантика одинаковая: «Мне этот ресурс больше не нужен, можете делать с ним все, что считаете нужным».
mird
Э нет. Семантика IDisposable — я с ресурсом закончил, освободи его немедленно.
Bonart
Вы правда прочитали статью?
Там прямым текстом указаны различия в семантике IDisposable и IDisposable<T>
mird
Так вот вопрос, зачем вы называете свой интерфейс так же как уже существующий, с устоявшейся семантикой? Чтобы было проще?
Bonart
Совершенно верно. Оба интерфейса предназначены для очистки ресурсов и им логично иметь схожие имена.
У вас есть лучший вариант для имени нового интерфейса их статьи?
Serg046
Ну ведь правда же легко спутать. Я от того даже плюсанул.
Ну, в этом случае release звучит более корректно, чем dispose.
lair
Вот только у
Owned
семантика другая, и она означает: закройте lifetime scope, который был открыт при созданииOwned
. Это, только это, и ничего, кроме этого.Bonart
А вот здесь вы неправы.
Это всего лишь сценарий по умолчанию для автоматического разрешения зависимостей с помощью Autofac.
Данное поведение легко переопределить средствами самого Autofac как для конкретного типа, так и в общем случае.
Ваша трактовка на уровне класса-клиента совершенно избыточна и прямо противоречит паттерну Dependency Injection.
lair
Данное поведение, наверное — я, кстати, не знаю, как, — можно переопределить, но документация описывает именно то поведение, которое я озвучил. Переопределяя его, вы нарушаете ожидания клиентского кода.
Тем не менее, в Autofac она такова. Я не уверен, что это хорошая идея, поэтому я предпочитаю комбинацию Func/Disposable, но у нее есть свои недостатки. И в любом случае, это не решение для частого использования.
Bonart
Неужели? Вот что написано по вашей же ссылке об ожиданиях клиентского кода:
Сразу после — пример того самого клиентского кода.
И только потом — объяснение, как оно работает по умолчанию.
Пока клиентский код исполняет контракт «can be released by the owner when it is no longer required» никаких проблем с нарушением ожиданий нет и не будет.
Вот поэтому я и сделал свое решение — оно не привязывает ни к какому конкретному инструменту как Owned и не имеет проблем с зависимостями как Func/Disposable.
lair
Там нет ни одного слова о том, что это поведение по умолчанию, и оно может быть изменено. Поэтому я считаю его такой же частью контракта, как и то, что над примером кода.
Зато привязывает к вашему инструменту, и имеет неопределенную семантику.
Bonart
Полагаю, что напрасно. Такая трактовка вас же и ограничивает, при этом не давая никаких плюшек взамен.
Контракт в виде интерфейса с двумя членами и поведением, описываемым в одну строку, к чему-то привязывает? Ну я даже не знаю.
Так это только плюс: от клиента требуется сущий мизер, а при реализации Composition Root у вас полностью развязаны руки.
lair
Как раз наоборот. В качестве плюшки я получаю заведомо определенное поведение.
Конечно. Я должен иметь бинарную зависимость от этого интерфейса, например.
Это минус как раз. Контракт с неопределенной семантикой — это, по сути, не контракт, а видимость оного.
Bonart
Где вы нашли неопределенность? На стороне клиента — «я могу известить, что этот ресурс мне нужен». На стороне Composition Root — «как только ресурс не будет нужен клиенту — я об этом узнаю». Напротив, семантика определена весьма жестко и компактно, соблюдать такой контракт проблем нет c обеих сторон.
Это только когда моя статья превратиться в готовый к установке nuget-пакет. Но даже тогда зависимость будет не столь неприятной, как от контейнера, которому место строго в Composition Root.
Определенность, в которой указаны особенности работы с lifetimeScope для класса-клиента скорее вредна чем бесполезна.
lair
Там, где вы выше написали, что неопределенная семантика — это благо.
А такой контракт мне просто не нужен, он не решает моих задач.
А вот это предоставьте решать клиенту. Вы можете не верить, но иногда клиентам нужно строго детерминированное время жизни зависимости.
mird
Вы в этой статье написали некую реализацию
которая является оберткой вокруг IDisposable. При этом совершенно не очевидно, как она решает проблемы вынесенные в начало статьи, а главное, не понятно зачем эти проблемы решать и проблемы ли это. Именно это я называю «обертки ничего не упрощают, при этом усложняя код или компиляцию».Если бы вы не вырывали из контекста, вы бы увидели, что пример кода там был в случае, если хочется больше контроля над disposable зависимостями. А значит данная конкретная зависимость наследуется от IDisposable. Наиболее правильно (и об этом я пишу выше), передать управление зависимостями DI контейнеру.
Bonart
То есть вы не готовы показать усложнение кода или компиляции на примере? Очень жаль, без них ваша оценка сильно теряет в убедительности.
Кстати, Николас Блумхард тоже почему-то с вами не согласен.
Вот именно что НЕ значит. Не может зависимость отвечать за свои зависимости и так далее, а вот Composition Root может и должен. Но есть нюанс: о том, что с момента X зависимость Y больше не нужна объекту Z этот самый объект должен как-то сообщить. Мой IDisposable<T>, так же как и Owned из Autofac, именно эту задачу и решает.
mird
Если эта зависимость не диспозабл, а внутри нее какие-то диспозабл зависимости, то этот код должен переехать глубже (к диспозабл зависимостям).
Я понимаю что такое Owned из Autofac, и его использование, что характерно, оверхеда почти не добавляет (потому что весь оверхед в том, что в фабрику мне нужно добавить слово Owned). При этом я делегирую управление зависимостью Autofac. Но я продолжаю не понимать зачем мне ваша реализация без DI контейнера? Какой от нее профит?
Bonart
Не должен. По определению паттерна Dependency Injection классу-клиенту безразличны любые аспекты реализации зависимости. Значение имеет только контракт (интерфейс).
Предположим, у вас есть такой ресурс, как соединение (IConnection)
Его контракт не допускает параллельное использование в нескольких разных задачах.
Поддержка соединения в рабочем состоянии кое-чего стоит, но создание нового.гораздо дороже.
Вы хотите полностью развязать себе руки в части выбора способа управления соединениями, при этом сохранив все возможности достичь максимальной эффективности.
Нет никакого смысла позволять клиентам делать вызов Dispose для соединения, так как вам вовсе не нужно закрывать его каждый раз.
Но нельзя давать соединение новому клиенту, если его еще использует старый.
Решение:
Давать клиентам зависимость в виде
Клиенты получают соединение, пользуются им, после чего вызывают Dispose у IDisposable<IConnection>
Реализовать «фабрику» соединений можно так:
lair
(не Dependency Injection, а Dependency Inversion, в данном случае, но не принципиально).
На самом деле, именно поэтому мы не должны ничего знать о том, есть ли какие-то зависимости у того, чем мы пользуемся; и если объект, которым мы пользуемся, не предоставляет семантики Release/Dispose, значит, навязывать ее ему некорректно.
Ну и типовой коннекшн пул, прекрасно реализуется без дополнительных оберток. Более того, реализуется прозрачно для клиента.
Bonart
А где навязывание? Есть только комбинирование основной семантики и Dispose с помощью обобщенного типа.
Вы же не отвергаете по тому же принципу обобщенные коллекции?
Пример прозрачной для клиента реализации можете привести?
Кстати, у меня само соединение о пуле тоже ничего не знает.
lair
Ну так странно же комбинировать Dispose с объектом, у которого его нет.
SqlConnection.
Bonart
Не более странно чем комбинировать одиночные объекты в коллекции.
Там ЕМНИП соединения очень даже хорошо знают о пуле. И в плане простоты что связей что иерархии классов далеко не положительный пример.
lair
Неа. Обязанность одиночного объекта от комбинирования в коллекции не меняется.
Вы просили простую для клиента. И для клиента проще придумать сложно.
Bonart
Как и обязанности одиночной реализации от заворачивания в IDisposable<T>
Сначала вы говорите про «прекрасно реализуется», а потом приводите в качестве примера чужое и тяжеловесное? Видимо, не так уж оно и прекрасно на практике-то?
Кстати, я для клиента проще не только придумал, но и реализовал. Клиент вообще не имел дела с соединениями, только с транзакциями.
lair
А какая разница, свое оно или чужое? Мне достаточно того, что оно работает с заданной семантикой.
Пример кода в студию. Только именно кода connection (object) pool, потому что как убрать от пользователя коннекшны, я как раз прекрасно знаю, только здесь это не обсуждалось. А то мне как-то сложно себе представить что-то проще чем
Ну да, явный Open можно бы убрать было. Но это с пулингом не связано.
mird
Ну так либо интерфейс зависимости является IDisposable и тогда клиент знает что любая пришедшая сюда зависимость Disposable (и это не деталь реализации а контракт), либо не нужно ее диспозить.
Про коннекшн пул не слышали? Типовой паттерн.
Bonart
Либо вы так и не прочитали статью.
Зависимости нельзя диспозить, так как за их время жизни отвечает Composition Root, а не клиент.
Зависимости надо диспозить, так ненужные дорогие ресурсы надо освобождать как можно быстрее, но про момент ненужности знает только клиент, а не Composition Root.
Для разрешения этого противоречия и был придуман IDisposable<T>
Bonart
Не только слышал.
Вы точно прочитали мой комментарий? Там как раз про реализацию пула, прозрачную как для клиента, так и для самого соединения.
mird
А зачем мне ваша обертка над коннекшн пулом?
Bonart
Нельзя у зависимости вызвать Dispose: класс-клиент ей не владеет.
FiresShadow
mird
Например, класс использует эти ресурсы но не управляет ими.
Как тот же StreamWriter поверх переданного снаружи стрима. Смысл в том, что если ваш класс не создает внутри себя зависимость, а получает ее снаружи, он не должен ее и освобождать, потому что ему не известно что с этой зависимостью предполагается делать дальше.
creker
Поэтому у StreamWriter есть параметр в конструкторе, чтобы стрим извне не освобождался при Dispose у StreamWriter. И то и то поведение нужное и полезное, поэтому дан выбор. Не понятно, к чему пример конкретно с ним.
Bonart
Ответил про выбор поведения в другом комментарии