Зависимости, внедрение зависимостей и сквозная функциональность в современной разработке
Зависимости
Зависимости окружают нас повсюду. Только Господь Бог создал из ничего всё. Люди создают что-то на основе других вещей. Строителю нужны стройматериалы, садовнику — семена, писателю — мысли. Программисты тоже работают с текстом, выражают идеи и воплощают их с помощью кода. По статистике, 90% времени разработчик тратит на чтение чужого и своего кода. Текст — основной материал, та самая глина, из которой каждый из нас ежедневно пытается слепить мысль. У кого-то мысли неуклюжи, у кого-то они получаются почти совершенными (предела совершенству, как известно, нет). Тогда другие программисты могут оценить всю красоту этого словесного шедевра — или карикатуры. Идеального. Почти идеального. Потому что первая и главная зависимость — это сам человек. Его ограниченные возможности, его мыслительные способности и несовершенные черты характера, и всем известные слабости.
Опыт — это череда проб и ошибок. Память о хороших и плохих решениях, а также чутьё, интуиция, которую тоже можно воспитать. И когда программист добавляет новую функциональность — делает выбор: изобретать велосипед или использовать опыт коллег. Всегда хочется создать что-то новое, необычное, нужное. И кажется, что все программы уже написаны (обычно непрограммисты в этом искренне убеждены), всё сделано, всё изобретено и открыто и нет больше места твоим идеям. Но всегда находится кто-то, кто реализует идею и даёт ей долгую жизнь. Этот кто-то, обычно, опережает наши идеи. Кто-то более умный, трудолюбивый и настойчивый постоянно нас опережает.
Мы, обычные программисты, зависим от идей и мыслей. От чужих идей и чужих мыслей. Неохотно, очень неохотно программисты используют чужие идеи. Главным критерием, как, впрочем, и в других областях творчества и ремесла, является красота. Если красиво — значит, скорее всего, правильно.
«Если код выглядит как говно — скорее всего, он и работает как говно.»
— Неизвестный автор
Но каждая новая зависимость, каждая новая компонента, библиотека, сервис, фреймворк, класс или скрипт, добавленные в проект, — увеличивают его сложность. Заставляют зависеть от них, заставляют сталкиваться с неизвестным чужим кодом и чужими мыслями. А нужно, чтобы программа работала. И работала хорошо. И работала хорошо позавчера. (Иногда только получая задание — ты уже опаздываешь с его выполнением). И выглядеть она должна намного лучше. А время? Постоянная зависимость от времени. И нужна кроссбраузерность, кроссплатформенность, кроссвременность и кросс-что-то-там-ещё. Это постоянный кросс на длинную дистанцию самого последнего релиза, самой лучшей, без сомнения, программы.
Зависимость подозрительна. В зависимостях сидят баги, хуже того — чужие баги. Зависимости требуют к себе внимания, как дорогие гости. Нужно уметь выбирать зависимости, нужно тратить время на их обслуживание. Нужно очень быстро учиться, изучая зависимости. Их выбор — искусство, а их влияние на любой проект трудно переоценить.
С другой стороны, хорошие зависимости (а таких очень много) избавляют нас от рутинной работы. Не нужно заново решать уже решённые задачи. Не нужно задумываться над старыми велосипедами, тракторами и бульдозерами. Можно и нужно сосредоточиться над своей задачей. Инженер-конструктор самолётов не начинает постройку истребителя с создания горнодобывающего комбината, не изобретает металлопрокатный станок и не стоит у горна, добывая необходимый алюминий. Чтобы построить самолёт — сложное устройство — необходим исходный материал. Чтобы построить программу — нужны исходные компоненты, строительные блоки. Качественный фундамент — качественный проект. Наиболее успешные проекты используют до 70% заимствованного кода [Гради Буч]. Зависимости определяют будущий проект, и правильное внедрение этих зависимостей является искусством и наукой и мастерством в одном лице. Подробнее о зависимостях, их внедрении и выборе рассмотрим на примере платформы .NET.
Города, которые живут, и города, которые спроектированы
Прежде чем говорить о коде — скажем о городах. Эта аналогия не случайна.
Физик Джеффри Уэст в книге «Масштаб» (Scale, 2017) (рекомендую для прочтения всем) открыл эффект масштаба: данные из городов всего мира демонстрируют, что инфраструктура и потребление энергии масштабируются субпропорционально (то есть растут медленнее, чем само население: удвоили город — инфраструктура выросла не в 2 раза, а в 1.8), тогда как показатели социального взаимодействия — суперпропорционально (быстрее, чем население: удвоил город — зарплаты и инновации выросли уже в 2.2 раза). Проще говоря: дороги и трубы растут медленнее населения, зарплаты и инновации — быстрее. Уэст обнаружил универсальные законы роста и самоорганизации, которые управляют живыми системами, городами и компаниями, причём подчиняются они одним и тем же базовым принципам. Города — это самоорганизующиеся сети, а не спроектированные машины.
Бразилиа — самый известный контрпример. Столица, спроектированная с нуля в 1956 году учеником Ле Корбюзье Лусио Коста и Оскаром Нимейером — на пустом плато, свободном от балласта трущоб и колониального наследия. Архитектурный шедевр, объект ЮНЕСКО. Но город построен исключительно для автомобиля. Эти огромные пространства не созданы для прогулок. Некоторые утверждают, что Бразилиа не может считаться настоящим городом, потому что в ней отсутствуют необходимые «антропоморфные ингредиенты» — грязные улицы, люди, идущие пешком в соседний офис, стихийные рынки, случайные встречи.
Что пошло не так? Всегда что-то не так, правда? Человек планирует, а выходит обычно не очень. Город был спроектирован сверху вниз, исходя из идеологии архитекторов, а не из потребностей жителей. Живой город всегда вырастает из паттернов поведения людей — снизу вверх. Именно это имеет в виду Уэст, когда говорит о самоорганизации: сложность городской жизни не задаётся генеральным планом, а возникает из миллионов мелких взаимодействий.
Это прямая аналогия с разработкой программного обеспечения.
UI как главная зависимость: архитектура снизу вверх
Традиционный подход к разработке: сначала база данных → бизнес-логика → 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) | , статические методы, синглтоны | ||
| Внутренние | Бизнес-логика, доменные объекты | ||
| Внешние | БД, файловая система, веб-сервисы, сторонние библиотеки | ||
| Базовые | Фреймворк, 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 Deursen — Dependency Injection Principles, Practices, and Patterns (Manning, 2019)
- Geoffrey West — Scale: The Universal Laws of Growth, Innovation, Sustainability, and the Pace of Life in Organisms, Cities, Economies, and Companies (Penguin Press, 2017)
- Martin Fowler — Inversion of Control Containers and the Dependency Injection pattern
- Robert C. Martin — Agile Principles, Patterns, and Practices in C#
Инструменты и фреймворки
- Microsoft.Extensions.DependencyInjection
- Autofac
- Scrutor — convention-based decoration
- MediatR — CQRS + pipeline behaviors
- Metalama — современная замена PostSharp на Roslyn
- OpenTelemetry .NET
- Polly — resilience & fault tolerance
- Microsoft.FeatureManagement
AI и LLM-зависимости
- Semantic Kernel — AI orchestration для .NET
- Microsoft.Extensions.AI — абстракция над AI-провайдерами
- Azure OpenAI Service
Михаил Струтинский, 2013-2026