ServiceContainer, IoC и одноразовые предметы
У меня есть вопрос, и я собираюсь обозначить это субъективное, поскольку, как мне кажется, оно превращается в дискуссию. Я надеюсь на некоторые хорошие идеи или провокаторов. Я прошу прощения за многословный вопрос, но вы должны знать контекст.
Вопрос в основном:
- Как вы относитесь к конкретным типам по отношению к контейнерам IoC? В частности, кто отвечает за их удаление, если они требуют утилизации, и как эти знания распространяются на вызывающий код?
Вы требуете, чтобы они были IDisposable? Если нет, то является ли этот код перспективным, или вы не можете использовать одноразовые предметы? Если вы применяете требования IDisposable к интерфейсам и конкретным типам, чтобы быть ориентированными на будущее, кто несет ответственность за объекты, внедряемые как часть вызовов конструктора?
Редактировать: я принял ответ Chris Ballard, так как он наиболее близок к подходу, с которым мы закончили.
По сути, мы всегда возвращаем тип, который выглядит следующим образом:
public interface IService<T> : IDisposable
where T: class
{
T Instance { get; }
Boolean Success { get; }
String FailureMessage { get; } // in case Success=false
}
Затем мы возвращаем объект, реализующий этот интерфейс, как из.Resolve, так и.TryResolve, так что то, что мы получаем в вызывающем коде, всегда имеет один и тот же тип.
Теперь объект, реализующий этот интерфейс, IService<T>
IDisposable, и всегда должен быть утилизирован. Программист, который разрешает службу, не может решить, IService<T>
объект должен быть утилизирован или нет.
Тем не менее, и это важная часть, должен ли экземпляр службы располагаться или нет, что знания внедряются в объект, реализующий IService<T>
, так что если это сервис с фабричной областью (т. е. каждый вызов Resolve заканчивается новым экземпляром службы), то экземпляр службы будет удален, когда IService<T>
Объект расположен.
Это также позволило поддерживать другие специальные области, такие как пул. Теперь мы можем сказать, что нам нужно минимум 2 экземпляра службы, максимум 15 и обычно 5, что означает, что каждый вызов.Resolve либо извлечет экземпляр службы из пула доступных объектов, либо создаст новый. А потом, когда IService<T>
объект, содержащий службу в пуле, удаляется, экземпляр службы освобождается обратно в пул.
Конечно, это заставило весь код выглядеть так:
using (var service = ServiceContainer.Global.Resolve<ISomeService>())
{
service.Instance.DoSomething();
}
но это чистый подход, и он имеет один и тот же синтаксис независимо от типа службы или конкретного используемого объекта, поэтому мы выбрали это в качестве приемлемого решения.
Исходный вопрос следует для потомков
Длинный вопрос приходит сюда:
У нас есть контейнер IoC, который мы используем, и недавно мы обнаружили, что является проблемой.
В коде, отличном от IoC, когда мы хотели использовать, скажем, файл, мы использовали такой класс:
using (Stream stream = new FileStream(...))
{
...
}
Не было никаких сомнений относительно того, был ли этот класс чем-то, что содержало ограниченный ресурс, или нет, так как мы знали, что файлы должны быть закрыты, и сам класс реализовал IDisposable. Правило состоит в том, что каждый класс, из которого мы создаем объект, реализующий IDisposable, должен быть удален. Никаких вопросов не было задано. Пользователь этого класса не должен решать, является ли вызов Dispose необязательным или нет.
Итак, перейдем к первому шагу в направлении контейнера IoC. Давайте предположим, что мы не хотим, чтобы код говорил напрямую с файлом, а вместо этого пройдем один уровень косвенности. Давайте назовем этот класс BinaryDataProvider для этого примера. Внутри класс использует поток, который все еще является одноразовым объектом, поэтому приведенный выше код будет изменен на:
using (BinaryDataProvider provider = new BinaryDataProvider(...))
{
...
}
Это мало что меняет. Знание того, что класс реализует IDisposable, все еще здесь, без вопросов, нам нужно вызвать Dispose.
Но давайте предположим, что у нас есть классы, которые предоставляют данные, которые сейчас не используют такие ограниченные ресурсы.
Приведенный выше код может быть записан как:
BinaryDataProvider provider = new BinaryDataProvider();
...
Хорошо, пока все хорошо, но здесь идет главный вопрос. Давайте предположим, что мы хотим использовать контейнер IoC для внедрения этого провайдера вместо зависимости от конкретного конкретного типа.
Код будет тогда:
IBinaryDataProvider provider =
ServiceContainer.Global.Resolve<IBinaryDataProvider>();
...
Обратите внимание, что я предполагаю, что существует независимый интерфейс, через который мы можем получить доступ к объекту.
С учетом вышеуказанного изменения, что если мы позже захотим использовать объект, от которого действительно нужно избавиться? Ни один из существующих кодов, разрешающих этот интерфейс, не написан для удаления объекта, и что теперь?
Как мы видим, мы должны выбрать одно решение:
- Реализуйте проверку во время выполнения, которая проверяет, что если конкретный тип, который регистрируется, реализует IDisposable, требует, чтобы интерфейс, через который он предоставляется, также реализовывал IDisposable. Это не хорошее решение
- Прежде чем накладывать ограничения на используемые интерфейсы, они всегда должны наследоваться от IDisposable, чтобы быть ориентированными на будущее
- Обеспечить выполнение, чтобы никакие конкретные типы не могли быть IDisposable, так как это определенно не обрабатывается кодом, использующим контейнер IoC
- Просто оставьте это на усмотрение программиста, чтобы проверить, реализует ли объект IDisposable и "делать правильные вещи"?
- Есть ли другие?
Кроме того, как насчет внедрения объектов в конструкторы? Наш контейнер и некоторые другие контейнеры, которые мы рассмотрели, способны вводить свежий объект в параметр в конструктор конкретного типа. Например, если наш BinaryDataProvider
нужен объект, который реализует ILogging
Интерфейс, если мы применяем IDispose-"способность" к этим объектам, кто несет ответственность за удаление объекта регистрации?
Как вы думаете? Я хочу мнения, хорошие и плохие.
3 ответа
(Отказ от ответственности: я отвечаю на это на основе java. Хотя я программирую на C#, я ничего не проксировал в C#, но я знаю, что это возможно. Извините за терминологию java)
Вы могли бы позволить инфраструктуре IoC проверять создаваемый объект, чтобы увидеть, поддерживает ли он IDisposable. Если нет, вы можете использовать динамический прокси для обертывания фактического объекта, который инфраструктура IoC предоставляет клиентскому коду. Этот динамический прокси может реализовывать IDisposable, так что вы всегда будете предоставлять IDisposable клиенту. Пока вы работаете с интерфейсами, это должно быть довольно просто?
Тогда у вас просто возникнет проблема общения с разработчиком, когда объект является IDisposable. Я не совсем уверен, как это было бы сделано хорошим способом.
Одним из вариантов может быть использование заводского шаблона, чтобы объекты, созданные непосредственно контейнером IoC, никогда не нуждались в удалении самостоятельно, например
IBinaryDataProviderFactory factory =
ServiceContainer.Global.Resolve<IBinaryDataProviderFactory>();
using(IBinaryDataProvider provider = factory.CreateProvider())
{
...
}
Недостатком является сложность, но это означает, что контейнер никогда не создает ничего, от чего должен избавиться разработчик - это всегда явный код, который делает это.
Если вы действительно хотите сделать это очевидным, фабричный метод может быть назван как-нибудь как CreateDisposableProvider().
Вы действительно придумали очень грязное решение: ваш IService
Контракт нарушает ПСП, который является большим нет-нет.
Я рекомендую отличать так называемые "синглтонные" сервисы от так называемых "прототипных" сервисов. Время жизни одноэлементных управляется контейнером, который может запросить во время выполнения, реализует ли конкретный экземпляр IDisposable
и вызвать Dispose()
на отключение, если так.
Управление прототипами, с другой стороны, является полностью ответственностью вызывающего кода.