Как используешь букву D из SOLID в своих приложениях?

Ответ

Принцип инверсии зависимостей (Dependency Inversion Principle - DIP) применяется для создания гибких и тестируемых систем. Его суть: модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Практическое применение:

  1. Зависимость от интерфейсов, а не от классов:

    // Вместо этого (зависимость от детали):
    public class OrderService
    {
        private readonly SqlOrderRepository _repository; // Прямая зависимость
        public OrderService() => _repository = new SqlOrderRepository();
    }
    
    // Делаем так (зависимость от абстракции):
    public class OrderService
    {
        private readonly IOrderRepository _repository; // Зависимость от интерфейса
        public OrderService(IOrderRepository repository) => _repository = repository;
    }
  2. Внедрение зависимостей (DI): Конкретная реализация (SqlOrderRepository, FileOrderRepository) предоставляется извне (через конструктор, свойство или метод), обычно с помощью DI-контейнера.

    // Регистрация в контейнере (например, в Program.cs)
    builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
    // Или для тестов:
    builder.Services.AddScoped<IOrderRepository, MockOrderRepository>();
  3. Выгоды:

    • Тестируемость: Легко подменить реальный репозиторий или сервис mock-объектом в юнит-тестах.
    • Гибкость: Замена реализации (например, с SQL на NoSQL) требует изменения только в одном месте — конфигурации DI.
    • Слабая связанность: Классы знают только о контрактах (интерфейсах), а не о конкретных деталях реализации.

Пример из реального проекта: Сервис отправки уведомлений зависит от интерфейса INotificationSender. В зависимости от конфигурации, через DI внедряется EmailNotificationSender, SmsNotificationSender или PushNotificationSender. Это позволяет легко добавлять новые способы отправки, не меняя код основного сервиса.

Ответ 18+ 🔞

Давай разберём этот ваш принцип инверсии зависимостей, а то звучит как заклинание из высшей магии, а на деле всё проще пареной репы, если без соплей.

Вот представь: у тебя есть класс OrderService, который заказывает пиццу. И он такой: «Я буду работать только с пиццерией «У Дяди Васи» на углу, и точка!». И внутри него жёстко прибито гвоздями создание объекта этой пиццерии. Что получается? Захотел ты протестировать логику заказа — тебе реальную пиццу на дом тащить? Захотел сменить пиццерию — переписывай весь класс. Полный пиздец, короче.

Так вот, принцип инверсии зависимостей (DIP) тебе говорит: «Э, дружок-пирожок, давай-ка ты не будешь зависеть от конкретной пиццерии «У Дяди Васи». Ты будешь зависеть от абстрактной «Службы доставки еды». А уж кто там будет по ту сторону контракта — Дядя Вася, пафосный итальянский ресторан или вообще тётя Зина с пирожками — тебе похуй».

Как это выглядит в коде, без этих твоих заумных слов?

Вместо этого убожества (жёсткая привязка к деталям):

public class OrderService
{
    private readonly UncleVasyaPizzaService _pizzaService; // Привязан намертво!
    public OrderService()
    {
        _pizzaService = new UncleVasyaPizzaService(); // Создаём сами. Ошибка!
    }
    public void OrderPizza() => _pizzaService.DeliverPizza();
}

Делаем по-человечески (зависим от абстракции):

public class OrderService
{
    private readonly IFoodDeliveryService _deliveryService; // Ага, интерфейс! Контракт!
    // Кто будет доставлять — принесут извне и всунут в конструктор.
    public OrderService(IFoodDeliveryService deliveryService)
    {
        _deliveryService = deliveryService; // Принимаем кого угодно, кто подписал контракт.
    }
    public void OrderFood() => _deliveryService.DeliverOrder();
}

И как это, блядь, использовать-то?

А вот так. Это и есть Внедрение зависимостей (DI). Ты в основном коде говоришь: «Эй, контейнер! Когда кто-то просит IFoodDeliveryService, давай ему реальную UncleVasyaPizzaService».

builder.Services.AddScoped<IFoodDeliveryService, UncleVasyaPizzaService>();

А когда пишешь тесты, то подсовываешь какую-нибудь FakeDeliveryService, которая не ебёт мозги и не жрёт ресурсы, а просто имитирует работу.

// В тестах
var fakeService = new Mock<IFoodDeliveryService>();
var orderService = new OrderService(fakeService.Object); // Подсунули заглушку!
orderService.OrderFood();
// Проверяем, что метод DeliverOrder вызвался, и не паримся.

А нахуя это всё?

Да всё просто, как три копейки:

  1. Тестируемость — овердохуищная. Захотел протестировать логику заказа — подсунул заглушку. Никаких реальных API, баз данных и вызовов курьеров. Всё быстро, изолированно и не зависит от внешнего мира.
  2. Гибкость — пиздец. Надоел тебе Дядя Вася со своей пересоленной пиццей. Решил перейти на «Итальянскую кухню». Раньше ты бы перелопачивал весь OrderService. А теперь? Ты просто в одном месте (в конфигурации DI) меняешь реализацию:
    // Раньше было:
    // builder.Services.AddScoped<IFoodDeliveryService, UncleVasyaPizzaService>();
    // Стало:
    builder.Services.AddScoped<IFoodDeliveryService, FancyItalianRestaurantService>();

    И весь остальной код даже не чихнул. Он как работал с интерфейсом, так и работает.

  3. Слабая связанность. Твой основной сервис теперь не знает ни хуя про то, как именно готовят пиццу, на каких санях её везут и как зовут курьера. Он знает только контракт: «доставь заказ». А детали — это проблемы тех, кто этот контракт выполняет.

Реальный пример, чтобы вообще всё встало на свои места:

Пишешь сервис оповещений. Раньше он тупо слал email. Потом понадобилось слать SMS. Потом — пуши в мобильное приложение. Если бы ты был тормозом и не использовал DIP, ты бы с каждым новым требованием ковырял один и тот же жирный класс, рискуя всё сломать.

А умный ты сделал так: объявил интерфейс INotificationSender с методом SendNotification(string message). Твой главный сервис теперь зависит только от него.

А дальше — дело техники:

  • EmailNotificationSender — шлёт на почту.
  • SmsNotificationSender — шлёт смс.
  • PushNotificationSender — шлёт пуш.
  • CompositeNotificationSender — шлёт всем сразу, потому что можно.

И добавляешь ты их в проект, не трогая основной код. Просто регистрируешь нужную реализацию в DI-контейнере. Красота, да и только. В рот меня чих-пых, вот это архитектура!