Когда нужно вводить абстракцию (интерфейс или абстрактный класс)?

Ответ

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

Конкретные случаи из практики:

  1. Работа с внешними зависимостями: Все взаимодействия с базами данных, API, файловыми системами или сторонними сервисами (платежи, email, смс) я абстрагирую за интерфейсом. Это позволяет легко подменять реальную реализацию моком в unit-тестах.
  2. Наличие или ожидание нескольких реализаций: Если есть алгоритм сортировки, который может выполняться по-разному (QuickSort, MergeSort), или способ отправки уведомлений (Email, SMS, Push) — это прямой сигнал к введению абстракции.
  3. Следование принципу инверсии зависимостей (DIP): Модули верхнего уровня (бизнес-логика) не должны зависеть от модулей нижнего уровня (инфраструктура). Оба должны зависеть от абстракций.

Пример на C#:

// Абстракция для службы отправки уведомлений
public interface INotificationService
{
    Task SendAsync(string recipient, string message);
}

// Конкретная реализация для Email
public class EmailNotificationService : INotificationService
{
    public async Task SendAsync(string recipient, string message)
    {
        // Логика отправки email через SMTP или SendGrid API
        await _smtpClient.SendMailAsync(...);
    }
}

// Класс бизнес-логики, зависящий от абстракции
public class OrderProcessor
{
    private readonly INotificationService _notifier;
    // Внедрение зависимости через конструктор
    public OrderProcessor(INotificationService notifier)
    {
        _notifier = notifier;
    }

    public async Task ProcessOrder(Order order)
    {
        // ... логика обработки заказа
        await _notifier.SendAsync(order.CustomerEmail, "Your order is confirmed!");
    }
}

// В тестах используем заглушку
public class MockNotificationService : INotificationService
{
    public List<string> SentMessages { get; } = new();
    public Task SendAsync(string recipient, string message)
    {
        SentMessages.Add($"To: {recipient}, Msg: {message}");
        return Task.CompletedTask;
    }
}

Важное правило: Не создаю абстракцию «на будущее», если нет хотя бы одной конкретной причины или уже существующей второй реализации. Преждевременная абстракция так же вредна, как и ее отсутствие.

Ответ 18+ 🔞

А, ну вот, подъехали к самому соку, к этой вашей абстракции! Ебать мои старые костыли, сколько же народу её не понимает, а потом плачут, что тесты не пишутся и всё намертво срослось.

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

Где у меня в практике это вылезает постоянно:

  1. Всякая внешняя хуйня. База данных, какой-нибудь API для платежей, отправка писем или смсок, работа с файлами — это всё надо сразу загонять за интерфейс. Почему? Да потому что в юнит-тестах тебе не нужно, чтобы твой тест реально лез в базу или слал письма. Ты просто подсовываешь мок (заглушку) и проверяешь, что твоя бизнес-логика правильно вызвала нужный метод. Доверия ебать ноль к этим внешним штукам в тестах.
  2. Когда реализаций больше одной или она вот-вот появится. Ну, типа, сортировка может быть быстрой, а может — слиянием. Или уведомление можно отправить и в телегу, и на почту, и смской. Вот это прямой звоночек — пора выносить абстракцию. Не жди, пока всё превратится в кашу.
  3. Следование принципу DIP (Инверсии зависимостей). Запомни раз и навсегда: твоя крутая бизнес-логика (модули верхнего уровня) не должна напрямую зависеть от какой-то там библиотеки для работы с SQL или SDK для отправки смс (модули нижнего уровня). И то, и другое должно смотреть на одну абстракцию (интерфейс). Это как закон. Вы ходите по охуенно тонкому льду, если его нарушаете.

Смотри, как это выглядит в коде (C#):

// Вот она, абстракция для отправки уведомлений. Ничего лишнего.
public interface INotificationService
{
    Task SendAsync(string recipient, string message);
}

// Конкретная реализация для Email. Она знает все детали про SMTP.
public class EmailNotificationService : INotificationService
{
    public async Task SendAsync(string recipient, string message)
    {
        // Тут вся магия с письмами
        await _smtpClient.SendMailAsync(...);
    }
}

// А это ядро, бизнес-логика. Оно НЕ ЗНАЕТ про SMTP. Оно знает только про интерфейс.
public class OrderProcessor
{
    private readonly INotificationService _notifier;
    // Зависимость приходит извне (Dependency Injection)
    public OrderProcessor(INotificationService notifier)
    {
        _notifier = notifier;
    }

    public async Task ProcessOrder(Order order)
    {
        // ... обработали заказ...
        // И просто говорим: "эй, сервис, отправь что-то!"
        await _notifier.SendAsync(order.CustomerEmail, "Your order is confirmed!");
    }
}

// А в тестах — красота! Подсовываем заглушку.
public class MockNotificationService : INotificationService
{
    public List<string> SentMessages { get; } = new();
    public Task SendAsync(string recipient, string message)
    {
        // Никуда ничего не шлём, просто запоминаем, что хотели отправить.
        SentMessages.Add($"To: {recipient}, Msg: {message}");
        return Task.CompletedTask;
    }
}

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