← Назад к вопросам

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");
}

}

Задание:

  1. Выявите нарушения принципов SOLID
  2. Рефакторите код с использованием Dependency Injection
  3. Создайте необходимые интерфейсы
  4. Покажите, как зарегистрировать зависимости в ASP.NET Core DI-контейнере

Критерии оценки:

  • Выделение интерфейсов (IDatabase, IEmailService, ILogger)
  • Внедрение зависимостей через конструктор
  • Корректная регистрация в DI
  • Объяснение преимуществ (тестируемость, гибкость)

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

Анализ проблем: Нарушения SOLID

Код содержит множество нарушений:

  1. Single Responsibility (SRP) — OrderService отвечает за заказы, БД, email и логирование
  2. Dependency Inversion (DIP) — OrderService зависит от конкретных классов, а не интерфейсов
  3. Open/Closed (OCP) — нельзя использовать другую БД или email-сервис без изменения OrderService
  4. Interface Segregation (ISP) — нет интерфейсов для работы с зависимостями
  5. 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

  1. Тестируемость — unit-тесты становятся простыми с mocks
  2. Гибкость — легко менять реализации без изменения кода
  3. Слабая связанность — классы зависят от интерфейсов
  4. SOLID принципы — автоматически соблюдаются
  5. Readability — понятно, какие зависимости нужны
  6. Масштабируемость — просто добавлять новые реализации
  7. Конфигурируемость — различные реализации для разных окружений

Практическое применение

// 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 решает:

  1. Избавляет от тесной связанности (tight coupling)
  2. Делает код тестируемым
  3. Повышает переиспользование кода
  4. Помогает соблюдать SOLID принципы
  5. Облегчает конфигурацию для разных окружений

Ключевые техники:

  • Определение интерфейсов
  • Внедрение через конструктор
  • Регистрация в DI контейнере
  • Использование mocks для тестирования
Dependency Injection: Рефакторинг тесно связанного кода | PrepBro