суббота, 7 июня 2025 г.

DIP, Di, Dependency, Inversion, Giga

DIP, DI, Dependency, Inversion, Giga
----------------------

Dependency Injection (DI)

Dependency Injection — это метод внедрения зависимостей в класс или компонент,

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

Идея проста:

вместо того чтобы класс создавал свои зависимости сам,

он получает их извне (через конструктор, свойство или метод).

Именно поэтому DI называется "injection" — зависимость "вкалывается" извне.

Примеры:

  • Конструктор инъекции:
public class UserService
{
    private readonly IUserRepository _repository;

    public UserService(IUserRepository repository)
    {
        _repository = repository;
    }
}
Свойство инъекции:
public class UserService
{
    public IUserRepository Repository { get; set; }
}
----------------------

Dependency Inversion Principle (DIP)

Dependency Inversion Principle — один из принципов SOLID,

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

Главная идея DIP заключается в том, что модули верхнего уровня не должны зависеть от деталей нижнего уровня.

Оба уровня должны зависеть от абстракций.

Другими словами, высокая зависимость должна строиться вокруг интерфейсов, а не конкретных классов.

Пример:

// До DIP
public class EmailSender : ISender
{
    public void Send(string message)
    {
        Console.WriteLine("Sending email...");
    }
}

// После DIP
public interface ISender
{
    void Send(string message);
}

public class EmailSender : ISender
{
    public void Send(string message)
    {
        Console.WriteLine("Sending email...");
    }
}

public class NotificationService
{
    private readonly ISender _sender;
    
    public NotificationService(ISender sender)
    {
        _sender = sender;
    }

    public void Notify(string message)
    {
        _sender.Send(message);
    }
}
----------------
.NET Aspire активно использует концепцию DI, предоставляя готовый DI-контейнер, 
который упрощает управление зависимостями в микросервисах. 
Он автоматически создаёт экземпляры классов и разрешает зависимости между ними.

Ранее у нас была жесткая привязанность класса NotificationService к классу EmailSender.

Код выглядел примерно так:

public class NotificationService
{
    private readonly EmailSender _emailSender;

    public NotificationService()
    {
        _emailSender = new EmailSender();
    }

    public void Notify(string message)
    {
        _emailSender.Send(message); // Прямая зависимость от конкретного класса
    }
}

Проблема здесь очевидна:

если нам нужно отправить уведомление другим способом (SMS, Telegram и т.д.),

мы вынуждены изменять класс NotificationService.

--

Что произошло после применения DIP?

После вынесения метода Send в интерфейс ISender, ситуация изменилась коренным образом:

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

public class EmailSender : ISender
{
    public void Send(string message)
    {
        Console.WriteLine("Sending email...");
    }
}

public class SMSSender : ISender
{
    public void Send(string message)
    {
        Console.WriteLine("Sending SMS...");
    }
}

public class NotificationService
{
    private readonly ISender _sender;

    public NotificationService(ISender sender)
    {
        _sender = sender;
    }

    public void Notify(string message)
    {
        _sender.Send(message); // Мы обращаемся к интерфейсу, а не к конкретному классу
    }
}

Где здесь инверсия?

Инверсия заключается в том, что высокий уровень (класс NotificationService)

теперь зависит от абстрактного интерфейса (ISender),

а не от конкретной реализации (EmailSender).

До применения DIP:

  • Класс высокого уровня (NotificationService) напрямую зависел от низкого уровня (EmailSender).

После применения DIP:

  • Высокий уровень (NotificationService) зависит от интерфейса (ISender),
  • а конкретные реализации зависят от интерфейса.

Таким образом, направления зависимостей изменились:

теперь зависимости идут от высшего уровня к низшему уровню через абстракцию, а не наоборот.


Каковы практические последствия?

  1. Повысилась гибкость: Если завтра захотите поменять отправку уведомлений на SMS или Push-уведомления,
  2. достаточно написать новую реализацию интерфейса ISender, не трогая NotificationService.
  3. Легче тестировать: Можно создать mock-реализацию интерфейса для юнит-тестов,
  4. не затрагивая реальную логику отправки.
  5. Лучше масштабируется: Если понадобятся десятки способов доставки уведомлений,
  6. это не усложняет основную логику уведомления.

Итог

Принцип Dependency Inversion Principle гласит:

модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций.

В нашем примере интерфейс ISender стал такой абстракцией,

освободив NotificationService от жесткой привязки к конкретной реализации.

-----

Уровень классов в контексте Dependency Inversion Principle (DIP)

High-level module (Высокий уровень) — это модуль, выполняющий главную логику приложения,

решающий важную задачу. Обычно он определяет стратегию, управляет поведением системы и вызывает другие модули.

Low-level module (Низкий уровень) — это модули, реализующие детальную механику выполнения операции.

Это классы, которые выполняют конкретные задачи, такие как чтение-запись данных, сетевые запросы,

отправка писем и прочее.


Анализ примера

Посмотрим внимательно на наши классы:

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

public class EmailSender : ISender
{
    public void Send(string message)
    {
        Console.WriteLine("Sending email...");
    }
}

public class SMSSender : ISender
{
    public void Send(string message)
    {
        Console.WriteLine("Sending SMS...");
    }
}

public class NotificationService
{
    private readonly ISender _sender;

    public NotificationService(ISender sender)
    {
        _sender = sender;
    }

    public void Notify(string message)
    {
        _sender.Send(message);
    }
}

Определение уровней:

  • High-level module:Класс NotificationService — это высокий уровень.
  • Он определяет стратегию уведомления (метод Notify()),
  • вызывая методы других классов для выполнения детализированных операций (отправка сообщения).
  • Low-level modules:Классы EmailSender и SMSSender относятся к низким уровням.
  • Их задача — выполнение детализированных операций (конкретно, отправка письма или SMS-сообщения).

Почему так получается?

Высокий уровень (NotificationService) диктует общую политику работы приложения (посылать уведомления),

но не интересуется техническими деталями (как именно отправляется письмо или SMS).

Детали (низкий уровень) выполняются отдельными модулями (EmailSender, SMSSender),

которые знают, как выполнить операцию.

Абстракция (ISender) служит мостиком между высоким уровнем и низким уровнем,

обеспечивая необходимую изоляцию и свободу расширения.


Выводы

  1. Высокий уровень:
  2. Описывает стратегию и координирует выполнение задач.
  3. Низкий уровень:
  4. Выполняет конкретные задачи, такие как отправка почты или SMS.
  5. Инверсия:
  6. Происходит, когда оба уровня начинают зависеть от абстракции (интерфейса), а не друг от друга напрямую.

Таким образом,

в твоём примере NotificationService — это высокий уровень, а EmailSender и SMSSender — низкий уровень.

--

Твоя интерпретация интуитивно правильная и понятная, но давай чуть углубимся в тему,

чтобы раскрыть нюансы принципа Dependency Inversion Principle (DIP) и

сравнить её с привычным пониманием «правильной» зависимости.


Старая трактовка (прямая зависимость)

Исторически, программисты привыкли думать о правильной зависимости как о ситуации,

когда класс не зависит напрямую от другой реализации.

Вместо этого мы стараемся вводить слои абстракции,

передавая ответственность за создание экземпляров другому классу (фабрике, конструктору или DI-контейнеру).

Пример прямой зависимости (до применения DIP):

public class HighLevelClass
{
    private LowLevelClass _lowLevelObject;

    public HighLevelClass()
    {
        _lowLevelObject = new LowLevelClass(); // Непосредственная зависимость
    }

    public void DoWork()
    {
        _lowLevelObject.DoSomething(); // Прямой вызов
    }
}

Проблема здесь в том, что изменение реализации LowLevelClass неизбежно ведет к изменению HighLevelClass.

Если появляется потребность использовать другую реализацию (например, AnotherLowLevelClass),

приходится менять и верхний уровень.


Новая трактовка (Dependency Inversion Principle)

Принципы SOLID ввели понятие инверсии зависимостей, что меняет наше восприятие правильного дизайна.

Согласно принципу DIP,

высокие уровни (High Level Modules) не должны зависеть от низких уровней (Low Level Modules).

Вместо этого обе стороны должны зависеть от абстракций (интерфейсов или абстрактных классов).

Пример после применения DIP:

public interface ILowLevelInterface
{
    void DoSomething();
}

public class LowLevelClass : ILowLevelInterface
{
    public void DoSomething() {}
}

public class AnotherLowLevelClass : ILowLevelInterface
{
    public void DoSomething() {}
}

public class HighLevelClass
{
    private readonly ILowLevelInterface _lowLevelObject;

    public HighLevelClass(ILowLevelInterface lowLevelObject)
    {
        _lowLevelObject = lowLevelObject; // Зависимость от интерфейса
    }

    public void DoWork()
    {
        _lowLevelObject.DoSomething(); // Абстрактный вызов
    }
}

Здесь зависимость инвертировалась:

HighLevelClass больше не зависит от конкретной реализации LowLevelClass,

а использует интерфейс ILowLevelInterface.

Следовательно,

любое изменение реализации ниже уровня интерфейса не приведет к изменениям в верхнем уровне.


Сравнение старой и новой трактовки:

  • Старая трактовка: Минимизировать прямую зависимость, пытаясь разделить классы.
  • Новая трактовка (DIP): Высший уровень не должен зависеть от нижних уровней вовсе,
  • вместо этого обе стороны зависят от абстракций.

Когда применять старую трактовку?

Иногда старая трактовка полезна, когда проблема не касается сильной зависимости.

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


Итог

  • Старая трактовка («независимость от вложенных классов») полезна, но недостаточно глубока.
  • Принцип DIP идет дальше, предлагая использовать абстракции для полного отделения высоких уровней от низших.

Понимая обе точки зрения,

вы сможете выбирать подходящий подход в зависимости от требований и особенностей проекта.