Dependency Injection в .NET/C#

Dependency Injection (DI, внедрение зависимостей) — это набор принципов и паттернов проектирования программных продуктов, позволяющий разрабатывать слабосвязанный код. Стоит отметить, что Dependency Injection — это не фреймворк, не библиотека классов и т.д. Применительно к .NET/C#, DI часто отождествляют с конкретным фреймворком — ASP.NET Core. Это неверно. Применение DI — это возможность вообще всей платформы .NET, однако, именно в ASP.NET Core мощь и красота Dependency Injection раскрываются наиболее полно. В этой части мы рассмотрим то, как можно использовать DI в своих приложениях.

Зависимости. Пример DI

Зависимость — это любой объект, от которого зависит другой объект. Чтобы продемонстрировать, что это означает для нас, напишем простой пример в виде консольного приложения без использования операторов верхнего уровня. Вот как выглядит стандартный шаблон такого приложения:

namespace Example1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");
        }
    }
}

Допустим, мы решили, что было бы неплохо организовать в нашем приложении отдельный класс, который будет выводить различные сообщения в консоль. Такой класс может выглядеть следующим образом:

public class MessageWriter
{
    public void SendMessage(string text)
    {
        Console.WriteLine(text);
    }
}

Мы можем добавлять в этот класс различные свойства, методы, делать вывод сообщений разными цветами и так далее. В любом случае, использование этого класса в нашем приложении будет выглядеть следующим образом:

static void Main(string[] args)
{
    MessageWriter writer = new MessageWriter();
    writer.SendMessage("Hello, World!");
}

Опять же, всё прекрасно работает. В какой-то момент времени мы решили, что класс оказался настолько хорошим и удобным, что мы решили его использовать для вывода сообщений в другом классе, например так:

public class Calculator
{
    private readonly MessageWriter writer = new();

    public void Sum(int a, int b)
    {
        writer.SendMessage($"Сумма двух чисел равна: {a + b}");
    }

    public void Multiply(int a, int b)
    {
        writer.SendMessage($"Произведение двух чисел равно: {a * b}");
    }
}

И теперь в методе Main() мы можем задействовать наш новый класс:

static void Main(string[] args)
{
   Calculator calculator = new Calculator();
    calculator.Sum(10, 20);
    calculator.Sum(1, 2);
}

В консоли мы увидим результат:

Сумма двух чисел равна: 30
Сумма двух чисел равна: 3

Теперь мы можем вызывать методы класса Calculator и получать результаты вычислений в консоль. При этом, класс MessageWriter стал зависимостью для класса Calculator. И теперь мы подходим к одному из главных вопросов, касающихся проектирования приложений, а именно: как такой код сопровождать в дальнейшем? Ведь любой программный продукт поддерживается, развивается, в него добавляются новые функции и возможности. Сейчас всё работает прекрасно, но завтра нам потребуется выводить сообщения не в консоль, а, например, в файл или вообще отправлять по Сети. Сейчас в нашем коде прослеживается прямая зависимость:

Прямая зависимость
Прямая зависимость

Если нам потребуется изменить способ вывода результата, например, в файл Word, то нам придется в классе Calculator менять зависимость MessageWriter, например, на WordWriter. Когда мы имеем дело с таким простым приложением, как у нас, то заменить одну зависимость на другую не составляет никаких проблем. Проблемы возникают когда связи между различными частями приложения обеспечиваются через десятки различных зависимостей. В этом случае, чтобы заменить одну зависимость на другую нам может потребоваться пересмотреть весь код приложения, выявить все места, где используется устаревшая зависимость, внести изменения и затем перекомпилировать программу.

Как спроектировать наше приложение таким образом, чтобы можно было без лишних проблем заменить одну зависимость на другую? Здесь нам на помощь приходят абстракции. Перепишем наше приложение следующим образом:

    interface IMessageWriter
    {
        public void SendMessage(string text);
    }


    public class MessageWriter: IMessageWriter
    {
        public void SendMessage(string text)
        {
            Console.WriteLine(text);
        }
    }

    public class Calculator
    {
        private readonly IMessageWriter writer;

        public Calculator(IMessageWriter writer)
        {
            if (writer == null)
                throw new ArgumentNullException(nameof(writer));
            this.writer = writer;
        }

        public void Sum(int a, int b)
        {
            writer.SendMessage($"Сумма двух чисел равна: {a + b}");
        }

        public void Multiply(int a, int b)
        {
            writer.SendMessage($"Произведение двух чисел равно: {a * b}");
        }
    }
}

    internal class Program
    {
        static void Main(string[] args)
        {
           IMessageWriter writer = new MessageWriter();
           Calculator calculator = new Calculator(writer);
           calculator.Sum(10, 20);
           calculator.Sum(1, 2);
        }
    }

Во-первых, у нас появилась абстракция в виде интерфейса IMessageWriter в котором определен метод SendMessage() для отправки сообщения. Этот интерфейс реализуется нашим классом MessageWriter.

Во-вторых, мы добавили в класс Calculator конструктор, в котором передаем какую-либо реализацию IMessageWriter (то, что у нас пока только одна реализация — сути не меняет).  Сам класс калькулятора теперь вообще ничего не знает про то, что скрывается за интерфейсом, но зато может вызывать его методы.

В-третьих, внутри метода Main() мы создаем объект типа MessageWriter и передаем его в Calculator. В сути работы нашей программы ничего не изменилось — работать она будет точно также как и раньше, но теперь связь компонентов внутри приложения выглядит следующим образом:

Создадим ещё один класс, чтобы продемонстрировать возможности нашего приложения:

public class ColorMessageWriter : IMessageWriter
{
    public void SendMessage(string text)
    {
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine(text);
        Console.ForegroundColor = ConsoleColor.White;
    }
}

В методе Main() нам достаточно заменить всего одну строку кода:

static void Main(string[] args)
{
    //IMessageWriter writer = new MessageWriter(); было
    IMessageWriter writer = new ColorMessageWriter();//стало
    Calculator calculator = new Calculator(writer);
    calculator.Sum(10, 20);
    calculator.Sum(1, 2);
}

и получить новую функциональность нашего приложения:

Сумма двух чисел равна: 30
Сумма двух чисел равна: 3

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

Позднее связывание

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

namespace Example1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            //определяем объект какого типа необходимо создать
            Type type;
            string typeName;
            if (args.Length == 0) //аргументы командной строки не задана - используем тип по умолчанию
                typeName = "Example1.MessageWriter";
            else
                typeName = args[0];

            //пытаемся получить тип данных, объект которого необходимо создать
            type = Type.GetType(typeName, throwOnError: true);
            // создаем объект
            IMessageWriter writer = (IMessageWriter)Activator.CreateInstance(type);
            //передаем зависимость
            Calculator calculator = new Calculator(writer);
            calculator.Sum(10, 20);
            calculator.Sum(1, 2);
        }
    }
}

Здесь мы, используя аргументы командной строки, пытаемся получить имя класса, объект которого будет передаваться в Calculator.  Если имя класса получено и такой тип данных существует, то создается объект, реализующий IMessageWriter и этот объект будет использован в нашем калькуляторе для вывода сообщений.  Ключевым здесь является класс Activator. Именно с помощью его статического метода CreateInstance() и создается новый объект.

Чтобы воспользоваться этим примером, необходимо перейти в Visual Studio в главное меню «Проект — Свойства — Отладка» и выбрать пункт «Открыть пользовательский интерфейс профилей запуска отладки»:

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

В моем случае, класс ColorMessageWriter расположен в пространстве имен Example1. Теперь можно запустить приложение и убедиться, что для вывода сообщений в консоль используется именно этот класс. Аналогичным образом можно указать другой класс непосредственно в командной строке при запуске приложения не из Visual Studio, например

Во всех трех случаях, показанных на рисунке выше, нам не пришлось перекомпилировать приложение — необходимые объекты создавались в процессе выполнения приложения, исходя из наших настроек.

Итого

Dependency Injection (DI, внедрение зависимостей) — это набор принципов и паттернов проектирования программных продуктов, позволяющий разрабатывать слабосвязанный код. В этой части мы рассмотрели простейший пример DI без каких-либо дополнительных библиотек. Этот пример демонстрирует пример написания слабосвязанного кода при котором мы можем менять одну часть приложения на другую без вмешательства в остальные части системы.

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии