Зависимости, внедрение зависимостей и сквозная функциональность в современной разработке

Dependency Injection & AOP in .NET

Зависимости

Зависимости окружают нас повсюду. Только Господь Бог создал из ничего всё. Люди создают что-то на основе других вещей. Строителю нужны стройматериалы, садовнику — семена, писателю — мысли. Программисты тоже работают с текстом, выражают идеи и воплощают их с помощью кода. По статистике, 90% времени разработчик тратит на чтение чужого и своего кода. Текст — основной материал, та самая глина, из которой каждый из нас ежедневно пытается слепить мысль. У кого-то мысли неуклюжи, у кого-то они получаются почти совершенными (предела совершенству, как известно, нет). Тогда другие программисты могут оценить всю красоту этого словесного шедевра — или карикатуры. Идеального. Почти идеального. Потому что первая и главная зависимость — это сам человек. Его ограниченные возможности, его мыслительные способности и несовершенные черты характера, и всем известные слабости.

Опыт — это череда проб и ошибок. Память о хороших и плохих решениях, а также чутьё, интуиция, которую тоже можно воспитать. И когда программист добавляет новую функциональность — делает выбор: изобретать велосипед или использовать опыт коллег. Всегда хочется создать что-то новое, необычное, нужное. И кажется, что все программы уже написаны (обычно непрограммисты в этом искренне убеждены), всё сделано, всё изобретено и открыто и нет больше места твоим идеям. Но всегда находится кто-то, кто реализует идею и даёт ей долгую жизнь. Этот кто-то, обычно, опережает наши идеи. Кто-то более умный, трудолюбивый и настойчивый постоянно нас опережает.

Мы, обычные программисты, зависим от идей и мыслей. От чужих идей и чужих мыслей. Неохотно, очень неохотно программисты используют чужие идеи. Главным критерием, как, впрочем, и в других областях творчества и ремесла, является красота. Если красиво — значит, скорее всего, правильно.

«Если код выглядит как говно — скорее всего, он и работает как говно.»
— Неизвестный автор

Но каждая новая зависимость, каждая новая компонента, библиотека, сервис, фреймворк, класс или скрипт, добавленные в проект, — увеличивают его сложность. Заставляют зависеть от них, заставляют сталкиваться с неизвестным чужим кодом и чужими мыслями. А нужно, чтобы программа работала. И работала хорошо. И работала хорошо позавчера. (Иногда только получая задание — ты уже опаздываешь с его выполнением). И выглядеть она должна намного лучше. А время? Постоянная зависимость от времени. И нужна кроссбраузерность, кроссплатформенность, кроссвременность и кросс-что-то-там-ещё. Это постоянный кросс на длинную дистанцию самого последнего релиза, самой лучшей, без сомнения, программы.

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

С другой стороны, хорошие зависимости (а таких очень много) избавляют нас от рутинной работы. Не нужно заново решать уже решённые задачи. Не нужно задумываться над старыми велосипедами, тракторами и бульдозерами. Можно и нужно сосредоточиться над своей задачей. Инженер-конструктор самолётов не начинает постройку истребителя с создания горнодобывающего комбината, не изобретает металлопрокатный станок и не стоит у горна, добывая необходимый алюминий. Чтобы построить самолёт — сложное устройство — необходим исходный материал. Чтобы построить программу — нужны исходные компоненты, строительные блоки. Качественный фундамент — качественный проект. Наиболее успешные проекты используют до 70% заимствованного кода [Гради Буч]. Зависимости определяют будущий проект, и правильное внедрение этих зависимостей является искусством и наукой и мастерством в одном лице. Подробнее о зависимостях, их внедрении и выборе рассмотрим на примере платформы .NET.


Города, которые живут, и города, которые спроектированы

Прежде чем говорить о коде — скажем о городах. Эта аналогия не случайна.

Физик Джеффри Уэст в книге «Масштаб» (Scale, 2017) (рекомендую для прочтения всем) открыл эффект масштаба: данные из городов всего мира демонстрируют, что инфраструктура и потребление энергии масштабируются субпропорционально (то есть растут медленнее, чем само население: удвоили город — инфраструктура выросла не в 2 раза, а в 1.8), тогда как показатели социального взаимодействия — суперпропорционально (быстрее, чем население: удвоил город — зарплаты и инновации выросли уже в 2.2 раза). Проще говоря: дороги и трубы растут медленнее населения, зарплаты и инновации — быстрее. Уэст обнаружил универсальные законы роста и самоорганизации, которые управляют живыми системами, городами и компаниями, причём подчиняются они одним и тем же базовым принципам. Города — это самоорганизующиеся сети, а не спроектированные машины.

Бразилиа — самый известный контрпример. Столица, спроектированная с нуля в 1956 году учеником Ле Корбюзье Лусио Коста и Оскаром Нимейером — на пустом плато, свободном от балласта трущоб и колониального наследия. Архитектурный шедевр, объект ЮНЕСКО. Но город построен исключительно для автомобиля. Эти огромные пространства не созданы для прогулок. Некоторые утверждают, что Бразилиа не может считаться настоящим городом, потому что в ней отсутствуют необходимые «антропоморфные ингредиенты» — грязные улицы, люди, идущие пешком в соседний офис, стихийные рынки, случайные встречи.

Что пошло не так? Всегда что-то не так, правда? Человек планирует, а выходит обычно не очень. Город был спроектирован сверху вниз, исходя из идеологии архитекторов, а не из потребностей жителей. Живой город всегда вырастает из паттернов поведения людей — снизу вверх. Именно это имеет в виду Уэст, когда говорит о самоорганизации: сложность городской жизни не задаётся генеральным планом, а возникает из миллионов мелких взаимодействий.

Это прямая аналогия с разработкой программного обеспечения.


UI как главная зависимость: архитектура снизу вверх

Under construction — refactoring inevitable

Традиционный подход к разработке: сначала база данных → бизнес-логика → UI. Звучит логично. На практике это Бразилиа: система спроектирована сверху вниз по идеальной схеме, а когда доходит до пользователя — жить в ней неудобно, и приходится переписывать.

Пользовательский интерфейс — это не «слой поверх» архитектуры. UI — это точка сборки реальных требований. Именно здесь система встречается с живым пользователем, именно здесь проявляются настоящие паттерны использования. Как в органическом городе улицы прокладываются там, где люди ходят, — а не там, где архитектор провёл линейку.

Марк Шиман описывает типичный антипаттерн: представьте интернет-магазин, где пользовательский интерфейс напрямую взаимодействует с запросами к базе данных. При переходе с SQL на NoSQL придётся переписывать значительные части приложения. Применяя принципы DI, пользовательский интерфейс взаимодействует исключительно через определённые интерфейсы — и при смене базы данных достаточно создать новую реализацию интерфейса доступа к данным, не затрагивая UI-слой [Mark Seemann, Dependency Injection in .NET, Manning].

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


UI-first: что это значит на практике

Традиционный порядок (DB-first, Бразилиа):

1
База данных → Репозитории → Сервисы → API → UI

UI-first подход (органический город):

1
User Story → UI Mockup → API contract → Сервисы → Репозитории → БД

Это не «нарисовать кнопки первыми». Это значит, что форма данных и поведение системы определяются запросами пользователя, а не удобством хранения.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ❌ DB-first: форма данных диктует UI
public class ProductDto
{
    public int ProductId { get; set; }           // из таблицы
    public int CategoryFk { get; set; }           // из схемы БД
    public decimal UnitPriceWithTax { get; set; } // из хранимой процедуры
}

// ✅ UI-first: форма данных отражает потребность пользователя
public class ProductCardViewModel
{
    public string Name { get; set; }
    public string FormattedPrice { get; set; }   // "$12.99"
    public string CategoryLabel { get; set; }    // "Electronics"
    public bool IsAvailable { get; set; }
    public string[] Tags { get; set; }
}

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


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

DIP (Dependency Inversion Principle) — один из пяти принципов SOLID:

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

[Agile Principles, Practices and Patterns in C#]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ❌ Нарушение DIP: высокоуровневый класс зависит от конкретной реализации
public class OrderService
{
    private readonly SqlOrderRepository _repository = new SqlOrderRepository();
}

// ✅ Соблюдение DIP: зависимость от абстракции
public class OrderService
{
    private readonly IOrderRepository _repository;

    public OrderService(IOrderRepository repository)
    {
        _repository = repository;
    }
}

Dependency Injection & IoC

«”Dependency Injection” — это дорогой термин для дешёвой идеи. Внедрение зависимостей означает передачу объекту его переменных экземпляра. Вот и всё.»
— James Shore

Dependency Injection — это набор принципов и паттернов проектирования, которые позволяют разрабатывать слабосвязанный код.

— Mark Seemann

Inversion of Control (IoC) — паттерн для инверсии интерфейсов, потоков и зависимостей.

— John Sonmez

IoC Container — фреймворк для внедрения зависимостей.

Типы зависимостей

Тип Примеры
Явные (public) Параметры конструктора, свойства
Неявные (hidden)
1
new SomeService()
, статические методы, синглтоны
Внутренние Бизнес-логика, доменные объекты
Внешние БД, файловая система, веб-сервисы, сторонние библиотеки
Базовые Фреймворк, runtime, ОС
Современные (2025) AI/LLM сервисы, облачные провайдеры, feature flags, language models

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

1. Constructor Injection (рекомендуется)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class PaymentService
{
    private readonly IPaymentGateway _gateway;
    private readonly ILogger<PaymentService> _logger;

    public PaymentService(IPaymentGateway gateway, ILogger<PaymentService> logger)
    {
        _gateway = gateway ?? throw new ArgumentNullException(nameof(gateway));
        _logger = logger;
    }

    public async Task<PaymentResult> ProcessAsync(PaymentRequest request)
    {
        _logger.LogInformation("Processing payment {Id}", request.Id);
        return await _gateway.ChargeAsync(request);
    }
}

2. Property Injection (опционально, с локальными дефолтами)

1
2
3
4
5
6
7
8
9
10
public class ReportService
{
    // Опциональная зависимость — работает и без внешней инъекции
    public ILogger Logger { get; set; } = NullLogger.Instance;

    public void Generate(Report report)
    {
        Logger.LogInformation("Generating report: {Name}", report.Name);
    }
}

3. Method Injection (зависимость меняется с каждым вызовом)

1
2
3
4
5
6
7
8
public class DataProcessor
{
    // transformer варьируется в зависимости от контекста вызова
    public void Process(IDataSet dataSet, ITransformer transformer)
    {
        var result = transformer.Transform(dataSet);
    }
}

4. Factory Injection

1
2
3
4
5
6
7
8
9
10
public class NotificationService
{
    private readonly Func<NotificationType, INotifier> _notifierFactory;

    public NotificationService(Func<NotificationType, INotifier> notifierFactory)
        => _notifierFactory = notifierFactory;

    public void Notify(NotificationType type, string message)
        => _notifierFactory(type).Send(message);
}

Инверсия управления (IoC)

Три вида инверсии:

Вид инверсии Описание
Interface Inversion Контроль над интерфейсом между двумя компонентами
Flow Inversion Контроль над потоком выполнения (Event-driven, Hollywood Principle)
Creation Inversion Контроль над созданием и связыванием зависимостей

IoC-контейнер в .NET 8+

1
2
3
4
5
6
7
8
9
10
11
12
13
// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddSingleton<ICache, MemoryCache>();
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();

// Современные зависимости
builder.Services.AddOpenAIClient(builder.Configuration["OpenAI:ApiKey"]);
builder.Services.AddAzureBlobStorage(builder.Configuration["Azure:Storage"]);
builder.Services.AddFeatureManagement();

var app = builder.Build();

Паттерн Register → Resolve → Release

1
2
3
4
5
6
┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│  REGISTER   │───▶│   RESOLVE   │───▶│   RELEASE   │
│             │    │             │    │             │
│ Настройка   │    │ Получение   │    │ Освобождение│
│ контейнера  │    │ зависимост. │    │ ресурсов    │
└─────────────┘    └─────────────┘    └─────────────┘

Hollywood Principle: «Не звони в контейнер — он позвонит тебе сам.»

Современные IoC-фреймворки (.NET)

IoC-фреймворк Преимущества Недостатки
Microsoft.Extensions.DI Встроен в .NET, официальная поддержка Минималистичный API
Autofac Гибкий API, модули, декораторы Немного сложнее в освоении
Scrutor Конвенционная регистрация поверх MS DI Зависит от MS DI
Castle Windsor Полный, поддержка декораторов, типизированные фабрики Местами quirky API
Ninject Лёгкий, расширяемый Медленная производительность

2025: Для большинства проектов на .NET 8+ достаточно встроенного контейнера +

1
Scrutor
для конвенционной регистрации.


Сквозная функциональность (Cross-cutting Concerns)

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

Аспект Описание
Logging Запись событий о состоянии приложения
Auditing Мониторинг важных операций и данных
Caching Повышение производительности через кеширование
Security Авторизация и аутентификация
Error Handling Перехват и обработка исключений
Fault Tolerance Circuit Breaker, Retry, Timeout
Performance Counting Диагностика производительности
Distributed Tracing OpenTelemetry — современная замена простому логированию

AOP & Aspects

Aspect-oriented programming (AOP) — подход к программированию, который позволяет глобальным свойствам программы определять, как она компилируется в исполняемую программу.

Аспект — подпрограмма, связанная с конкретным свойством программы. При изменении этого свойства эффект «расходится» по всей программе. Аспект используется как часть нового вида компилятора — aspect weaver.

Ключевые принципы, на которых строится AOP:

  • Wrapper / обёртка
  • Single Responsibility Principle
  • Decorator Pattern
  • Open/Closed Principle

AOP → Middleware: современный подход

В 2025 году большинство задач AOP решается через Middleware Pipeline (ASP.NET Core), Decorator Pattern, Behaviors (MediatR) и Interceptors. PostSharp-подход с IL Weaving актуален для специфических случаев.

ASP.NET Core Middleware Pipeline

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestLoggingMiddleware> _logger;

    public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var sw = Stopwatch.StartNew();
        _logger.LogInformation("→ {Method} {Path}", context.Request.Method, context.Request.Path);
        try
        {
            await _next(context);
        }
        finally
        {
            sw.Stop();
            _logger.LogInformation("← {Status} {Path} [{Elapsed}ms]",
                context.Response.StatusCode, context.Request.Path, sw.ElapsedMilliseconds);
        }
    }
}

app.UseMiddleware<RequestLoggingMiddleware>();

Декоратор — универсальный AOP без магии

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public interface IProductRepository
{
    Task<Product?> GetByIdAsync(int id);
}

public class EfProductRepository : IProductRepository
{
    private readonly AppDbContext _db;
    public EfProductRepository(AppDbContext db) => _db = db;
    public Task<Product?> GetByIdAsync(int id) => _db.Products.FindAsync(id).AsTask();
}

// Декоратор кеширования — AOP без PostSharp
public class CachedProductRepository : IProductRepository
{
    private readonly IProductRepository _inner;
    private readonly IMemoryCache _cache;

    public CachedProductRepository(IProductRepository inner, IMemoryCache cache)
    {
        _inner = inner;
        _cache = cache;
    }

    public Task<Product?> GetByIdAsync(int id)
        => _cache.GetOrCreateAsync($"product:{id}", _ => _inner.GetByIdAsync(id));
}

// Регистрация с Scrutor
services.AddScoped<IProductRepository, EfProductRepository>();
services.Decorate<IProductRepository, CachedProductRepository>();

MediatR Pipeline Behaviors (CQRS + AOP)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;

    public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
        => _logger = logger;

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        var name = typeof(TRequest).Name;
        _logger.LogInformation("[START] {Request}", name);
        var sw = Stopwatch.StartNew();
        var response = await next();
        _logger.LogInformation("[END] {Request} [{Elapsed}ms]", name, sw.ElapsedMilliseconds);
        return response;
    }
}

services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(CachingBehavior<,>));

Современные зависимости: AI, Cloud, LLM

В 2025 году зависимости вышли за пределы баз данных и сторонних библиотек.

AI и языковые модели как обычная инъекция

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public interface IAIAssistant
{
    Task<string> CompleteAsync(string prompt, CancellationToken ct = default);
}

public class OpenAIAssistant : IAIAssistant
{
    private readonly OpenAIClient _client;
    private readonly string _model;

    public OpenAIAssistant(OpenAIClient client, IConfiguration config)
    {
        _client = client;
        _model = config["OpenAI:Model"] ?? "gpt-4o";
    }

    public async Task<string> CompleteAsync(string prompt, CancellationToken ct = default)
    {
        var response = await _client.GetChatClient(_model)
            .CompleteChatAsync(prompt, cancellationToken: ct);
        return response.Value.Content[0].Text;
    }
}

// Легко переключиться на другой провайдер:
// services.AddScoped<IAIAssistant, ClaudeAssistant>();
builder.Services.AddScoped<IAIAssistant, OpenAIAssistant>();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// AI как сквозной concern: умная обработка ошибок через Middleware
public class AIEnhancedErrorMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IAIAssistant _ai;

    public AIEnhancedErrorMiddleware(RequestDelegate next, IAIAssistant ai)
    {
        _next = next;
        _ai = ai;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            var suggestion = await _ai.CompleteAsync(
                $"Suggest a user-friendly error message for: {ex.GetType().Name}: {ex.Message}");

            context.Response.StatusCode = 500;
            await context.Response.WriteAsJsonAsync(new
            {
                Error = "An error occurred",
                Suggestion = suggestion
            });
        }
    }
}

Feature Flags как зависимости (AI по требованию)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class ProductService
{
    private readonly IFeatureManager _features;
    private readonly IAIAssistant _ai;
    private readonly IProductRepository _repo;

    public ProductService(IFeatureManager features, IAIAssistant ai, IProductRepository repo)
    {
        _features = features;
        _ai = ai;
        _repo = repo;
    }

    public async Task<ProductDescription> GetDescriptionAsync(int id)
    {
        var product = await _repo.GetByIdAsync(id);

        if (await _features.IsEnabledAsync("AIDescriptions"))
        {
            var enhanced = await _ai.CompleteAsync(
                $"Enhance product description: {product!.Description}");
            return new ProductDescription(enhanced);
        }

        return new ProductDescription(product!.Description);
    }
}

OpenTelemetry: современный аспект наблюдаемости

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation()
        .AddEntityFrameworkCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddOtlpExporter(opt => opt.Endpoint = new Uri("http://jaeger:4317")))
    .WithMetrics(metrics => metrics
        .AddAspNetCoreInstrumentation()
        .AddRuntimeInstrumentation()
        .AddPrometheusExporter());

public class OrderService
{
    private static readonly ActivitySource _activity = new("MyApp.OrderService");

    public async Task<Order> CreateOrderAsync(CreateOrderCommand cmd)
    {
        using var span = _activity.StartActivity("CreateOrder");
        span?.SetTag("order.customerId", cmd.CustomerId);
        // бизнес-логика...
        return order;
    }
}

Сравнение подходов к AOP в 2025

Подход Плюсы Минусы Когда использовать
ASP.NET Core Middleware Встроен, прост, HTTP-centric Только HTTP-запросы Логирование HTTP, auth, CORS
Decorator Pattern Явный, тестируемый, без магии Много бойлерплейта Кеширование, валидация
MediatR Behaviors Удобен для CQRS, гибкий Требует MediatR Commands/Queries pipeline
Scrutor Decorators Конвенционная регистрация Зависит от MS DI Массовое декорирование
Castle Windsor Interceptors Мощный, AOP без атрибутов Сложная настройка Легаси + enterprise
PostSharp / Metalama Compile-time weaving, быстрый Платный, магия в коде Финансовые системы, аудит
Source Generators Compile-time, нет runtime overhead Сложно писать Высоконагруженные системы

Ссылки и дополнительные материалы

Книги

  • Mark Seemann, Steven van DeursenDependency Injection Principles, Practices, and Patterns (Manning, 2019)
  • Geoffrey WestScale: The Universal Laws of Growth, Innovation, Sustainability, and the Pace of Life in Organisms, Cities, Economies, and Companies (Penguin Press, 2017)
  • Martin FowlerInversion of Control Containers and the Dependency Injection pattern
  • Robert C. MartinAgile Principles, Patterns, and Practices in C#

Инструменты и фреймворки

AI и LLM-зависимости


Михаил Струтинский, 2013-2026