Хабр Курсы для всех
РЕКЛАМА
Практикум, Хекслет, SkyPro, авторские курсы — собрали всех и попросили скидки. Осталось выбрать!
При использовании Dependency Injection объект класса не только не должен отвечать за жизненный цикл своих зависимостей, он просто физически не может это делать: зависимость может реализовать IDisposable, а может не реализовать, но при этом у нее могут быть свои зависимости и так далее.У класса есть ссылка на зависимость, по этой ссылке можно вызвать Dispose.
class Foo : IDisposable
{
IDependency _dependency;
public Foo(IDependency dependency)
{
_dependency = dependency;
}
public void Dispose()
{
if (_dependency is IDisposable)
((IDisposable)_dependency).Dispose();
}
}
лучше вбрасывать фабрику и использовать ееНе совсем понял, что вы предлагаете. Какую фабрику? Что она создаёт? Как инициализирует то, что создаёт?
Лучше увидеть внутри Dispose применение стратегии, чем увидеть внутри Dispose часть логики освобождения ресурсов (как предлагает автор статьи), а потом разбираться, почему освобождение ресурсов работает совсем не так, как описано в Dispose, и как оно работает на самом деле.
Не совсем понял, что вы предлагаете. Какую фабрику? Что она создаёт? Как инициализирует то, что создаёт?
class Foo : IDisposable
{
Func<IDependency> _dependencyFactory;
public Foo(Func<IDependency> dependencyFactory)
{
_dependencyFactory= dependencyFactory;
}
public void SomMethod()
{
...
using(var dependency = _dependencyFactory())
{
///используем зависимость.
}
}
}
Предлогаемые им обертки ничего не упрощают, при этом усложняя код или компиляцию.
Если вы работаете с DI контейнером, можно сделать так
Func<Owned<IDependency>> _dependencyFactory;
Func<IDisposable<IDependency>> _dependencyFactory;
Owned<T> и любым другим классом (если только вы не написали свое расширение к Autofac): когда вы сделаете Dispose на Owned<T>, Autofac закроет соответствующий LifetimeScope, и все зависимости, которые он создавал под T, будут явно отпущены.Семантика абсолютно одинаковая.
IDisposable<T> полностью определяется тем, кто его создает, и может не делать вообще ничего.Э нет. Семантика IDisposable — я с ресурсом закончил, освободи его немедленно.
Семантика обобщенного IDisposable отличается от обычного примерно так же как «можете быть свободны» от «немедленно освободите помещение».
Owned семантика другая, и она означает: закройте lifetime scope, который был открыт при создании Owned. Это, только это, и ничего, кроме этого.закройте lifetime scope, который был открыт при создании Owned
Это всего лишь сценарий по умолчанию для автоматического разрешения зависимостей с помощью Autofac.
Данное поведение легко переопределить средствами самого Autofac как для конкретного типа, так и в общем случае.
Ваша трактовка на уровне класса-клиента совершенно избыточна и прямо противоречит паттерну Dependency Injection.
Данное поведение, наверное — я, кстати, не знаю, как, — можно переопределить, но документация описывает именно то поведение, которое я озвучил. Переопределяя его, вы нарушаете ожидания клиентского кода.
An owned dependency can be released by the owner when it is no longer required. Owned dependencies usually correspond to some unit of work performed by the dependent component.
Я не уверен, что это хорошая идея, поэтому я предпочитаю комбинацию Func/Disposable, но у нее есть свои недостатки.
И только потом — объяснение, как оно работает по умолчанию.
Вот поэтому я и сделал свое решение — оно не привязывает ни к какому конкретному инструменту как Owned и не имеет проблем с зависимостями как Func/Disposable.
Там нет ни одного слова о том, что это поведение по умолчанию, и оно может быть изменено. Поэтому я считаю его такой же частью контракта, как и то, что над примером кода.
Зато привязывает к вашему инструменту, и имеет неопределенную семантику.
имеет неопределенную семантику
Полагаю, что напрасно. Такая трактовка вас же и ограничивает, при этом не давая никаких плюшек взамен.
Контракт в виде интерфейса с двумя членами и поведением, описываемым в одну строку, к чему-то привязывает?
Так это только плюс: от клиента требуется сущий мизер, а при реализации Composition Root у вас полностью развязаны руки.
Контракт с неопределенной семантикой — это, по сути, не контракт, а видимость оного.
Конечно. Я должен иметь бинарную зависимость от этого интерфейса, например.
Как раз наоборот. В качестве плюшки я получаю заведомо определенное поведение.
Где вы нашли неопределенность?
На стороне клиента — «я могу известить, что этот ресурс мне нужен». На стороне Composition Root — «как только ресурс не будет нужен клиенту — я об этом узнаю».
Определенность, в которой указаны особенности работы с lifetimeScope для класса-клиента скорее вредна чем бесполезна.
IDisposable<T> которая является оберткой вокруг IDisposable. При этом совершенно не очевидно, как она решает проблемы вынесенные в начало статьи, а главное, не понятно зачем эти проблемы решать и проблемы ли это. Именно это я называю «обертки ничего не упрощают, при этом усложняя код или компиляцию».Нельзя сделать так.
IDependency не обязан наследоваться от IDisposable
Экземпляр реализации IDependency может переиспользоваться (пример — соединения)
У реализации IDependency могут быть собственные зависимости
При этом совершенно не очевидно, как она решает проблемы вынесенные в начало статьи, а главное, не понятно зачем эти проблемы решать и проблемы ли это. Именно это я называю «обертки ничего не упрощают, при этом усложняя код или компиляцию».
А значит данная конкретная зависимость наследуется от IDisposable
Если эта зависимость не диспозабл, а внутри нее какие-то диспозабл зависимости, то этот код должен переехать глубже (к диспозабл зависимостям).
Но я продолжаю не понимать зачем мне ваша реализация без DI контейнера? Какой от нее профит?
Func<IDisposable<IConnection>>
() => context =>
{
var pool = context.Resolve<IConnectionPool>();
return pool.GetConnection().ToDisposable(connection => pool.PutConnection(connection));
}
По определению паттерна Dependency Injection классу-клиенту безразличны любые аспекты реализации зависимости. Значение имеет только контракт (интерфейс).
Предположим, у вас есть такой ресурс, как соединение (IConnection)
Его контракт не допускает параллельное использование в нескольких разных задачах.
Поддержка соединения в рабочем состоянии кое-чего стоит, но создание нового.гораздо дороже.
Вы хотите полностью развязать себе руки в части выбора способа управления соединениями, при этом сохранив все возможности достичь максимальной эффективности.
Нет никакого смысла позволять клиентам делать вызов Dispose для соединения, так как вам вовсе не нужно закрывать его каждый раз.
Но нельзя давать соединение новому клиенту, если его еще использует старый.
На самом деле, именно поэтому мы не должны ничего знать о том, есть ли какие-то зависимости у того, чем мы пользуемся; и если объект, которым мы пользуемся, не предоставляет семантики Release/Dispose, значит, навязывать ее ему некорректно.
Ну и типовой коннекшн пул, прекрасно реализуется без дополнительных оберток. Более того, реализуется прозрачно для клиента.
Есть только комбинирование основной семантики и Dispose с помощью обобщенного типа.
Пример прозрачной для клиента реализации можете привести?
Ну так странно же комбинировать Dispose с объектом, у которого его нет.
SqlConnection
Не более странно чем комбинировать одиночные объекты в коллекции.
Там ЕМНИП соединения очень даже хорошо знают о пуле. И в плане простоты что связей что иерархии классов далеко не положительный пример.
Неа. Обязанность одиночного объекта от комбинирования в коллекции не меняется.
Вы просили простую для клиента. И для клиента проще придумать сложно.
Сначала вы говорите про «прекрасно реализуется», а потом приводите в качестве примера чужое и тяжеловесное?
Кстати, я для клиента проще не только придумал, но и реализовал
using(var cn = new SqlConnection(...))
{
cn.Open();
//...
}
Не должен. По определению паттерна Dependency Injection классу-клиенту безразличны любые аспекты реализации зависимости. Значение имеет только контракт (интерфейс).
Предположим, у вас есть такой ресурс, как соединение (IConnection)
Его контракт не допускает параллельное использование в нескольких разных задачах.
Поддержка соединения в рабочем состоянии кое-чего стоит, но создание нового.гораздо дороже.
Вы хотите полностью развязать себе руки в части выбора способа управления соединениями, при этом сохранив все возможности достичь максимальной эффективности.
Нет никакого смысла позволять клиентам делать вызов Dispose для соединения, так как вам вовсе не нужно закрывать его каждый раз.
Но нельзя давать соединение новому клиенту, если его еще использует старый.
Ну так либо интерфейс зависимости является IDisposable и тогда клиент знает что любая пришедшая сюда зависимость Disposable (и это не деталь реализации а контракт), либо не нужно ее диспозить.
Про коннекшн пул не слышали? Типовой паттерн.
Зависимость сама может не быть IDisposable, в отличие от ее собственных зависимостей, зависимостей ее зависимостей и т.д.То есть объект косвенно владеет ресурсами (пусть и через свои зависимости), но его класс не реализует IDisposable? Зачем так сделано? Имхо, класс, управляющий управляемыми ресурсами, сам становится управляемым ресурсом (потому что его надо явно освободить), поэтому ему следует реализовывать IDisposable.
Тут можно было просто добавить событие\делегат в ViewModel, тогда cohesion было бы выше при том же coupling.var disposableViewModel = new ViewModel().ToDisposable(vm => { observableCollection.Add(vm); return () => observableCollection.Remove(vm); });
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);
зависимость может реализовать IDisposable, а может не реализовать
IDisposable, то в чем проблема-то? Не реализует — не диспозь.семантика-то разная совершенно.
Так давайте для этого использовать ISignalToContainer (он же Owned в автофаке)
Чем она разная-то? И там, и там «это мне больше не нужно».
… и пронесем зависимость от контейнера в обычный класс?
IDisposable<T> — оно чем-то лучше?А так пронесем зависимость от вашего IDisposable — оно чем-то лучше?
Тем, что нет никакой привязки к контейнеру и никаких лишних обязательств для реализации.
Ну и да, тема абстракции от DI-контейнера, конечно, интересная и плодотворная, но она совершенно не связана с тем, что вы в посте пишете.
Вы можете так считать. Я полагаю, что поддержка абстракции классов-клиентов от контейнеров — один из типовых вариантов использования для IDisposable<T> Просто в Autofac эта тема на мой взгляд раскрыта настолько полно, что я предпочел ограничиться ссылкой.
Тем, что нет никакой привязки к контейнеру и никаких лишних обязательств для реализации.
Вот именно, что нет обязательств — что означает, что мне никто не обещает, что поведет себя каким-то конкретным образом.
CompositionRoot реализуете не вы, а кто-то другой?
Вам дают ресурс и просят сообщить, когда он он перестанет быть вам нужен. Чего не хватает?
Так вот, я запрашиваю зависимость
Так вот, я запрашиваю зависимость, и я хочу быть уверен, что (а) эта зависимость (вместе с деревом зависимостей) будет порезолвлена именно тогда, когда я попрошу, и (б) эта зависимость (вместе с деревом зависимостей) будет отпущена именно тогда, когда я попрошу.
Это у вас уже не Dependency Injection, а Service Locator какой-то
Понятно, в рамках 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». К явному управлению жизненным циклом это отношения не имеет.К явному управлению жизненным циклом это отношения не имеет
IDisposable<T>.Dispose(), как уже обсуждалось, не определено.>CompositionRoot реализуете не вы, а кто-то другой?
Не я.
эта зависимость (вместе с деревом зависимостей) будет отпущена именно тогда, когда я попрошу.
А сообщать о том, что ресурс мне больше не нужен
Тогда у вас никаких гарантий по определению
Кстати, очистка lifetimeScope внутри Owned на самом деле ничего конкретного вам не гарантирует.
… кроме тех, которые предоставляет контейнер.
Почему же. Она мне гарантирует то, что я прошу: открытие/закрытие скоупа по открытию/закрытию owned.
А в контейнере все определяется его конфигурацией.
Это, к примеру, дурная привычка StreamReader закрывать нижележащий Stream при вызове Dispose
Один bool вариант в StreamReader/StreamWriter покрывает все, что необходимо от этого класса — закрыть или не закрыть стрим, который ему дается.
IDisposable<T> позволяет гибко настроить оба вариантаStreamReader принимает на вход Stream, а не IDisposable.StreamReader/Writer, — я совершенно не понимаю, зачем куда-то о чем-то сигнализировать. Этот флаг покрывал все случавшиеся в моей практике варианты работы.Да, сознаюсь, я наверное в 100500 классов уже скопировал реализацию IDisposable, IComparable и т.д., но это особенность C#, и с ней не надо воевать, тем более есть тот же Решарпер. А все попытки добавить свой сахар и сделать «поудобнее» очень сильно пинают по производительности.
А в вашем варианте и множественное наследование не реализовать толком, и вызовы виртуальных функций и не sealed классы до сих пор такой пенальти по производительности дают, хоть стой хоть падай.
И как раз «наследование классов, вызовы виртуальный функций»: у вас есть класс с достаточно базовой функциональностью, и нам нужно запилить наследника с еще какой-то базовой функциональностью и начинается пляска с кучей интерфейсов на разных этажах.
При использовании Dependency Injection объект класса не только не должен отвечать за жизненный цикл своих зависимостей, он просто физически не может это делать: зависимость может разделяться между несколькими клиентами
Кстати, даже если регистрировать объект как синглтон — он обязан быть Disposable, если имеет ресурсы, не очищаемые сборщиком мусора.
Потому что иначе класс будет содержать утечки ресурсов by design
Понимаете ли, избыточный код — это тоже оверхед. Его надо осознавать, его надо поддерживать.
Ну и да, получается, что нет никакого «объект обязан быть Disposable, если он имеет ресурсы, не очищаемые сборщиком мусора»
«я предпочитаю делать такие объекты Disposable, потому что я никогда не знаю, как они будут использованы»
Недостающий код — оверхед много больший.
Класс, который сам по себе течет как слониха в красный день календаря… причина выбора столь своеобразного метода очистки ресурсов до сих пор актуальна.
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 означает пренебрежение обязанностью, а не ее отсутствие.
IDisposable? Что изменится, если он не будет его иметь?Но на уровне реализации знать не хочу, так это будет неявной зависимостью.
Не стоит пытаться подогнать условия под ответ.
Кстати, даже если регистрировать объект как синглтон — он обязан быть Disposable, если имеет ресурсы, не очищаемые сборщиком мусора.
В таком случае реализация будет зависеть от того, что является «подходящим моментом» или кто его определяет.
Т.е. когда класс проектируется как IDisposable, то предполагается, что его инстанцирование будет производиться в using.
Если он является частью агрегата, то корень этого агрегат должен вызывать его Dispose в своём Dispose (в случае RAII деструкторы частей агрегата вызываются при разрушении корня агрегата). В DI-контейнерах такие объекты регистрируются как transient и освобождаются явно, как конкретно — зависит от контейнера.
Не будет. Класс, владеющий ресурсами, которые нельзя освободить автоматически, обязан дать возможность сделать это вручную. В какой именно момент освобождать — ответственность не объекта, а его владельца.
Так что вы будете делать в условиях задачи?
Вызов Dispose у LifetimeScope автоматически вызовет Dispose у всех созданных в ней объектов.
То, как класс следует использовать, определяется при проектировании класса.
Например, если я реализую класс SingleInstance, задача которого — держать открытым некий файл на протяжении работы процесса для контроля единственности запущенного экземпляра, я вызову Dispose() у файлового потока (он обязан быть вызван по задумке его разработчиков) в ~SingleInstance. А SingleInstance.IDisposable реализовывать, на всякий случай, я не буду. Потому что так класс задуман, такая у него задача.
Приведите условие конкретной задачи
Но необходимость освобождать ресурсы в произвольный момент должна быть чем-то вызвана. Она не безусловна, как вы утверждаете. И уж тем более такая необходимость крайне сомнительна, когда речь идёт об очень большом ресурсе, разделяемом между большим количеством потребителей.
Кто вызывает Dispose у LifetimeScope?
Включать в контракт класса, владеющего ресурсами, реализацию IDisposable — это паттерн.
А вот включать в контракт класса контроль его единственности на процесс — это антипаттерн
В задаче достаточно информации для решения.
Disposable без границ