Основы 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Этот паттерн следует принципу зависимости от абстракций (интерфейсов), а не от конкретных реализаций. Это позволяет заменять реализации без изменения класса, который их использует, что крайне важно для модульного тестирования и поддержки гибких архитектур.
Задание
ЛегкоДавайте создадим систему логирования, которая демонстрирует мощь внедрения зависимостей (dependency injection). Вместо жесткого кодирования способа записи логов, вы создадите гибкий класс Logger, который может работать с любым местом назначения логов — консолью, симуляцией файла или чем-то еще — не зная специфики реализации.
Вы организуете свой код в трех файлах:
ILogWriter.cs: Определите интерфейс с именемILogWriterв пространстве именLogging. Этот интерфейс представляет собой контракт для любого места назначения логов и должен иметь один методWrite(string message), который возвращает строку, описывающую, куда было записано сообщение.LogWriters.cs: Создайте две конкретные реализацииILogWriterв пространстве именLogging:ConsoleLogWriter— его методWriteвозвращает"Console: " + messageFileLogWriter— принимает имя файла через конструктор, а его метод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 placedDI следует принципу зависимости от абстракций (интерфейсов), а не от конкретных реализаций, что позволяет заменять реализации без изменения класса, который их использует.
Попробуйте сами
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) и залогируйте то же самое сообщение
// Выведите результат
}
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыПространства имен и директивыВведение в классы и объектыКлючевое слово 'this'Методы и параметрыПоля и свойстваКонструкторыИнициализаторы объектовИтоги — Простой калькулятор4Наследование
Основы синтаксиса наследования (:)Ключевое слово 'base'Ключевые слова Virtual и OverrideЗапечатанные классыБазовый класс 'object'Итоги — Иерархия сотрудников7Продвинутые возможности
Перегрузка операторовИндексаторы (this[])Переопределение ToString()Методы расширенияПовторение — Пользовательский список10Паттерны проектирования. Часть 1
Введение в паттерны проектированияПотокобезопасный SingletonПаттерн FactoryПаттерн Observer (События)Паттерн Strategy13Финальные испытания
Платформа для онлайн-обученияКомпонент игрового персонажаСервис аренды транспорта2Свойства и статические члены
Автоматически реализуемые свойстваСвойства только для чтения и записиСтатические поля и методыСтатические классыЧлены в виде выражений5Полиморфизм и интерфейсы
Полиморфизм: компиляция vs выполнениеИнтерфейс vs абстрактный классМножественные интерфейсыЯвные интерфейсыUpcasting и DowncastingИтоги: Калькулятор фигур8Продвинутые концепции ООП
Композиция вместо наследованияGenerics (классы и методы)Делегаты и событияАтрибуты и рефлексияIDisposable и оператор usingОсновы Dependency Injection11Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн Компоновщик3Архитектура классов
Экземплярные и статические данныеКлючевые слова 'readonly' и 'const'Опорные поляПовторение — Менеджер банковских счетов6Инкапсуляция
Модификаторы доступаСвойства для инкапсуляцииРеализация скрытия данныхПаттерны неизменяемостиИтоги — Записи студентов9Переменное количество аргументов
Ключевое слово 'params'Необязательные параметрыИменованные аргументыПерегрузка методов