Страницы

Поиск по вопросам

суббота, 7 декабря 2019 г.

Кто должен отвечать за уничтожение переданных в конструктор другого класса объектов?

#c_sharp #net


Скажем, есть класс работы с БД.

Ему на вход может быть передан Connection. Затем, этот Connection запоминается в
поле класса и класс работает через него. А можно передать вообще new Connection(),
тогда ссылка на этот экземпляр будет только в этом классе. Так же может быть еще один
конструктор, где строковое предстовление будет передано и я уже в своем конструкторе
создам сам Connection.

Как правило один объектConnectionможет быть использован параллельно. В MS SQL точно,
на сколько я помню, это разрешено.

Так вот, кто в данном случае должен отвечать за уничтожения объекта?

У нас получается 3 случая:


Connection может быть использоваться параллельно в вызывающем коде
Connection используется только в 1 классе
Connection используется только в 1 классе 


В первом случае, очевидно, вызывающий код должен быть ответственным, а во втором
по идее, сам класс, в третим очевидно мы сами, так как мы собственноручно создали объект
из строки => не понятно, как реализовать паттерн IDiposible. Хранить где-то флаги?
Однако, с флагами опять же не будет понятно, храню ли я только в своем классе ссылку
или вызывающий код ее тоже имеет.

Более того- это должно быть очевидно для конечного пользователя, так как класс для
него- это черный ящик.

Для примера, если заглянуть в исходники майков:


SqlCommand 
SqlDataReader


, то мы видим, что в первом случае команда не диспозит Connection, хотя он хранится
в полях класса, а во втором случае так вообще не реализован интерфейс IDisposible.

Может быть вообще правильно наружу выставлять подобные ресурсы в ReadOnly виде. Тогда
сам вызывающий код в любом случае сможет это прибивать?

Еще одним решением я вижу- это в конструкторе задавать bool флаг по типу closeAfterDispose
и тогда мы однозначно сможем определить нужно ли соединение пользователю.

Пример со строкой подключения- это просто один из примеров. Такие же вопросы могут
возникнуть при использовании Stream'ов, например.
    


Ответы

Ответ 1



Сейчас при построении архитектуры приложения активно применяется внедрение зависимостей (Dependency Injection, DI). В общем случае, внедрение предполагает, что классы получают ссылки друг на друга через конструктор, но не создают и не уничтожают объекты. За создание и уничтожение объектов отвечает такая штука, которую называют IoC-контейнер. Уточнение. IoC это старое название DI. Подробности о том, почему IoC превратилось в DI читайте в статье Мартина Фаулера, Inversion of Control Containers and Dependency Injection pattern. Это та самая статья, где введён термин Dependency Injection. Вот вольный перевод на русский язык. Чтобы всё это работало, обычно используют библиотеку, такую как Castle, Autofac, Ninject. Всё то же самое можно сделать и вручную, но это будет сложнее и дольше. Если мы решаем, что именно IoC-контейнер отвечает за создание и уничтожение объектов, вопрос нужно переформулировать: как компоновать классы с разным временем жизни, которые зависят друг от друга? Например, у вас есть SqlConnection, который в приложении REST API обычно «живёт» на протяжении HTTP-запроса, а для доступа к данным вы используете какой-нибудь OrderRepository. Репозитории не содержат собственного состояния, поэтому их эффективно создать один раз и просто хранить в течение всего времени работы приложения. Мы не может передать в конструктор репозитория готовое соединение, его просто не существует. Мы должны создать фабрику или локатор, скажем, SqlConnectionFactory, которую будем вызывать, когда нам потребуется подключение. Фабрика создаёт нам новый объект, и, если он disposable, мы должны освободить его в том же методе, пользуясь оператором using. Локатор находит готовый объект, и мы не должны его освобождать сами. Для конкретного примера возьмём библиотеку Autofac. Для начала опишем зависимости между классами/интерфейсами. public class OrderRepository : IOrderRepository { private readonly Func _locateSqlConnection; public OrderRepository(Func locateSqlConnection) { _locateSqlConnection = locateSqlConnection; } public Order ReadById(int id) { var connection = _locateSqlConnection(); using (var command = connection.CreateCommand()) { // Здесь загружаем данные, конструируем Order и возвращаем его . . . return new Order(id, createdAt, amount); } } . . . } Здесь показано, как локатор может быть описан в Autofac. Когда будет вызвана функция _locateSqlConnection, Autofac вернёт существующее подключение, создав его, если его не было. Если ответственность за создание SqlConnection лежит на библиотеке Autofac, она же должна освободить объект. По умолчанию, Autofac сохраняет все ссылки на созданные IDisposable объекты и освобождает их, когда контейнер больше не нужен. Контейнеров в программе может быть несколько, и Autofac умеет создавать специальный контейнер для HTTP-запроса. Для разных запросов будут созданы разные контейнеры и, соответственно, разные экземпляры SqlConnection. Чтобы это всё работало, достаточно правильно описать «время жизни» при регистрации репозитория в Autofac. builder.Register() .As() .SingleInstance(); OrderRepository это singleton, который будет создан один раз. SqlConnection создаётся один раз на HTTP-запрос: builder.Register() .InstancePerRequest(); А что с SqlCommand? SqlCommand мы создаём сами в рамках метода OrderRepository.ReadById, поэтому и освобождать его мы должны сами. Подробнее о DI/IoC можно почитать в книге Внедрение зависимостей в .NET. В сети бродит PDF, но ссылку давать не буду, потому что не знаю, как там с авторскими правами.

Ответ 2



Надеюсь, что вопрос понял правильно :) На сколько я понимаю GarbageCollector(GC) важно количество ссылок на обьект с разных мест. То есть, если на некий обьект ссылается еще хотя бы один обьект - он не будет уничтожен. Так же есть обьекты которые нужно принудительно уничтожать из памяти, до того как их захавает GC. Они наследуются через iDisposable. У них нужно вызывать Dispose() для того, что бы очистить вручную не дожидаясь когда это сделает GC. В основном это используется для больших обьектов, например, картинок. Что б прога не жрала памяти по пару гигабайт на пустом месте, до момента, пока не проработает GC :) Connection может быть использоваться параллельно в вызывающем коде Он будет помечен на освобожденние как только закончится работа ОБЕИХ методов - и вызывающего и вызываемого. А освобожден во время вызова GC. В случае если это конекшн, то он наследуется от IDisposable только в случае, если ты хочешь сделать нагрузку на БД меньшей. То есть сделать так, что бы к ней было приконекчено как можно меньше пользователей одновременно. Если же проэкт маленький изначально, то с этим можно и не запариватся. Connection используется только в 1 классе Он будет помечен на освободление как только закончится этот метод. И будет диспоузонут автоматически при вызове GC. Ну или, лучше, диспоузонуть вручную (раз уж гарантированно!!! не затронет ничего лишнего) Вызывать Dispose() ты должен только в том случае, когда ты уверен что конекшн никем более не используется. Если же такой уверенности нет - лучше отдать эту обязанность на GC. И в случае с конекшнами к базам данных, хочу напомнить что базы данных сами могут обрывать конекшн по таймауту. То есть если клиент не отзывается некоторое время, то сервер перестает с ним работать.

Комментариев нет:

Отправить комментарий