← Назад к вопросам
Dependency Injection: Рефакторинг тесно связанного кода
2.3 Middle🔥 151 комментариев
#Dependency Injection и IoC
Условие
Дан следующий код с тесной связанностью (tight coupling):
public class OrderService { public void ProcessOrder(Order order) { var db = new SqlDatabase(); db.Save(order);
var emailService = new SmtpEmailService();
emailService.Send(order.CustomerEmail, "Order confirmed");
var logger = new FileLogger();
logger.Log($"Order {order.Id} processed");
}
}
Задание:
- Выявите нарушения принципов SOLID
- Рефакторите код с использованием Dependency Injection
- Создайте необходимые интерфейсы
- Покажите, как зарегистрировать зависимости в ASP.NET Core DI-контейнере
Критерии оценки:
- Выделение интерфейсов (IDatabase, IEmailService, ILogger)
- Внедрение зависимостей через конструктор
- Корректная регистрация в DI
- Объяснение преимуществ (тестируемость, гибкость)
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Анализ проблем: Нарушения SOLID
Код содержит множество нарушений:
- Single Responsibility (SRP) — OrderService отвечает за заказы, БД, email и логирование
- Dependency Inversion (DIP) — OrderService зависит от конкретных классов, а не интерфейсов
- Open/Closed (OCP) — нельзя использовать другую БД или email-сервис без изменения OrderService
- Interface Segregation (ISP) — нет интерфейсов для работы с зависимостями
- Tight Coupling — невозможно тестировать OrderService без реальной БД и email-сервиса
Проблемы:
- Сложно писать unit-тесты (нужны реальные БД и email-сервисы)
- Нельзя заменить SqlDatabase на другую БД без изменения кода
- Код жёстко связан с конкретными реализациями
- Сложно отследить все зависимости
Шаг 1: Определяем интерфейсы
/// <summary>
/// Интерфейс для работы с БД
/// </summary>
public interface IDatabase
{
Task SaveAsync(Order order);
}
/// <summary>
/// Интерфейс для отправки email
/// </summary>
public interface IEmailService
{
Task SendAsync(string recipient, string subject, string body);
}
/// <summary>
/// Интерфейс для логирования
/// </summary>
public interface ILogger
{
void LogInfo(string message);
void LogError(string message, Exception ex = null);
void LogWarning(string message);
}
Шаг 2: Реализуем конкретные классы
/// <summary>
/// SQL Database реализация
/// </summary>
public class SqlDatabase : IDatabase
{
private readonly string _connectionString;
public SqlDatabase(string connectionString)
{
_connectionString = connectionString;
}
public async Task SaveAsync(Order order)
{
// TODO: Сохранение в БД
using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
// ... SQL команды
Console.WriteLine($"Saved order {order.Id} to SQL");
}
}
/// <summary>
/// MongoDB реализация (альтернатива)
/// </summary>
public class MongoDbDatabase : IDatabase
{
public async Task SaveAsync(Order order)
{
// TODO: Сохранение в MongoDB
Console.WriteLine($"Saved order {order.Id} to MongoDB");
}
}
/// <summary>
/// SMTP Email реализация
/// </summary>
public class SmtpEmailService : IEmailService
{
private readonly string _smtpServer;
private readonly int _port;
public SmtpEmailService(string smtpServer = "smtp.gmail.com", int port = 587)
{
_smtpServer = smtpServer;
_port = port;
}
public async Task SendAsync(string recipient, string subject, string body)
{
using var client = new SmtpClient(_smtpServer, _port);
// TODO: Отправка email
Console.WriteLine($"Sent email to {recipient}: {subject}");
}
}
/// <summary>
/// SendGrid Email реализация (альтернатива)
/// </summary>
public class SendGridEmailService : IEmailService
{
private readonly string _apiKey;
public SendGridEmailService(string apiKey)
{
_apiKey = apiKey;
}
public async Task SendAsync(string recipient, string subject, string body)
{
// TODO: Использование SendGrid API
Console.WriteLine($"Sent email via SendGrid to {recipient}");
}
}
/// <summary>
/// File Logger реализация
/// </summary>
public class FileLogger : ILogger
{
private readonly string _logPath;
public FileLogger(string logPath = "logs.txt")
{
_logPath = logPath;
}
public void LogInfo(string message)
=> AppendLog($"[INFO] {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}");
public void LogError(string message, Exception ex = null)
=> AppendLog($"[ERROR] {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message} | Exception: {ex?.Message}");
public void LogWarning(string message)
=> AppendLog($"[WARNING] {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}");
private void AppendLog(string message)
{
File.AppendAllText(_logPath, message + Environment.NewLine);
}
}
/// <summary>
/// Console Logger реализация (для тестирования)
/// </summary>
public class ConsoleLogger : ILogger
{
public void LogInfo(string message) => Console.WriteLine($"[INFO] {message}");
public void LogError(string message, Exception ex = null) => Console.WriteLine($"[ERROR] {message} | {ex?.Message}");
public void LogWarning(string message) => Console.WriteLine($"[WARNING] {message}");
}
Шаг 3: Рефакторим OrderService
/// <summary>
/// OrderService с Dependency Injection
/// </summary>
public class OrderService
{
private readonly IDatabase _database;
private readonly IEmailService _emailService;
private readonly ILogger _logger;
// ✅ Все зависимости внедряются через конструктор
public OrderService(
IDatabase database,
IEmailService emailService,
ILogger logger)
{
_database = database ?? throw new ArgumentNullException(nameof(database));
_emailService = emailService ?? throw new ArgumentNullException(nameof(emailService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task ProcessOrderAsync(Order order)
{
try
{
// Сохраняем заказ
await _database.SaveAsync(order);
_logger.LogInfo($"Order {order.Id} saved to database");
// Отправляем email
await _emailService.SendAsync(
order.CustomerEmail,
"Order Confirmation",
$"Your order {order.Id} has been confirmed");
_logger.LogInfo($"Confirmation email sent to {order.CustomerEmail}");
_logger.LogInfo($"Order {order.Id} processed successfully");
}
catch (Exception ex)
{
_logger.LogError($"Error processing order {order.Id}", ex);
throw; // Пробрасываем дальше для обработки на более высоком уровне
}
}
}
/// <summary>
/// Модель заказа
/// </summary>
public class Order
{
public int Id { get; set; }
public string CustomerEmail { get; set; }
public decimal Amount { get; set; }
public DateTime CreatedAt { get; set; }
}
Шаг 4: Регистрация в ASP.NET Core DI контейнере
Program.cs (ASP.NET Core)
var builder = WebApplicationBuilder.CreateBuilder(args);
// ✅ Регистрируем все зависимости в DI контейнере
// Сценарий 1: Использование SQL Database
builder.Services.AddScoped<IDatabase>(provider =>
new SqlDatabase(builder.Configuration.GetConnectionString("DefaultConnection")));
// Сценарий 2: Использование SMTP Email (альтернативный комментарий)
// builder.Services.AddScoped<IEmailService>(provider =>
// new SmtpEmailService("smtp.gmail.com", 587));
// Сценарий 3: Использование SendGrid Email (производство)
builder.Services.AddScoped<IEmailService>(provider =>
new SendGridEmailService(builder.Configuration["SendGrid:ApiKey"]));
// Логирование
builder.Services.AddScoped<ILogger, FileLogger>(provider =>
new FileLogger("logs/orders.log"));
// OrderService (зависимости внедрятся автоматически)
builder.Services.AddScoped<OrderService>();
// Контроллеры
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();
Альтернатива: Более читаемый способ регистрации
// Более явный и читаемый подход
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddOrderServices(this IServiceCollection services, IConfiguration configuration)
{
// Database
services.AddScoped<IDatabase>(provider =>
{
var connString = configuration.GetConnectionString("DefaultConnection");
return new SqlDatabase(connString);
});
// Email Service (зависит от окружения)
var environment = configuration["ASPNETCORE_ENVIRONMENT"];
if (environment == "Development")
{
services.AddScoped<IEmailService, SmtpEmailService>();
}
else
{
services.AddScoped<IEmailService>(provider =>
new SendGridEmailService(configuration["SendGrid:ApiKey"]));
}
// Logger
services.AddScoped<ILogger>(provider =>
new FileLogger(Path.Combine("logs", $"orders-{DateTime.Now:yyyyMMdd}.log")));
// OrderService
services.AddScoped<OrderService>();
return services;
}
}
// Использование в Program.cs
builder.Services.AddOrderServices(builder.Configuration);
Шаг 5: Использование в контроллере
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly OrderService _orderService;
// ✅ OrderService внедряется через конструктор
public OrdersController(OrderService orderService)
{
_orderService = orderService;
}
[HttpPost]
public async Task<IActionResult> CreateOrder([FromBody] Order order)
{
try
{
await _orderService.ProcessOrderAsync(order);
return Ok(new { message = "Order processed successfully" });
}
catch (Exception ex)
{
return BadRequest(new { error = ex.Message });
}
}
}
Шаг 6: Unit тесты (теперь очень просто!)
using Xunit;
using Moq;
public class OrderServiceTests
{
[Fact]
public async Task ProcessOrder_CallsDatabaseSave()
{
// Arrange - создаём mock зависимостей
var mockDatabase = new Mock<IDatabase>();
var mockEmailService = new Mock<IEmailService>();
var mockLogger = new Mock<ILogger>();
var service = new OrderService(mockDatabase.Object, mockEmailService.Object, mockLogger.Object);
var order = new Order { Id = 1, CustomerEmail = "test@example.com", Amount = 100 };
// Act
await service.ProcessOrderAsync(order);
// Assert - проверяем, что методы были вызваны
mockDatabase.Verify(db => db.SaveAsync(order), Times.Once);
mockEmailService.Verify(es => es.SendAsync(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>()), Times.Once);
mockLogger.Verify(l => l.LogInfo(It.IsAny<string>()), Times.AtLeastOnce);
}
[Fact]
public async Task ProcessOrder_SendsEmailToCustomer()
{
var mockDatabase = new Mock<IDatabase>();
var mockEmailService = new Mock<IEmailService>();
var mockLogger = new Mock<ILogger>();
var service = new OrderService(mockDatabase.Object, mockEmailService.Object, mockLogger.Object);
var order = new Order { Id = 1, CustomerEmail = "john@example.com", Amount = 150 };
await service.ProcessOrderAsync(order);
mockEmailService.Verify(
es => es.SendAsync("john@example.com", It.IsAny<string>(), It.IsAny<string>()),
Times.Once);
}
[Fact]
public async Task ProcessOrder_LogsError_OnException()
{
var mockDatabase = new Mock<IDatabase>();
mockDatabase.Setup(db => db.SaveAsync(It.IsAny<Order>()))
.ThrowsAsync(new Exception("DB error"));
var mockEmailService = new Mock<IEmailService>();
var mockLogger = new Mock<ILogger>();
var service = new OrderService(mockDatabase.Object, mockEmailService.Object, mockLogger.Object);
var order = new Order { Id = 1, CustomerEmail = "test@example.com" };
// Act & Assert
await Assert.ThrowsAsync<Exception>(() => service.ProcessOrderAsync(order));
mockLogger.Verify(l => l.LogError(It.IsAny<string>(), It.IsAny<Exception>()), Times.Once);
}
[Fact]
public void OrderService_ThrowsArgumentNull_WhenDependencyIsNull()
{
Assert.Throws<ArgumentNullException>(() =>
new OrderService(null, new Mock<IEmailService>().Object, new Mock<ILogger>().Object));
}
}
Сравнение: ДО и ПОСЛЕ
| Аспект | ДО (Tight Coupling) | ПОСЛЕ (DI) |
|---|---|---|
| Тестируемость | ❌ Невозможно (нужна реальная БД) | ✅ Легко (используем mocks) |
| Гибкость | ❌ Жёстко привязано к SqlDatabase | ✅ Легко подменять реализации |
| Переиспользование | ❌ Сложно | ✅ Простая инъекция |
| Вложенность зависимостей | ❌ Все создаются внутри | ✅ Управляется контейнером |
| Читаемость | ⚠️ Сложная (много кода в методе) | ✅ Ясная (зависимости в конструкторе) |
| Изоляция | ❌ Всё смешано | ✅ Разделено по интерфейсам |
Преимущества DI
- Тестируемость — unit-тесты становятся простыми с mocks
- Гибкость — легко менять реализации без изменения кода
- Слабая связанность — классы зависят от интерфейсов
- SOLID принципы — автоматически соблюдаются
- Readability — понятно, какие зависимости нужны
- Масштабируемость — просто добавлять новые реализации
- Конфигурируемость — различные реализации для разных окружений
Практическое применение
// Development версия
if (env.IsDevelopment())
{
services.AddScoped<IEmailService, SmtpEmailService>();
services.AddScoped<ILogger, ConsoleLogger>();
}
else
{
services.AddScoped<IEmailService, SendGridEmailService>();
services.AddScoped<ILogger, FileLogger>();
}
// Production версия с кэшированием
if (env.IsProduction())
{
services.AddScoped<IDatabase, SqlDatabase>();
services.Decorate<IDatabase, CachedDatabaseDecorator>();
}
Выводы
✅ DI решает:
- Избавляет от тесной связанности (tight coupling)
- Делает код тестируемым
- Повышает переиспользование кода
- Помогает соблюдать SOLID принципы
- Облегчает конфигурацию для разных окружений
✅ Ключевые техники:
- Определение интерфейсов
- Внедрение через конструктор
- Регистрация в DI контейнере
- Использование mocks для тестирования