Menu
Coddy logo textTech

Основы Dependency Injection

Часть раздела Объектно-ориентированное программирование путешествия по C# на Coddy — урок 46 из 70.

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

Без внедрения зависимостей (DI) класс сам создает свои зависимости, что затрудняет их изменение или тестирование:

public class OrderService
{
    private EmailSender sender = new EmailSender();  // Жесткая связь
    
    public void PlaceOrder() => sender.Send("Order placed");
}

При использовании DI зависимости передаются через конструктор:

public interface IMessageSender
{
    void Send(string message);
}

public class EmailSender : IMessageSender
{
    public void Send(string message) => Console.WriteLine("Email: " + message);
}

public class OrderService
{
    private IMessageSender sender;
    
    public OrderService(IMessageSender sender)
    {
        this.sender = sender;
    }
    
    public void PlaceOrder() => sender.Send("Order placed");
}

Теперь OrderService не знает и не заботится о том, использует ли он email, SMS или фиктивный отправитель для тестирования. Зависимость «внедряется» при создании объекта:

IMessageSender emailSender = new EmailSender();
OrderService service = new OrderService(emailSender);
service.PlaceOrder();  // Email: Order placed

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

challenge icon

Задание

Легко

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

Вы организуете свой код в трех файлах:

  • ILogWriter.cs: Определите интерфейс с именем ILogWriter в пространстве имен Logging. Этот интерфейс представляет собой контракт для любого места назначения логов и должен иметь один метод Write(string message), который возвращает строку, описывающую, куда было записано сообщение.
  • LogWriters.cs: Создайте две конкретные реализации ILogWriter в пространстве имен Logging:
    • ConsoleLogWriter — его метод Write возвращает "Console: " + message
    • FileLogWriter — принимает имя файла через конструктор, а его метод Write возвращает "File[" + filename + "]: " + message
  • Logger.cs: Создайте класс Logger в пространстве имен Logging, который получает свою зависимость через внедрение в конструктор. Класс Logger должен:
    • Принимать ILogWriter через конструктор и сохранять его в приватном поле
    • Иметь метод Log(string message), который делегирует выполнение внедренному объекту записи (writer) и возвращает результат
  • Program.cs: Продемонстрируйте внедрение зависимостей, создав два разных логгера — один с ConsoleLogWriter, а другой с FileLogWriter. Покажите, как один и тот же класс Logger ведет себя по-разному в зависимости от того, какая зависимость внедрена.

Вы получите два входных значения:

  • Имя файла для файлового логгера
  • Сообщение для логирования

Создайте Logger с ConsoleLogWriter и залогируйте сообщение. Затем создайте другой Logger с FileLogWriter (используя имя файла из входных данных) и залогируйте то же самое сообщение. Выведите каждый результат на отдельной строке.

Например, если входные данные — app.log и System started, вывод должен быть следующим:

Console: System started
File[app.log]: System started

Обратите внимание, что класс Logger никогда не меняется — он работает с любой реализацией ILogWriter. В этом и заключается суть внедрения зависимостей: Logger зависит от абстракции (интерфейса), а конкретная реализация «внедряется» извне. Вы могли бы легко добавить DatabaseLogWriter или CloudLogWriter позже, вообще не меняя класс Logger!

Шпаргалка

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

Без DI (жесткая связь):

public class OrderService
{
    private EmailSender sender = new EmailSender();  // Жесткая связь
    
    public void PlaceOrder() => sender.Send("Order placed");
}

С DI (слабая связь):

public interface IMessageSender
{
    void Send(string message);
}

public class EmailSender : IMessageSender
{
    public void Send(string message) => Console.WriteLine("Email: " + message);
}

public class OrderService
{
    private IMessageSender sender;
    
    public OrderService(IMessageSender sender)
    {
        this.sender = sender;
    }
    
    public void PlaceOrder() => sender.Send("Order placed");
}

Внедрение зависимости:

IMessageSender emailSender = new EmailSender();
OrderService service = new OrderService(emailSender);
service.PlaceOrder();  // Email: Order placed

DI следует принципу зависимости от абстракций (интерфейсов), а не от конкретных реализаций, что позволяет заменять реализации без изменения класса, который их использует.

Попробуйте сами

using System;
using Logging;

class Program
{
    public static void Main(string[] args)
    {
        // Чтение входных данных
        string filename = Console.ReadLine();
        string message = Console.ReadLine();

        // TODO: Создайте Logger с ConsoleLogWriter и залогируйте сообщение
        // Выведите результат

        // TODO: Создайте другой Logger с FileLogWriter (используя filename) и залогируйте то же самое сообщение
        // Выведите результат
    }
}
quiz iconПроверьте себя

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

Все уроки раздела Объектно-ориентированное программирование