Иконка подарка

Весенняя распродажа! Скидка 15% по промокоду

до 01.04.2026

Инверсия зависимостей - подробное руководство для разработчиков

27 марта 2026
Автор

Олег Марков

Введение

Инверсия зависимостей (Dependency Inversion Principle, DIP) — это один из принципов SOLID, который помогает строить гибкую архитектуру приложения. Когда вы начинаете разделять код на модули и слои (UI, бизнес-логика, доступ к данным и т.д.), довольно быстро сталкиваетесь с проблемой тесной связности. Один модуль напрямую создает объекты другого, знает все детали их реализации и сильно от них зависит.

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

Что такое инверсия зависимостей

Формулировка принципа DIP

Принцип инверсии зависимостей обычно формулируют так:

  1. Модули верхнего уровня не должны зависеть от модулей нижнего уровня. И те, и другие должны зависеть от абстракций.
  2. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Давайте разберем это более приземленно.

  • Модули верхнего уровня — это ваш бизнес-код, сценарии использования (use cases), доменная логика.
  • Модули нижнего уровня — это базы данных, файлы, сетевые клиенты, фреймворки, конкретные UI-библиотеки.
  • Абстракции — интерфейсы, абстрактные классы, протоколы, контракты.
  • Детали — конкретные реализации этих интерфейсов.

Когда вы следуете DIP, ваш бизнес-код начинает зависеть не от конкретной базы данных или HTTP-клиента, а от интерфейса “хранилище пользователей” или “клиент для отправки уведомлений”. Конкретные классы подключаются уже снаружи, чаще всего на уровне настройки приложения (composition root).

Зачем нужна инверсия зависимостей

Инверсия зависимостей решает сразу несколько задач:

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

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

Пример без инверсии зависимостей

Сначала рассмотрим типичный пример нарушения DIP. Пусть у нас есть сервис, который отправляет уведомления пользователю по email.

Код без DIP (жесткая зависимость от реализации)

Пример на языке, похожем на Java:

// Конкретный низкоуровневый класс для отправки email
public class SmtpEmailSender {
    public void send(String to, String subject, String body) {
        // Здесь логика отправки письма через SMTP
        // Подключение к серверу, авторизация и т.д.
    }
}

// Высокоуровневый класс, который использует отправку email
public class RegistrationService {
    private final SmtpEmailSender emailSender;

    public RegistrationService() {
        // Здесь создается конкретная реализация
        this.emailSender = new SmtpEmailSender();
    }

    public void register(String email) {
        // Логика регистрации пользователя
        // ...

        // Отправляем письмо о регистрации
        emailSender.send(email, "Добро пожаловать", "Вы успешно зарегистрированы");
    }
}

Обратите внимание, что здесь происходит:

  • RegistrationService сам создает объект SmtpEmailSender.
  • Регистрация “знает” о конкретном способе отправки писем (SMTP).
  • Чтобы заменить SMTP на, скажем, внешний сервис, нужно менять код RegistrationService.

Этот код сложно тестировать. В юнит-тесте вы не можете просто подменить отправку email на фейковую реализацию, потому что RegistrationService жестко создает SmtpEmailSender внутри конструктора.

Пример с инверсией зависимостей

Теперь давайте инвертируем зависимость. Для этого введем абстракцию — интерфейс EmailSender.

// Абстракция - интерфейс для отправки email
public interface EmailSender {
    void send(String to, String subject, String body);
}

// Одна конкретная реализация - отправка через SMTP
public class SmtpEmailSender implements EmailSender {
    @Override
    public void send(String to, String subject, String body) {
        // Логика отправки через SMTP
    }
}

// Другая реализация - "фейковая" для тестов
public class FakeEmailSender implements EmailSender {
    @Override
    public void send(String to, String subject, String body) {
        // В тестах мы просто запоминаем вызовы
        // или логируем их в память
    }
}

// Высокоуровневый класс теперь зависит от абстракции, а не от реализации
public class RegistrationService {
    private final EmailSender emailSender;

    // Смотрите - зависимость передается "снаружи"
    public RegistrationService(EmailSender emailSender) {
        this.emailSender = emailSender;
    }

    public void register(String email) {
        // Логика регистрации пользователя
        // ...

        // Отправляем письмо через абстракцию
        emailSender.send(email, "Добро пожаловать", "Вы успешно зарегистрированы");
    }
}

Что изменилось:

  • RegistrationService больше не создает SmtpEmailSender.
  • RegistrationService зависит только от интерфейса EmailSender.
  • Конкретная реализация выбирается при создании объекта RegistrationService (обычно в месте настройки приложения).

Теперь вы увидите, как это выглядит при подключении конкретной реализации:

public class Application {
    public static void main(String[] args) {
        // В "боевом" коде мы создаем настоящую реализацию
        EmailSender realSender = new SmtpEmailSender();

        // И передаем ее в RegistrationService
        RegistrationService registrationService = new RegistrationService(realSender);

        registrationService.register("user@example.com");
    }
}

А в тесте можно сделать так:

public class RegistrationServiceTest {

    @Test
    public void testRegisterSendsEmail() {
        // В тесте используем фейковую реализацию
        FakeEmailSender fakeSender = new FakeEmailSender();

        // Передаем ее в тестируемый класс
        RegistrationService service = new RegistrationService(fakeSender);

        service.register("test@example.com");

        // Здесь мы проверяем, что fakeSender был вызван нужным образом
        // Например, assertEquals(1, fakeSender.getCallsCount());
    }
}

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

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

Инверсию зависимостей часто путают с внедрением зависимостей (Dependency Injection, DI). Давайте разберемся, чем они отличаются и как связаны.

DIP — это принцип, DI — это механизм

  • DIP — архитектурный принцип, который говорит “зависеть от абстракций, а не от реализаций”.
  • Dependency Injection — это способ передать в объект его зависимости извне. Это реализация идеи DIP на уровне кода.

Практически всегда, когда вы применяете DIP, вы используете один из видов DI: конструктор, сеттер или передачу в метод.

Виды внедрения зависимостей

Давайте посмотрим на основные способы внедрения зависимостей и то, как они сочетаются с DIP.

Внедрение через конструктор

Наиболее распространенный и рекомендуемый вариант.

public interface INotificationSender
{
    void Send(string to, string message);
}

public class SmsNotificationSender : INotificationSender
{
    public void Send(string to, string message)
    {
        // Отправка SMS
    }
}

public class OrderService
{
    private readonly INotificationSender _notificationSender;

    // Зависимость передается в конструктор
    public OrderService(INotificationSender notificationSender)
    {
        _notificationSender = notificationSender;
    }

    public void CreateOrder(string customerPhone)
    {
        // Логика создания заказа
        // ...

        // Отправляем уведомление через абстракцию
        _notificationSender.Send(customerPhone, "Ваш заказ создан");
    }
}

Комментарии:

// Здесь мы определяем абстракцию для отправки уведомлений
// В этом классе мы реализуем отправку уведомлений через SMS
// Здесь мы принимаем зависимость INotificationSender извне через конструктор
// В методе CreateOrder мы используем абстракцию, не зная о конкретной реализации

Этот способ хорошо работает с принципом “объект всегда готов к работе после создания”.

Внедрение через сеттер

Иногда используется, если зависимость опциональна или может меняться в процессе работы.

public class ReportService
{
    public INotificationSender NotificationSender { get; set; }

    public void GenerateReport()
    {
        // Генерация отчета
        // ...

        // Проверяем, задана ли зависимость
        if (NotificationSender != null)
        {
            // Отправляем уведомление, если есть чем
            NotificationSender.Send("admin@example.com", "Отчет готов");
        }
    }
}

Комментарии:

// Здесь мы объявляем свойство, в которое извне можно передать реализацию
// В методе мы проверяем, установлена ли зависимость, и только тогда используем ее

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

Внедрение через метод

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

public interface PaymentGateway {
    void pay(double amount);
}

public class OrderProcessor {

    public void processOrder(double amount, PaymentGateway gateway) {
        // Логика обработки заказа
        // ...

        // Оплата через переданный шлюз
        gateway.pay(amount);
    }
}

Комментарии:

// Здесь мы определяем абстракцию для платежного шлюза
// В методе processOrder мы получаем зависимость как параметр
// Это удобно, если нужно выполнять один и тот же код с разными реализациями

Этот способ типичен для функционального и процедурного стиля.

Как формулировать правильные абстракции

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

Не абстрагируйте детали раньше времени

Частая ошибка — создавать интерфейсы “на всякий случай”, когда у вас есть только одна реализация, и нет понятного сценария второй. Тогда абстракции получаются неестественными, раздутыми и неудобными.

Лучший подход:

  • Сначала реализовать простое решение.
  • Понять, где модуль действительно зависит от конкретной технологии (БД, HTTP, файловая система).
  • Выделить абстракцию вокруг этого взаимодействия.

Абстракции ориентируйте на домен, а не на технологию

Абстракции должны описывать смысл в терминах вашей предметной области, а не деталей инфраструктуры.

Плохой вариант:

public interface IDbClient
{
    // Методы, завязанные на SQL или конкретный драйвер
    void ExecuteQuery(string sql);
}

Лучший вариант:

public interface IUserRepository
{
    // Методы, описывающие операции с пользователями
    User GetById(Guid id);
    void Save(User user);
}

Комментарии:

// В плохом примере интерфейс описывает детали доступа к БД
// В хорошем примере интерфейс описывает операции над сущностью User
// Так код верхнего уровня не знает о SQL, таблицах и т.п.

Как видите, при таком подходе слой бизнес-логики даже не “подозревает”, какая именно технология лежит под UserRepository.

Абстракции должны быть стабильнее реализаций

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

  • формировать интерфейсы вокруг устойчивых бизнес-операций;
  • избегать “протаскивания” в абстракции временных технических деталей;
  • группировать методы в интерфейсе по смыслу, а не потому что “так проще реализовать”.

Инверсия зависимостей и слои приложения

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

Типичная слоеная архитектура без DIP

Схематично:

  • UI (контроллеры, экраны)
  • Бизнес-логика (сервисы, доменные модели)
  • Доступ к данным (репозитории, ORM)

Без инверсии зависимости часто идут сверху вниз:

  • UI зависит от бизнес-логики.
  • Бизнес-логика зависит от слоя данных.
  • Слой данных зависит от конкретной БД.

Выглядит логично, но есть проблема: бизнес-логика “знает”, что данные хранятся, например, в SQL-базе. Заменить БД или вынести бизнес-логику в другой процесс становится сложнее.

Архитектура с DIP

С инверсией зависимостей мы переворачиваем часть ссылок:

  • UI зависит от бизнес-логики (как и раньше).
  • Бизнес-логика зависит от абстракций хранилища (интерфейсы репозиториев).
  • Конкретный слой данных реализует эти интерфейсы.
  • Конкретные реализации передаются в бизнес-логику “снаружи”.

Теперь зависимости с точки зрения кода могут выглядеть так:

  • Модуль “домена” (бизнес-логика) содержит интерфейсы репозиториев.
  • Модуль “данные” содержит реализации этих интерфейсов.
  • Модуль “приложение” склеивает все вместе и создает реальные объекты.

Таким образом, доменный код вообще не ссылается на инфраструктуру.

Давайте разберемся на упрощенном примере.

// Слой домена (бизнес-логика)

public class User
{
    // Доменная сущность
    public Guid Id { get; set; }
    public string Email { get; set; }
}

// Абстракция хранилища пользователей
public interface IUserRepository
{
    User GetById(Guid id);
    void Save(User user);
}

// Сервис домена, использующий абстракцию
public class UserService
{
    private readonly IUserRepository _userRepository;

    // Зависимость передается через конструктор
    public UserService(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public void ChangeEmail(Guid userId, string newEmail)
    {
        // Вся логика здесь работает только с абстракцией
        var user = _userRepository.GetById(userId);
        user.Email = newEmail;
        _userRepository.Save(user);
    }
}
// Слой данных (конкретная реализация)

public class SqlUserRepository : IUserRepository
{
    // Здесь хранятся детали работы с БД
    public User GetById(Guid id)
    {
        // SQL-запрос, маппинг результатов и т.д.
        return new User { Id = id, Email = "from-db@example.com" };
    }

    public void Save(User user)
    {
        // SQL-запрос на сохранение
    }
}
// Слой приложения (composition root)

public class ApplicationStartup
{
    public void Configure()
    {
        // Здесь мы создаем конкретную реализацию
        IUserRepository repo = new SqlUserRepository();

        // И передаем ее в доменный сервис
        UserService service = new UserService(repo);

        // Далее сервис используется в контроллерах, UI и т.п.
    }
}

Комментарии:

// В доменном слое мы определяем контракт IUserRepository
// UserService знает только об интерфейсе IUserRepository
// В SqlUserRepository реализованы все детали работы с SQL
// В ApplicationStartup мы "связываем" домен и инфраструктуру

Так вы добиваетесь того, что бизнес-логика не зависит от технических деталей.

Инверсия зависимостей в разных языках

Чтобы вам было проще применять DIP, давайте посмотрим, как это может выглядеть в разных языках.

В языках с интерфейсами (C sharp, Java, Go)

В таких языках инверсия зависимостей опирается на интерфейсы как на основной механизм абстракции. Мы уже видели примеры на C sharp и Java. Давайте быстро пробежимся по Go.

// Абстракция - интерфейс
type Logger interface {
    Info(message string)
    Error(message string)
}

// Конкретная реализация - лог в stdout
type StdoutLogger struct{}

// Реализация методов интерфейса
func (l StdoutLogger) Info(message string) {
    // Здесь мы выводим информационное сообщение в консоль
    fmt.Println("INFO", message)
}

func (l StdoutLogger) Error(message string) {
    // Здесь мы выводим сообщение об ошибке в консоль
    fmt.Println("ERROR", message)
}

// Высокоуровневая структура, зависящая от интерфейса
type UserService struct {
    logger Logger
}

// Конструктор, принимающий интерфейс
func NewUserService(logger Logger) *UserService {
    // Здесь мы создаем UserService и сохраняем зависимость
    return &UserService{logger: logger}
}

func (s *UserService) CreateUser(email string) {
    // Здесь выполняется логика создания пользователя
    s.logger.Info("Создаем пользователя с email " + email)
    // ...
}

Комментарии:

// Интерфейс Logger задает контракт логирования
// StdoutLogger - одна из возможных реализаций
// UserService зависит только от Logger, а не от StdoutLogger
// В NewUserService нам могут передать любую реализацию Logger

Здесь Go-интерфейсы как раз идеально подходят для реализации DIP.

В динамических языках (Python, JavaScript)

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

Пример на Python:

class EmailSender:
    # Базовый класс-абстракция (можно и через протоколы, и через duck typing)
    def send(self, to, subject, body):
        # Здесь мы определяем "контракт" - метод send должен существовать
        raise NotImplementedError


class SmtpEmailSender(EmailSender):
    def send(self, to, subject, body):
        # Реализация отправки письма через SMTP
        pass


class RegistrationService:
    def __init__(self, email_sender):
        # Здесь мы принимаем любой объект, у которого есть метод send
        self._email_sender = email_sender

    def register(self, email):
        # Здесь выполняется логика регистрации пользователя
        self._email_sender.send(email, "Добро пожаловать", "Вы зарегистрированы")

Комментарии:

// EmailSender задает базовый интерфейс через NotImplementedError
// RegistrationService зависит только от наличия метода send у email_sender
// В юнит-тесте можно передать фейковый объект с методом send

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

Инверсия зависимостей и тестирование

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

Подмена зависимостей в тестах

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

С применением DIP вы выносите взаимодействия с внешним миром в абстракции и в тесте подменяете их фейковыми реализациями.

Пример на C sharp:

public interface ITimeProvider
{
    DateTime Now { get; }
}

public class SystemTimeProvider : ITimeProvider
{
    public DateTime Now => DateTime.Now;
}

public class DiscountService
{
    private readonly ITimeProvider _timeProvider;

    public DiscountService(ITimeProvider timeProvider)
    {
        _timeProvider = timeProvider;
    }

    public decimal GetDiscount()
    {
        // Логика зависит от текущего времени
        var now = _timeProvider.Now;

        // Если сейчас утро, возвращаем одну скидку, иначе другую
        if (now.Hour < 12)
            return 0.1m;

        return 0.05m;
    }
}

В тесте:

public class FakeTimeProvider : ITimeProvider
{
    // В тесте мы можем задавать случай которого хотим
    public DateTime Now { get; set; }
}

[Test]
public void GetDiscount_ReturnsHigherDiscountInMorning()
{
    // Здесь мы создаем фейковый источник времени
    var fakeTime = new FakeTimeProvider
    {
        Now = new DateTime(2024, 1, 1, 9, 0, 0) // 9 утра
    };

    // Передаем его в сервис
    var service = new DiscountService(fakeTime);

    var discount = service.GetDiscount();

    Assert.AreEqual(0.1m, discount);
}

Комментарии:

// ITimeProvider инкапсулирует получение текущего времени
// DiscountService зависит от абстракции времени, а не от DateTime.Now напрямую
// В тесте мы управляем временем, подставляя нужное значение через FakeTimeProvider

Здесь DIP делает тест предсказуемым и независимым от реального времени.

Тестирование без DI-контейнеров

Важно понимать, что для использования инверсии зависимостей вам не обязательно использовать DI-контейнеры и фреймворки. Для тестирования достаточно:

  • передавать зависимости через конструктор;
  • создавать фейковые реализации интерфейсов;
  • вручную создавать объекты в тестах.

DI-контейнеры упрощают конфигурацию в больших приложениях, но DIP как принцип от них не зависит.

Типичные ошибки при применении инверсии зависимостей

Теперь давайте посмотрим на проблемы, с которыми часто сталкиваются разработчики, когда начинают применять DIP.

Ошибка 1. Интерфейсы “для всего подряд”

Иногда разработчики создают интерфейс для каждого класса без реальной необходимости. Это ведет к:

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

Когда создавать интерфейс оправдано:

  • есть понятная альтернатива реализации (например, SQL и in-memory);
  • вам нужно легко подменять реализацию в тестах;
  • вы хотите изолировать модуль от внешней технологии (библиотеки, БД, API).

Ошибка 2. Абстракции зависят от деталей

Иногда интерфейсы “протекают” деталями реализации. Например, репозиторий начинает принимать параметром класс из инфраструктурной библиотеки, а не доменную модель. Тогда вы вроде бы используете интерфейсы, но верхний слой по-прежнему привязан к деталям.

Лучше:

  • передавать в абстракции доменные объекты;
  • скрывать за интерфейсами любые специфичные типы библиотек.

Ошибка 3. Business-код все равно создает реализации

Еще одна типичная проблема — вы определяете интерфейсы, но внутри сервиса по-прежнему создаете конкретные классы, используя new. Тогда инверсия зависимостей не работает, потому что высокоуровневый модуль остается жестко привязан к реализации.

Решение:

  • создавать зависимости снаружи и передавать в конструктор;
  • не создавать new внутри бизнес-кода для внешних сервисов и хранилищ.

Ошибка 4. DIP только ради “модности”

Бывает, что принцип применяют там, где нет реальной проблемы:

  • маленькие скрипты;
  • одноразовые утилиты;
  • очень узкие локальные функции.

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

Практические рекомендации по внедрению DIP в проект

Теперь давайте соберем пошаговый подход, который вы можете применить у себя в проекте.

Шаг 1. Найдите места сильной связности

Посмотрите, где ваш код:

  • напрямую создает объекты внешних сервисов new SmtpClient, new HttpClient, new SqlConnection;
  • зависит от конкретных библиотек или фреймворков;
  • трудно тестируется, потому что внутри вызываются внешние ресурсы.

Именно здесь особенно полезно применить DIP.

Шаг 2. Выделите абстракции

Для каждого проблемного места:

  • сформулируйте, что должен делать внешний сервис с точки зрения бизнес-логики;
  • оформите это как интерфейс или контракт;
  • проследите, чтобы в интерфейсе не было деталей реализации.

Например, вместо “клиент HTTP” сделайте “клиент уведомлений”, вместо “обертки над SQL” — “репозиторий заказов”.

Шаг 3. Зависите от абстракций

Измените код верхнего уровня так, чтобы он:

  • получал зависимости через конструктор, параметры методов или сеттеры;
  • оперировал только интерфейсами, а не конкретными классами;
  • не вызывал new для внешних сервисов внутри бизнес-методов.

Шаг 4. Реализуйте конкретные классы отдельно

Создайте слой, где будут:

  • реализации интерфейсов для работы с БД, файлами, сетью;
  • адаптеры под конкретные библиотеки или фреймворки.

Важно, чтобы этот слой зависел от доменных интерфейсов, а не наоборот.

Шаг 5. Настройте “composition root”

В одном месте приложения (часто в точке входа) соберите все зависимости:

  • создайте конкретные реализации внешних сервисов;
  • создайте доменные сервисы, передав в них зависимости;
  • свяжите это с веб-фреймворком, UI и т.п.

Если вы используете DI-контейнер, он поможет автоматизировать эту сборку, но основной принцип остается тем же.

Шаг 6. Покройте код тестами

Используя инверсию зависимостей:

  • подменяйте внешние сервисы фейковыми реализациями;
  • тестируйте бизнес-логику изолированно от инфраструктуры;
  • проверяйте поведение, а не конкретные вызовы внешних библиотек.

Так ваш код станет более надежным и предсказуемым.

Заключение

Инверсия зависимостей — это принцип, который помогает вам перестроить архитектуру так, чтобы бизнес-логика не была заложником конкретных технологий. Вместо жесткой привязки “сверху вниз” вы делаете так, что и верхний, и нижний уровни зависят от общих абстракций.

Ключевые идеи, которые важно удерживать:

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

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

Частозадаваемые технические вопросы и ответы

Как поступать с зависимостями, которые создаются очень часто и “маленькие” (например, DTO, простые объекты)?

Для простых объектов, которые не зависят от внешних ресурсов и легко создаются, нет необходимости применять DIP. Их можно создавать напрямую через new внутри методов. Инверсия зависимостей нужна в основном для “тяжелых” зависимостей — тех, что ходят в сеть, БД, файловую систему или сильно завязаны на внешние библиотеки.

Нужно ли выносить в интерфейсы все классы, если я не планирую заменять реализацию?

Если реалистичного сценария замены реализации нет, и класс не мешает тестированию, можно не создавать интерфейс. Сфокусируйтесь на тех участках, где изоляция от реализации реально дает выигрыш — например, внешние сервисы, хранилища, системные ресурсы.

Как быть с логированием - делать ли интерфейс для логгера или использовать глобальный логгер?

Если проект небольшой, можно использовать общий логгер напрямую. В крупных системах удобнее сделать абстракцию Logger или воспользоваться уже существующим интерфейсом (например, ILogger в .NET). Это позволит подменять логгер в тестах и не привязываться к конкретной logging-библиотеке.

В каком модуле хранить интерфейсы - в доменном или в инфраструктурном?

Интерфейсы, от которых зависит домен (например, репозитории, сервисы отправки уведомлений), обычно размещают в доменном модуле. Так домен не будет зависеть от инфраструктуры. Конкретные реализации этих интерфейсов живут в отдельных инфраструктурных модулях.

Как применять инверсию зависимостей в микросервисах где есть границы по сервисам?

В микросервисах DIP работает внутри каждого сервиса. Внешние API других сервисов оборачиваются в интерфейсы (например, ICustomerClient), доменный слой зависит от этих интерфейсов, а конкретные HTTP-клиенты реализуют их. Это упрощает тестирование бизнес-логики без реальных сетевых вызовов и позволяет менять способы взаимодействия между сервисами (REST, gRPC и т.д.) без переписывания домена.

Стрелочка влевоПуть к файлу в архитектуре Feature Sliced Design - fsd-pathЦиклические зависимости circular-dependencies - причины проблемы и решенияСтрелочка вправо

Все гайды по Feature-sliced_design

Открыть базу знаний

Отправить комментарий