10 вопросов от junior для senior разработчиков

Опубликовано 14.11.2026 07:33:00 в категории FAQ по программированию

Не только ответы но и конкретные примеры .NET: C#, ASP.NET Core, EF Core Стек: .NET (C#, ASP.NET Core, EF Core).

1. Как понять, где действительно нужен ИИ, а где это «модный шум»?

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

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

ИИ — плохой выбор, когда есть чёткие правила:

  • валидация форм, расчёт налогов, бухгалтерские проводки — это switch и обычный код;
  • задача, где ошибка стоит дорого и нужна 100% предсказуемость (проведение платежа, списание со счёта);
  • «нам нужно, чтобы бот отвечал на всё» без KPI качества.

Практический чек-лист перед внедрением:

  1. Есть ли размеченные данные или хотя бы 100–200 примеров «правильных ответов»?
  2. Какую метрику качества вы измеряете (точность классификации, F1, доля корректных ответов)?
  3. Какая цена ошибки? Если ответ можно дёшево проверить человеком — ИИ подходит.
  4. Есть ли простое правило, дающее 80% результата? Тогда начните с правила, ИИ добавьте сверху как fallback.

Для бизнеса формулировка одна: «Ручная обработка стоит X рублей в месяц. Решение на ИИ стоит Y рублей и обрабатывает Z% автоматически с качеством N%. Отбиваемся за M месяцев».


2. Как интегрировать стороннюю ML-модель через API в .NET-сервис, чтобы это не стало «чёрным ящиком»?

Ключевые принципы: модель — это внешняя зависимость с деградирующим SLA, а не магия. Относитесь к ней как к unreliable third-party API.

Резервный тайм-аут, retry, circuit breaker — через Polly:

services.AddHttpClient<ITextClassifier, OpenAiTextClassifier>(client =>
{
    client.BaseAddress = new Uri("https://api.openai.com/v1/");
    client.Timeout = TimeSpan.FromSeconds(30);
})
.AddResilienceHandler("ml-pipeline", b => b
    .AddRetry(new HttpRetryStrategyOptions
    {
        MaxRetryAttempts = 2,
        BackoffType = DelayBackoffType.Exponential
    })
    .AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
    {
        FailureRatio = 0.5,
        MinimumThroughput = 10,
        SamplingDuration = TimeSpan.FromSeconds(30)
    }));

Деградация вместо падения. У клиента должен быть fallback-режим: если ML недоступен, тикет уходит в ручную очередь модерации, а не в исключение.

Наблюдаемость. Логируйте идентификатор запроса к модели, версию модели, промпт (в обрезанном виде), токены, латентность и итоговый ответ. Всё в OpenTelemetry: метрика «доля ответов, отвергнутых проверкой качества» — главный сигнал, что модель деградировала.

Контракт. Модель не должна возвращать «просто текст» в домен. Определите DTO с валидацией:

public record ClassificationResult(
    string Category,
    double Confidence,
    string RawResponse,
    string ModelVersion);

// Валидация на границе — модель может вернуть мусор
public bool IsValid(out string error)
{
    if (Confidence < 0.6) { error = "Low confidence"; return false; }
    if (!KnownCategories.Contains(Category)) { error = "Unknown category"; return false; }
    error = null;
    return true;
}

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

Откат. Если модель работает хуже (или провайдер поднял цены), вы переключаетесь на предыдущую версию или на fallback-режим через feature flags, без деплоя.


3. Как тестировать код, зависящий от недетерминированных ответов LLM?

Ключ: отделить детерминированную логику от недетерминированной модели и тестировать каждую по-своему.

Слои тестов:

  1. Unit-тесты вашей логики — модель замокана интерфейсом:
public interface ITextClassifier
{
    Task<ClassificationResult> ClassifyAsync(string text, CancellationToken ct);
}

[Fact]
public async Task Low_confidence_result_goes_to_manual_review()
{
    var classifier = Substitute.For<ITextClassifier>();
    classifier.ClassifyAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
        .Returns(new ClassificationResult("spam", 0.3, "...", "gpt-4o"));

    var handler = new TicketHandler(classifier, reviewQueue: mockRepo);
    await handler.HandleAsync(new Ticket("Some text"));

    await mockRepo.Received(1).EnqueueForReviewAsync(Arg.Any<Ticket>());
}
  1. Golden-датасет — 50–200 реальных примеров с ожидаемыми ответами. Прогоняете промпт через реальную модель (можно в ночном CI с температурой 0) и сравниваете. Порог: «95% примеров классифицированы верно — релиз ок».

  2. Снапшот-тесты промптов — зафиксированные «хорошие» промпты. Изменение промпта требует обновления снапшота и осознанного review.

  3. Регрессия при смене версии модели. Держите пайплайн, который при обновлении модели прогоняет golden-датасет и сравнивает метрики со старой версией. Падение качества — блокер.

  4. Сэмплы в проде — сэмплирование ответов и ручная проверка, плюс реакция пользователей (кнопка «это неверный ответ»).

Главное правило: не тестируйте саму модель — тестируйте ваш контракт с ней и реакцию системы на любой ответ.


4. Наследование или композиция? Красные флаги наследования

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

Красные флаги, что наследование выбрано плохо:

  • Базовый класс знает о наследниках (if (this is PremiumAccount) в базовом классе);
  • Наследник переопределяет метод и игнорирует контракт базового (бросает NotSupportedException — это нарушение LSP);
  • Наследование используется ради переиспользования кода, а не ради полиморфизма («User наследуется от Person ради метода GetFullName»);
  • Базовый класс постоянно меняется, и это ломает наследников («хрупкий базовый класс»);
  • Наследник переопределяет метод лишь для того, чтобы вставить логику до/после вызова base — это композиция с декоратором.

Классический .NET-пример неудачного наследования — List<T> как базовый класс. Нужен список с валидацией? Используйте композицию:

// Плохо: List<T> наружу, нарушения контракта неизбежны
public class Cart : List<CartItem> { }

// Хорошо: инкапсуляция
public class Cart
{
    private readonly List<CartItem> _items = new();

    public void Add(CartItem item)
    {
        if (item.Quantity <= 0)
            throw new ArgumentException("Quantity must be positive");
        _items.Add(item);
    }

    public IReadOnlyCollection<CartItem> Items => _items;
}

Официальные guidelines .NET (Framework Design Guidelines) говорят то же: предпочитайте композицию, публичное наследование от собственных коллекций — антипаттерн.

Композиция + декоратор — штатный способ «дорисовать» поведение в .NET: middleware, ILogger-обёртки, IAsyncEnumerable, Polly-политики — везде внутри именно композиция.


5. Как распознать God Object и безопасно его разбить

Признаки:

  • Файл больше 500–1000 строк, а класс — больше 15–20 публичных методов;
  • Вы не можете написать unit-тест без Substitute.For<> для 10+ зависимостей;
  • Класс называется Manager, Service, Processor, Helper, Controller — и при этом делает всё;
  • Половина полей класса не используется в половине методов;
  • Одно изменение ломает сразу несколько непохожих фич.

Метрики, на которые стоит смотреть в анализаторах (Roslyn Analyzer, SonarQube, NDepend): Cyclomatic Complexity > 15 на метод, Coupling (число зависимостей) > 7, LCOM (Lack of Cohesion of Methods) — низкая связность данных внутри класса.

План безопасного разбиения:

  1. Покрыть тестами до рефакторинга. Характеризационные тесты — фиксируем текущее поведение, даже если оно кривое.
  2. Найти естественные швы: группы методов, использующие одно и то же подмножество полей. Это кандидаты в новые классы.
  3. Извлекать по одному классу за раз, не меняя внешнее поведение. Каждый шаг — отдельный PR, который можно откатить.
  4. Использовать переходные фасады: старый класс остаётся точкой входа, новые классы приходят к нему как зависимости.
  5. Прогонять миграцию под feature flags, если риск велик.

Типичная структура после разбиения God-контроллера:

// Было: OrderProcessor на 2000 строк
// Стало:
public class CreateOrderHandler          // use case
{
    private readonly IOrderRepository _orders;
    private readonly IPricingService _pricing;
    private readonly IUnitOfWork _uow;

    public async Task<Result<OrderId>> HandleAsync(CreateOrderCommand cmd, CancellationToken ct)
    {
        var order = Order.Create(cmd.CustomerId);
        foreach (var line in cmd.Lines)
        {
            var price = await _pricing.GetPriceAsync(line.Sku, ct);
            order.AddLine(line.Sku, line.Quantity, price);
        }
        _orders.Add(order);
        await _uow.SaveChangesAsync(ct);
        return Result.Ok(order.Id);
    }
}

6. SRP на практике: как не превратить его в «каждый класс — одна строчка»

Формулировка Роберта Мартина, которую стоит держать в голове: «класс должен иметь одну причину для изменения» — и она связана с конкретным стейкхолдером (кто заинтересован в этом коде), а не с числом строк.

Плохой критерий — «маленький». Хороший — «кто спрашивает об этом классе изменения?»

// Одна причина для изменения — «правила расчёта заказа»
public class OrderCalculator
{
    public Money CalculateTotal(Order order, IDiscountPolicy discounts) =>
        order.Lines.Sum(l => l.Price * l.Quantity) - discounts.Apply(order);
}

// Другая причина для изменения — «способ сохранения»
public class OrderRepository : IOrderRepository { /* EF Core */ }

// Третья — «формат уведомлений»
public class OrderConfirmationEmailComposer { /* ... */ }

Золотая середина:

  • Класс Order с методами AddLine, RemoveLine, CalculateTotal — норм. Это один доменный концепт.
  • Класс, который парсит email, сохраняет заказ, отправляет уведомление и логирует — не норм. У него минимум четыре причины меняться: формат email, схема БД, шаблон письма, требования к логированию.
  • Не делайте классы на 5 строк из-за SRP. Если класс не имеет самостоятельного смысла без соседа — он не отдельная ответственность, а приватная деталь.

Практический тест: если описание класса требует союза «и» («этот класс валидирует и сохраняет заказы») — там два класса.


7. Настоящий агрегат в DDD: границы, инварианты и типичные ошибки

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

Критерии правильного агрегата:

  • Все инварианты агрегата проверяются за одну транзакцию;
  • Внешний мир ссылается на агрегат только по ID, а не по объектной ссылке;
  • Изменения внутри агрегата идут только через корень (order.AddItem(...), а не item.Quantity = 5 извне).

Типичная ошибка — неверно определённая граница. Например, инвариант «сумма позиций заказа не превышает кредитный лимит клиента» не может жить в агрегате Order, если лимит в агрегате Customer:

// Инвариант: total <= creditLimit. Это пересекает два агрегата!
// Решение: доменный сервис, а не «растягивание» агрегата

public class OrderPlacementService
{
    public async Task<Result> PlaceAsync(PlaceOrderCommand cmd, CancellationToken ct)
    {
        var customer = await _customers.LoadAsync(cmd.CustomerId);
        var order = Order.Create(cmd.CustomerId, cmd.Lines);

        // Кросс-агрегатное правило — здесь, в сервисе
        if (order.Total > customer.AvailableCredit)
            return Result.Fail("Credit limit exceeded");

        _orders.Add(order);
        customer.RegisterOrder(order.Id);
        await _uow.SaveChangesAsync(ct);
        return Result.Ok();
    }
}

Ошибки, которые делают все:

  • Слишком большие агрегаты («Order содержит Customer, Product, Store...») — приводят к проблемам блокировок и параллелизма. Правило: агрегат — минимально необходимый для инвариантов набор.
  • Слишком мелкие агрегаты — «каждая сущность — корень» — теряют инварианты, логика уезжает в сервисы-обёртки над CRUD.
  • Навигационные свойства вместо ID. В EF Core не оставляйте навигации на объекты других агрегатов — только OrderId, CustomerId.
  • Репозитории для не-корневых сущностей. IOrderLineRepository — сигнал, что границу вы нарисовали неверно.
  • Игнорирование eventual consistency. Между агрегатами обновления — через доменные события (OrderPlaced, CreditReserved), а не в одной транзакции.

8. Как внедрять DDD в легаси с «таблично-ориентированной» моделью

Пошагово, без слома за неделю:

Шаг 1. Карта домена. Установите ubiquitous language с бизнесом: список терминов («заявка», «отгрузка», «ставка») и контекстов, где они живут. Без этого DDD не взлетает — вы будете спорить о словах в PR.

Шаг 2. Выделите один ограниченный контекст — тот, где больше всего боли от легаси (частые баги, сложные изменения). Не пытайтесь переписать всё.

Шаг 3. ACL (антикоррупционный слой). Новая модель не должна тянуть легаси-таблицы напрямую. Стройте адаптер:

// Легаси: flat-таблица ORDERS с 40 колонками
// Новая модель: чистая доменная сущность

public class LegacyOrderAdapter : IOrderRepository
{
    public async Task<Order?> LoadAsync(OrderId id, CancellationToken ct)
    {
        var row = await _db.Orders.FirstOrDefaultAsync(o => o.Id == id.Value, ct);
        if (row is null) return null;

        // Транслируем легаси-представление в доменную модель
        var order = Order.Rehydrate(new OrderId(row.Id),
            new CustomerId(row.CustomerRef),
            MapStatus(row.StatusCode),
            row.Lines.Select(MapLine));

        return order;
    }

    private static OrderStatus MapStatus(string legacy) => legacy switch
    {
        "N" => OrderStatus.New,
        "C" => OrderStatus.Confirmed,
        "X" => OrderStatus.Cancelled,
        _ => throw new InvalidOperationException($"Unknown status: {legacy}")
    };
}

Шаг 4. Постепенно переносите логику из сервисов. Анемичная модель + OrderService на 3000 строк → начинаем двигать методы в Order. Подход: стрижка, а не big bang. Сначала новые use cases пишутся на новой модели, старые — трогаем по мере касания.

Шаг 5. Стратификация. Внутри нового контекста: домен (без зависимостей от EF, HTTP, JSON) → application (use cases) → infrastructure (EF Core, интеграции). В легаси эта граница обычно стёрта — восстанавливайте её.

Шаг 6. Интеграция через доменные события, а не через прямые запросы между контекстами. Публикуете OrderPlaced в брокер (RabbitMQ/Kafka/MassTransit) — соседний контекст подписывается.

Типичные ошибки: пытаться внедрить DDD «сверху» во всём приложении сразу; строить ACL, который со временем обрастает собственной логикой бизнес-правил; отправлять доменные события, не обеспечив атомарность (используйте Outbox-паттерн).


9. Паттерны, которые чаще всего переиспользуют неправильно

Singleton. В .NET правильный «синглтон» — DI-контейнер с Singleton lifetime. Ручной static-класс с Instance — антипаттерн: скрытая зависимость, проблемы в тестах, статики под нагрузкой и в async-мире. Для кэша: IMemoryCache / HybridCache из Microsoft.Extensions.Caching — а не свой singleton-словарь (который ещё и не потокобезопасен, пока вы не задумались).

// Плохо
public sealed class Cache
{
    public static Cache Instance { get; } = new();
    private readonly Dictionary<string, object> _map = new();
    // не потокобезопасно, негде подменить в тесте
}

// Хорошо
services.AddMemoryCache();
services.AddSingleton<IOrderCache, OrderCache>();

Repository. Ошибки: репозиторий поверх EF Core, который просто проксирует DbSet (GetAll(), GetById()) — это лишний слой без пользы; репозиторий, возвращающий IQueryable наружу — бизнес-логика по фильтрации утекает в контроллеры. Правильно: репозиторий для агрегата (см. вопрос 7), возвращающий готовые агрегаты, а не таблицы.

Service. OrderService, UserService с 30 методами — это тот же God Object из вопроса 5, просто с модным именем. Замените на use-case-классы (CreateOrderHandler, CancelOrderHandler).

Factory. Если для создания объекта нужен просто конструктор — фабрика не нужна. Фабрика оправдана, когда: создание с побочными эффектами (загрузка из БД), условный выбор семейства объектов, объект должен создаваться внутри агрегата для сохранения инвариантов (Order.Create(...) — это фабричный метод, и он уместен).

Observer / события. Для UI — да, reactive-подход. Для бизнес-потока «создать заказ → списать деньги → отправить письмо» синхронные C#-события не годятся: не контролируете порядок, отсутствие ретраев, «теряются» подписчики. Используйте message broker (MassTransit + RabbitMQ) или хотя бы MediatR с осознанным пониманием, что это in-process диспетчер, а не очередь.

DTO ≠ Domain Model. Возвращать доменную сущность из API-эндпоинта напрямую — путь к утечке контракта и проблемам с сериализацией циклов. Слой DTO/маппинга обязателен на границах.


10. Как выбрать паттерн, когда подходят 2–3 варианта, и обосновать выбор команде

Фреймворк принятия решения — пять вопросов:

  1. Что изменится вероятнее всего? (новые требования). Если сценарий «добавить новый тип X» — Strategy/полиморфизм. Если «добавить новое поведение к существующим типам» — декоратор/расширение.
  2. Какая цена ошибочного решения? Обратимо ли (дёшево откатить) или дорого (изменение схемы БД, публичного API)?
  3. Простота vs гибкость. Не платить за абстракцию, которая не окупится (YAGNI). Один интерфейс с одной реализацией — чаще всего ок, если оправдан тестированием.
  4. Тестируемость. Можно ли подменить зависимость в тесте? Если нет — выбор плох.
  5. Читаемость. Поймёт ли разработчик через полгода без вас?

Пример trade-off таблицы для выбора между стратегиями кэширования:

Критерий In-memory (IMemoryCache) Distributed (Redis) Без кэша
Латентность ~1 мкс ~1 мс —
Консистентность eventual per-node eventual сильная
Сложность низкая средняя нет
Масштабирование нет (per-node) да —
Стоимость отказа падает узел падает Redis нет риска

Как описывать компромисс в документации: формат ADR (Architecture Decision Record). Короткий шаблон:

# ADR-007: Хранение корзины в Redis вместо In-Memory

## Контекст
Кластер из 5 узлов; корзина должна быть доступна независимо от узла.

## Решение
Используем Redis как единственное хранилище корзины, TTL 72 часа.

## Альтернативы
- In-memory + sticky sessions — отклонено: не выдержит автоскейлинг.
- SQL — отклонено: частые записи корзины создадут нагрузку.

## Последствия
Нужен fallback-режим при недоступности Redis (корзина пуста + запрос повторить).

В PR-комментарии — компактнее: «Здесь использую декоратор вместо наследования, потому что X, альтернатива Y — вот её минусы». Три-четыре предложения достаточно. Если решение требует абзаца — оформляйте ADR, а не прячьте в комментарии.

Видео

Смотреть видео на boosty

Мои видео

Boosty.to | YouTube | Yandex.Дзен | RuTube | VK Video

Больше видео на boosty Calabonga, а если хотите тоже получать донаты, тогда заходите и регистрируйте свою страницу на boosty. Я использую хостинг reg.ru, Подключайся с промокодом 9A17-953A-8591-CF98 чтобы получить скидку 5%

Еще вопросы и еще ответы

На странице Вопросы и ответы (FAQ) есть другие ответы на другие вопросы. Простые и сложные, на разные темы.

Комментарии к статье ()

Загрузка...

Что-то пошло не по сценарию и завершилось ошибкой. Перезагрузить страницу (F5) 🗙

Переподключаем сервер...

Переподключение сломалось... Пробуем еще раз сек.

Не получилось переподключится.
Пожалуйста, обновите страницу F5

Сессия была поставлена сервером на паузу.

Восстановить сессию не получилось.
Пожалуйста, обновите страницу F5.