Правильное использование DI и IoC

312
02 июля 2021, 23:10

Начал изучать принципы DI и работу с IoC контейнерами. Вопрос следующего характера: Пусть у меня есть некая библиотека классов MyClassLibrary, там представлены следующие классы:

public class DefaultDownloader : IDownloader
{
    public string DownloadSomething(string param)
    {
        return $"{nameof(DefaultDownloader)} {param}";
    }
}
public static class MyContainer
{
    public static IContainer Container { get; set; }
    static MyContainer()
    {
        var builder = new ContainerBuilder();
        builder.RegisterType<DefaultDownloader>().As<IDownloader>();
        builder.RegisterType<FirstSource>().AsSelf();
        builder.RegisterType<SecondSource>().AsSelf();
        Container = builder.Build();
    }
}
public class SourceControl
{
    public string GetAllInfo(string param)
    {
        string toReturn = "";
        using (var scope = MyContainer.Container.BeginLifetimeScope())
        {
            var source1 = scope.Resolve<FirstSource>();
            var source2 = scope.Resolve<SecondSource>();
            toReturn += source1.DownloadInformation("FirstSource");
            toReturn += " "+ source2.DownloadInformation("SecondSource");
        }
        return toReturn;
    }
}
public class FirstSource
{
    private readonly IDownloader downloader;
    public FirstSource(IDownloader downloader)
    {
        this.downloader = downloader;
    }
    public string DownloadInformation(string param)
    {
        return downloader.DownloadSomething(param);
    }
}
public class SecondSource
{
    private readonly IDownloader downloader;
    public SecondSource(IDownloader downloader)
    {
        this.downloader = downloader;
    }
    public string DownloadInformation(string param)
    {
        return downloader.DownloadSomething(param);
    }
}
public interface IDownloader
{
    string DownloadSomething(string param);
}

Этими классами можно пользоваться и по отдельности, но есть еще один класс (SourceControl), который позволяет забирать информацию со всех источников. В классе необходимо создать экземпляры источников (FirstSource, SecondSource) и вызвать некоторые методы оттуда. Соответственно, в этом же классе придется внедрять зависимости в FirstSource и SecondSource.

Использование извне:

class Program
{
    private static IContainer Container { get; set; }
    public static void ConfigureContainer()
    {
        var builder = new ContainerBuilder();
        builder.RegisterType<MySecondDownloader>().As<IDownloader>();
        builder.RegisterType<FirstSource>().AsSelf();
        builder.RegisterType<SecondSource>().AsSelf();
        Container = builder.Build();
    }
    static void Main(string[] args)
    {
        // Следующие две строчки можно закомментировать, чтобы использовать
        // "стандартный" контейнер
        ConfigureContainer();
        MyContainer.Container = Container;
        SourceControl sourceControl = new SourceControl();
        Console.WriteLine(sourceControl.GetAllInfo("Main"));
        Console.ReadLine();
    }
}
public class MyFirstDownloader : IDownloader
{
    public string DownloadSomething(string param)
    {
        return $"{nameof(MyFirstDownloader)} {param} - MODIFIED! (1)";
    }
}
public class MySecondDownloader : IDownloader
{
    public string DownloadSomething(string param)
    {
        return $"{nameof(MySecondDownloader)} {param} - MODIFIED! (2)";
    }
}

Вопрос: Как это более правильно сделать с точки зрения логики и DI. В качестве IoC контейнера планирую использовать autofac. И правильно ли я сейчас это делаю?

Answer 1

Ваш подход вызывает вопросы, например, зачем нужны разные классы для Downloader'ов, зачем разные источники, почему это нельзя просто сконфигурировать как надо и прочее.

Но давайте отойдем от этих вопросов и попробуем поработать с вашим кодом как есть.

Что мы имеем: 3 имплементации одного и того же интерфкейа, а значит, чтобы их различать, можно их регистрировать с конкретными именами.

Далее, источники должны быть ассоциированы только с соответствующими реализациями Downloader'ов.

И наконец, SourceControl должен быть также зарегистрирован и создан с помощью контейнера.

Ну и последнее, все эти именованные регистрации и сопоставления интерфейсов с реализацией должны находиться только на этапе конфигурации контейнера, то есть это должно быть абсолютно прозрачно для классов.

Со всеми этими требованиями, давайте сначала перепишем класс SourceControl

public class SourceControl
{
    private ILifetimeScope  _scopeFactory;
    public SourceControl(ILifetimeScope  scopeFactory)
    {       
        _scopeFactory = scopeFactory;       
    }
    public string GetAllInfo(string param)
    {
        string toReturn = "";
        using (var scope = _scopeFactory.BeginLifetimeScope())
        {
            var source1 = scope.Resolve<FirstSource>();
            var source2 = scope.Resolve<SecondSource>();
            toReturn += source1.DownloadInformation("FirstSource");
            toReturn += "\n" + source2.DownloadInformation("SecondSource");
        }
        return toReturn;
    }
}

Создание контейнера становится таким

private static IContainer ConfigureContainer()
{
    var builder = new ContainerBuilder();
    builder.RegisterType<MyFirstDownloader>().Named<IDownloader>("MyFirstDownloader");
    builder.RegisterType<MySecondDownloader>().Named<IDownloader>("MySecondDownloader");
    builder.RegisterType<DefaultDownloader>().Named<IDownloader>("DefaultDownloader");
    builder
        .RegisterType<FirstSource>()
        .WithParameter(
            new ResolvedParameter(
                (p, c) => p.ParameterType == typeof(IDownloader),
                (p, c) => c.ResolveNamed<IDownloader>("MyFirstDownloader")));
    builder
        .RegisterType<SecondSource>()
        .WithParameter(
            new ResolvedParameter(
                (p, c) => p.ParameterType == typeof(IDownloader),
                (p, c) => c.ResolveNamed<IDownloader>("MySecondDownloader")));  
    builder
        .RegisterType<SourceControl>(); 
    var container = builder.Build();
    return container;
}

Использование элементарное

var container = ConfigureContainer();
var control = container.Resolve<SourceControl>();    
Console.WriteLine(control.GetAllInfo("Main"));

Вывод

MyFirstDownloader FirstSource - MODIFIED! (1)
MySecondDownloader SecondSource - MODIFIED! (2)

Очень рекомендую к прочтению книгу Марка Симана - Внедрение зависимостей в .NET, многие вещи после этого станут на свои места.

UPD

Что тут ещё можно сделать. Если заметить, то источники по сути одинаковые, так что можно выделить интерфейс

public interface ISource
{   
    string DownloadInformation(string param);
}

Зарегистрировать источники как интерфейсы

private static IContainer ConfigureContainer()
{
    var builder = new ContainerBuilder();
    builder.RegisterType<MyFirstDownloader>().Named<IDownloader>("MyFirstDownloader");
    builder.RegisterType<MySecondDownloader>().Named<IDownloader>("MySecondDownloader");
    builder.RegisterType<DefaultDownloader>().Named<IDownloader>("DefaultDownloader");
    builder
        .RegisterType<FirstSource>()
        .As<ISource>()
        .WithParameter(
            new ResolvedParameter(
                (p, c) => p.ParameterType == typeof(IDownloader),
                (p, c) => c.ResolveNamed<IDownloader>("MyFirstDownloader")));
    builder
        .RegisterType<SecondSource>()
        .As<ISource>()
        .WithParameter(
            new ResolvedParameter(
                (p, c) => p.ParameterType == typeof(IDownloader),
                (p, c) => c.ResolveNamed<IDownloader>("MySecondDownloader")));

    builder
        .RegisterType<SourceControl>();

    var container = builder.Build();
    return container;
}

И получать после просто их список

public class SourceControl
{
    private ILifetimeScope  _scopeFactory;
    public SourceControl(ILifetimeScope  scopeFactory)
    {       
        _scopeFactory = scopeFactory;       
    }
    public string GetAllInfo(string param)
    {
        var sb = new StringBuilder();
        using (var scope = _scopeFactory.BeginLifetimeScope())
        {
            var sources = scope.Resolve<IEnumerable<ISource>>();                
            foreach(var source in sources) 
                sb.AppendLine(source.DownloadInformation(source.GetType().Name));
        }
        return sb.ToString();
    }
}

Вывод будет аналогичный, но SourceControl теперь не знает ни о каких реализациях вообще.

UPD 2

Переписал SourceControl

public class SourceControl
{
    private IEnumerable<ISource> _sources;
    public SourceControl(IEnumerable<ISource> sources)
    {
        _sources = sources;
    }
    public string GetAllInfo(string param)
    {
        var sb = new StringBuilder();
        foreach (var source in _sources)
            sb.AppendLine(source.DownloadInformation(source.GetType().Name));
        return sb.ToString();
    }
}
READ ALSO
Программа выключается при запуске, а должна работать постоянно

Программа выключается при запуске, а должна работать постоянно

Написал сервер для чата, который должен работать постоянноНо он почему то выключается при запуске, исключений не бросает

221
Создание сайта в IIS из PowerShell

Создание сайта в IIS из PowerShell

Мне необходимо создать программно новый сайт в IISДля этого использую команду:

118
Значение по имени экземпляра класса

Значение по имени экземпляра класса

Можно ли как-то получить данные из класса, используя только имя экземпляра классаТо есть

105
C# WebSocket Server

C# WebSocket Server

Как создать вебсокет сервер, чтобы к нему могли подключиться не только локально, но и пользователи по всему миру?

163