Dependency Injection - kukkich/Graphify GitHub Wiki
IoC контейнер - Inversion of control контейнер, можно считать это некоторым объектом, в котором мы перечисляем компоненты нашей программы, а затем он может нам инициализировать любой объект в нашей системе вместе со всеми зависимостями, которые нужны этому объекты и вложенным в него объектам.
Допустим у нас есть такой пример
public class View
{
private readonly ViewModel _viewModel;
public View(ViewModel viewModel)
{
_viewModel = viewModel;
}
//...
}
public class ViewModel
{
private readonly Model _model;
public ViewModel(Model model)
{
_model = model;
}
//...
}
public class Model
{
//...
}
Как бы мы руками инициализировали такую систему?
public static void Main()
{
var model = new Model();
var viewModel = new ViewModel(model);
var view = new View(viewModel);
}
Получается такая матрешка, но жить можно.
Очевидно что модель тоже должна принимать какие-то зависимости, при чем от всех остальных модулей. Получается примерно такой класс:
public class Model
{
private readonly IImporter _importer;
private readonly IExporter _exporter;
private readonly IGeometryFactory _geometryFactory;
private readonly IGeometryContext _geometryContext;
public Model(
IImporter importer,
IExporter exporter,
IGeometryFactory geometryFactory,
IGeometryContext geometryContext
)
{
_importer = importer;
_exporter = exporter;
_geometryFactory = geometryFactory;
_geometryContext = geometryContext;
}
}
И всю логику создания нам, опять же, надо прописывать в методе Main и вручную передавать все зависимости в конструктор:
public static void Main()
{
var importer = new Importer();
var exporter = new Exporter();
var factory = new GeometryFactory();
var context = new GeometryContext();
var model = new Model(importer, exporter, factory, context);
var viewModel = new ViewModel(model);
var view = new View(viewModel);
}
Допустим, мы захотели добавить логирование в наше приложение, теперь каждый компонент нашей системы должен получать объект логгера в конструкторе:
public class View
{
private readonly ViewModel _viewModel;
private readonly ILogger<View> _logger;
public View(ViewModel viewModel, ILogger<View> logger)
{
_viewModel = viewModel;
_logger = logger;
}
//...
}
public class ViewModel
{
private readonly Model _model;
private readonly ILogger<ViewModel> _logger;
public ViewModel(Model model, ILogger<ViewModel> logger)
{
_model = model;
_logger = logger;
}
//...
}
public class Model
{
private readonly IImporter _importer;
private readonly IExporter _exporter;
private readonly IGeometryFactory _geometryFactory;
private readonly IGeometryContext _geometryContext;
private readonly ILogger<Model> _logger;
public Model(
IImporter importer,
IExporter exporter,
IGeometryFactory geometryFactory,
IGeometryContext geometryContext,
ILogger<Model> logger
)
{
_importer = importer;
_exporter = exporter;
_geometryFactory = geometryFactory;
_geometryContext = geometryContext;
_logger = logger;
}
//...
}
Понятно, что такая матрешка может разрастаться до бесконечности. Проблема не нова и давно придумано решение - IoC контейнер. Его использование очень просто. Последний пример с логгером разрешался бы вот так:
Регистрация всех зависимостей в контейнере. Другими словами, перечисление всех компонентов нашей системы.
private static void ConfigureServices(IServiceCollection services)
{
Log.Logger = new LoggerConfiguration().CreateLogger();
services.AddLogging(loggingBuilder => loggingBuilder.AddSerilog(dispose: true));
services.AddScoped<Model>();
services.AddScoped<ViewModel>();
services.AddScoped<View>();
}
Запрос к IoC контейнеру на создание объекта View
public static void Main()
{
var services = new ServiceCollection();
ConfigureServices(services);
var provider = services.BuildServiceProvider();
var view = provider.GetRequiredService<View>();
}
При этом будут инициализированы все зависимости всех классов, IoC контейнер создаст их с передаст в нужные конструкторы за нас.
Также хорошей практикой является выделение регистрации компонентов каждого модуля в отдельном методе расширения, при чем этот метод должен быть прописан в самом модуле, где реализованы эти компоненты. Таким образом, ответственность за регистрацию компонентов переходит из нашего метода Main в каждый отдельный модуль, а в методе Main мы будем лишь подключать модули целиком, а не по одному компоненту.
public static class ServicesExtension
{
public static void AddIO(this IServiceProvider services)
{
services.AddScoped<IImporter, Importer>();
services.AddScoped<IExporter, Exporter>();
}
}
public static class ServicesExtension
{
public static void AddGeometry(this IServiceProvider services)
{
services.AddScoped<IGeometryFactory, GeometryFactory>();
services.AddScoped<IGeometryContext, GeometryContext>();
}
}
Регистрация всех зависимостей
private static void ConfigureServices(IServiceCollection services)
{
Log.Logger = new LoggerConfiguration().CreateLogger();
services.AddLogging(loggingBuilder => loggingBuilder.AddSerilog(dispose: true));
services.AddScoped<Model>();
services.AddScoped<ViewModel>();
services.AddScoped<View>();
services.AddGeometry();
services.AddIO();
}
Запрос к IoC контейнеру
public static void Main()
{
var services = new ServiceCollection();
ConfigureServices(services);
var provider = services.BuildServiceProvider();
var view = provider.GetRequiredService<View>();
}
Там приводится пример для web приложения, но все правила по работе с IoC контейнером распространяются и на наш случай.