Dependency Injection - kukkich/Graphify GitHub Wiki

Для чего нужен IoC контейнер

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 мы будем лишь подключать модули целиком, а не по одному компоненту.

В модуле IO:

public static class ServicesExtension
{
    public static void AddIO(this IServiceProvider services)
    {
        services.AddScoped<IImporter, Importer>();
        services.AddScoped<IExporter, Exporter>();
    } 
}

В модуле Geometry:

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>();
}

Материалы для изучения:

  1. https://metanit.com/sharp/aspnet6/4.2.php
  2. https://metanit.com/sharp/aspnet6/4.4.php

Там приводится пример для web приложения, но все правила по работе с IoC контейнером распространяются и на наш случай.

⚠️ **GitHub.com Fallback** ⚠️