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

Refactoring: Устранение God Class

1.8 Middle🔥 191 комментариев
#Основы C# и .NET

Условие

Дан класс UserManager, который делает слишком много:

public class UserManager { private readonly DbContext _db;

public User GetById(int id) { /* DB query */ }
public void Save(User user) { /* DB save */ }
public bool ValidatePassword(string password) { /* validation */ }
public string HashPassword(string password) { /* hashing */ }
public void SendWelcomeEmail(User user) { /* email */ }
public void SendPasswordResetEmail(User user) { /* email */ }
public bool Authenticate(string email, string password) { /* auth */ }
public string GenerateJwtToken(User user) { /* JWT */ }
public void LogUserActivity(User user, string action) { /* logging */ }
public UserStatistics CalculateStatistics(int userId) { /* stats */ }

}

Задание:

  1. Выявите нарушения Single Responsibility Principle
  2. Разбейте класс на несколько с чёткими ответственностями
  3. Определите интерфейсы для каждого класса
  4. Покажите как они будут взаимодействовать

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

  • Логичное разделение ответственностей
  • Правильное выделение интерфейсов
  • Соблюдение SOLID принципов

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

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

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

Решение: Refactoring God Class - UserManager

Анализ проблемы

Класс UserManager нарушает принцип Single Responsibility Principle (SRP). Он отвечает за 10+ различных обязанностей:

  1. Data Access (DB операции)
  2. Password Validation
  3. Password Hashing
  4. Email отправка
  5. Authentication
  6. JWT Token генерация
  7. Logging активности
  8. Statistics вычисление

Это делает код хрупким, сложным для тестирования и модификации.

Нарушения SOLID принципов

Single Responsibility: НАРУШЕНИЕ ✗

  • Класс имеет более 10 причин для изменения
  • Password logic, Email logic, Auth logic, DB logic — 4 разные причины

Open/Closed: НАРУШЕНИЕ ✗

  • Чтобы добавить новый способ авторизации (OAuth, SAML) — меняем класс
  • Чтобы добавить новый способ отправки (SMS вместо Email) — меняем класс

Dependency Inversion: НАРУШЕНИЕ ✗

  • DbContext жёстко встроен в класс
  • Невозможно подменить реализацию на mock для тестов

Решение: Разбиение на специализированные сервисы

1. Интерфейсы для каждой ответственности

// Data Access
public interface IUserRepository
{
    Task<User> GetByIdAsync(int id);
    Task<User> GetByEmailAsync(string email);
    Task SaveAsync(User user);
    Task DeleteAsync(User user);
}

// Password operations
public interface IPasswordService
{
    bool ValidatePassword(string password);
    string HashPassword(string password);
    bool VerifyPassword(string plainPassword, string hashedPassword);
}

// Email operations
public interface IEmailService
{
    Task SendWelcomeEmailAsync(User user);
    Task SendPasswordResetEmailAsync(User user, string resetToken);
}

// Authentication
public interface IAuthenticationService
{
    Task<AuthResult> AuthenticateAsync(string email, string password);
    User ValidateToken(string token);
}

// JWT Token operations
public interface IJwtTokenService
{
    string GenerateAccessToken(User user);
    string GenerateRefreshToken(User user);
    ClaimsPrincipal ValidateToken(string token);
}

// Activity logging
public interface IActivityLogger
{
    Task LogUserActivityAsync(User user, string action);
}

// Statistics
public interface IUserStatisticsService
{
    Task<UserStatistics> CalculateStatisticsAsync(int userId);
}

2. Специализированные классы-реализации

// Repository для data access
public class UserRepository : IUserRepository
{
    private readonly DbContext _db;
    
    public UserRepository(DbContext db)
    {
        _db = db;
    }
    
    public async Task<User> GetByIdAsync(int id)
    {
        return await _db.Users.FindAsync(id);
    }
    
    public async Task<User> GetByEmailAsync(string email)
    {
        return await _db.Users.FirstOrDefaultAsync(u => u.Email == email);
    }
    
    public async Task SaveAsync(User user)
    {
        if (user.Id == 0)
            _db.Users.Add(user);
        else
            _db.Users.Update(user);
        
        await _db.SaveChangesAsync();
    }
    
    public async Task DeleteAsync(User user)
    {
        _db.Users.Remove(user);
        await _db.SaveChangesAsync();
    }
}

// Service для работы с паролями
public class PasswordService : IPasswordService
{
    public bool ValidatePassword(string password)
    {
        // Проверка требований: минимум 8 символов, заглавная, цифра
        if (string.IsNullOrWhiteSpace(password))
            return false;
        
        if (password.Length < 8)
            return false;
        
        if (!password.Any(char.IsUpper))
            return false;
        
        if (!password.Any(char.IsDigit))
            return false;
        
        return true;
    }
    
    public string HashPassword(string password)
    {
        // Используем bcrypt
        return BCrypt.Net.BCrypt.HashPassword(password);
    }
    
    public bool VerifyPassword(string plainPassword, string hashedPassword)
    {
        return BCrypt.Net.BCrypt.Verify(plainPassword, hashedPassword);
    }
}

// Service для отправки email
public class EmailService : IEmailService
{
    private readonly IEmailProvider _emailProvider;  // Может быть SendGrid, AWS SES и т.д.
    
    public EmailService(IEmailProvider emailProvider)
    {
        _emailProvider = emailProvider;
    }
    
    public async Task SendWelcomeEmailAsync(User user)
    {
        var message = new EmailMessage
        {
            To = user.Email,
            Subject = "Welcome to our service!",
            Body = $"Hello {user.Name}, welcome aboard!"
        };
        
        await _emailProvider.SendAsync(message);
    }
    
    public async Task SendPasswordResetEmailAsync(User user, string resetToken)
    {
        var resetLink = $"https://example.com/reset-password?token={resetToken}";
        var message = new EmailMessage
        {
            To = user.Email,
            Subject = "Reset your password",
            Body = $"Click here to reset: {resetLink}"
        };
        
        await _emailProvider.SendAsync(message);
    }
}

// Service для JWT токенов
public class JwtTokenService : IJwtTokenService
{
    private readonly IOptions<JwtSettings> _jwtSettings;
    
    public JwtTokenService(IOptions<JwtSettings> jwtSettings)
    {
        _jwtSettings = jwtSettings;
    }
    
    public string GenerateAccessToken(User user)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(_jwtSettings.Value.Secret);
        
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new[]
            {
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                new Claim(ClaimTypes.Email, user.Email),
                new Claim("role", user.Role)
            }),
            Expires = DateTime.UtcNow.AddHours(1),
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
        };
        
        var token = tokenHandler.CreateToken(tokenDescriptor);
        return tokenHandler.WriteToken(token);
    }
    
    public string GenerateRefreshToken(User user)
    {
        // Обычно это случайная строка, которая сохраняется в БД
        return Convert.ToBase64String(System.Security.Cryptography.RandomNumberGenerator.GetBytes(64));
    }
    
    public ClaimsPrincipal ValidateToken(string token)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(_jwtSettings.Value.Secret);
        
        try
        {
            var principal = tokenHandler.ValidateToken(token, new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(key),
                ValidateIssuer = false,
                ValidateAudience = false,
                ClockSkew = TimeSpan.Zero
            }, out SecurityToken validatedToken);
            
            return principal;
        }
        catch
        {
            return null;
        }
    }
}

// Service для аутентификации
public class AuthenticationService : IAuthenticationService
{
    private readonly IUserRepository _userRepository;
    private readonly IPasswordService _passwordService;
    private readonly IJwtTokenService _jwtTokenService;
    private readonly IActivityLogger _activityLogger;
    
    public AuthenticationService(
        IUserRepository userRepository,
        IPasswordService passwordService,
        IJwtTokenService jwtTokenService,
        IActivityLogger activityLogger)
    {
        _userRepository = userRepository;
        _passwordService = passwordService;
        _jwtTokenService = jwtTokenService;
        _activityLogger = activityLogger;
    }
    
    public async Task<AuthResult> AuthenticateAsync(string email, string password)
    {
        var user = await _userRepository.GetByEmailAsync(email);
        
        if (user == null || !_passwordService.VerifyPassword(password, user.PasswordHash))
        {
            return new AuthResult { Success = false, Message = "Invalid credentials" };
        }
        
        await _activityLogger.LogUserActivityAsync(user, "Login");
        
        var accessToken = _jwtTokenService.GenerateAccessToken(user);
        var refreshToken = _jwtTokenService.GenerateRefreshToken(user);
        
        return new AuthResult
        {
            Success = true,
            AccessToken = accessToken,
            RefreshToken = refreshToken
        };
    }
    
    public User ValidateToken(string token)
    {
        var principal = _jwtTokenService.ValidateToken(token);
        // Извлекаем user ID из claims и возвращаем пользователя
        return null; // упрощено
    }
}

// Service для логирования активности
public class ActivityLogger : IActivityLogger
{
    private readonly DbContext _db;
    
    public ActivityLogger(DbContext db)
    {
        _db = db;
    }
    
    public async Task LogUserActivityAsync(User user, string action)
    {
        var log = new UserActivityLog
        {
            UserId = user.Id,
            Action = action,
            Timestamp = DateTime.UtcNow
        };
        
        _db.UserActivityLogs.Add(log);
        await _db.SaveChangesAsync();
    }
}

// Service для статистики
public class UserStatisticsService : IUserStatisticsService
{
    private readonly DbContext _db;
    
    public UserStatisticsService(DbContext db)
    {
        _db = db;
    }
    
    public async Task<UserStatistics> CalculateStatisticsAsync(int userId)
    {
        var stats = new UserStatistics
        {
            LoginCount = await _db.UserActivityLogs
                .Where(l => l.UserId == userId && l.Action == "Login")
                .CountAsync(),
            LastLogin = await _db.UserActivityLogs
                .Where(l => l.UserId == userId && l.Action == "Login")
                .OrderByDescending(l => l.Timestamp)
                .Select(l => l.Timestamp)
                .FirstOrDefaultAsync()
        };
        
        return stats;
    }
}

3. Facade или Application Service (опционально)

Если нужен unified interface для клиентов:

public interface IUserService
{
    Task<AuthResult> RegisterAsync(string email, string password, string name);
    Task<AuthResult> LoginAsync(string email, string password);
    Task<UserProfileDto> GetProfileAsync(int userId);
}

public class UserService : IUserService
{
    private readonly IUserRepository _userRepository;
    private readonly IPasswordService _passwordService;
    private readonly IAuthenticationService _authenticationService;
    private readonly IEmailService _emailService;
    
    public UserService(
        IUserRepository userRepository,
        IPasswordService passwordService,
        IAuthenticationService authenticationService,
        IEmailService emailService)
    {
        _userRepository = userRepository;
        _passwordService = passwordService;
        _authenticationService = authenticationService;
        _emailService = emailService;
    }
    
    public async Task<AuthResult> RegisterAsync(string email, string password, string name)
    {
        // Валидация
        if (!_passwordService.ValidatePassword(password))
            return new AuthResult { Success = false, Message = "Password too weak" };
        
        // Создание пользователя
        var user = new User
        {
            Email = email,
            Name = name,
            PasswordHash = _passwordService.HashPassword(password),
            CreatedAt = DateTime.UtcNow
        };
        
        await _userRepository.SaveAsync(user);
        await _emailService.SendWelcomeEmailAsync(user);
        
        return new AuthResult { Success = true, Message = "User registered" };
    }
    
    public async Task<AuthResult> LoginAsync(string email, string password)
    {
        return await _authenticationService.AuthenticateAsync(email, password);
    }
    
    public async Task<UserProfileDto> GetProfileAsync(int userId)
    {
        var user = await _userRepository.GetByIdAsync(userId);
        // Маппим в DTO
        return new UserProfileDto { /* ... */ };
    }
}

4. Dependency Injection (Startup/Program.cs)

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Register repositories
        services.AddScoped<IUserRepository, UserRepository>();
        
        // Register services
        services.AddScoped<IPasswordService, PasswordService>();
        services.AddScoped<IEmailService, EmailService>();
        services.AddScoped<IJwtTokenService, JwtTokenService>();
        services.AddScoped<IAuthenticationService, AuthenticationService>();
        services.AddScoped<IActivityLogger, ActivityLogger>();
        services.AddScoped<IUserStatisticsService, UserStatisticsService>();
        
        // Facade service
        services.AddScoped<IUserService, UserService>();
        
        // External dependencies
        services.AddScoped<IEmailProvider, SendGridEmailProvider>();
        
        // DbContext
        services.AddDbContext<DbContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
        
        services.AddControllers();
    }
}

5. Использование в контроллере

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly IUserService _userService;
    
    public AuthController(IUserService userService)
    {
        _userService = userService;
    }
    
    [HttpPost("register")]
    public async Task<IActionResult> Register([FromBody] RegisterRequest request)
    {
        var result = await _userService.RegisterAsync(request.Email, request.Password, request.Name);
        
        if (!result.Success)
            return BadRequest(result.Message);
        
        return Ok(new { message = "Registration successful" });
    }
    
    [HttpPost("login")]
    public async Task<IActionResult> Login([FromBody] LoginRequest request)
    {
        var result = await _userService.LoginAsync(request.Email, request.Password);
        
        if (!result.Success)
            return Unauthorized(result.Message);
        
        return Ok(new { accessToken = result.AccessToken, refreshToken = result.RefreshToken });
    }
}

6. Unit тесты (демонстрация, почему это лучше)

[TestClass]
public class AuthenticationServiceTests
{
    private Mock<IUserRepository> _userRepositoryMock;
    private Mock<IPasswordService> _passwordServiceMock;
    private Mock<IJwtTokenService> _jwtTokenServiceMock;
    private Mock<IActivityLogger> _activityLoggerMock;
    private IAuthenticationService _authenticationService;
    
    [TestInitialize]
    public void Setup()
    {
        _userRepositoryMock = new Mock<IUserRepository>();
        _passwordServiceMock = new Mock<IPasswordService>();
        _jwtTokenServiceMock = new Mock<IJwtTokenService>();
        _activityLoggerMock = new Mock<IActivityLogger>();
        
        _authenticationService = new AuthenticationService(
            _userRepositoryMock.Object,
            _passwordServiceMock.Object,
            _jwtTokenServiceMock.Object,
            _activityLoggerMock.Object);
    }
    
    [TestMethod]
    public async Task Authenticate_WithValidCredentials_ReturnsAccessToken()
    {
        // Arrange
        var user = new User { Id = 1, Email = "test@example.com", PasswordHash = "hashed" };
        _userRepositoryMock.Setup(r => r.GetByEmailAsync("test@example.com"))
            .ReturnsAsync(user);
        _passwordServiceMock.Setup(p => p.VerifyPassword("password123", "hashed"))
            .Returns(true);
        _jwtTokenServiceMock.Setup(j => j.GenerateAccessToken(user))
            .Returns("jwt_token");
        
        // Act
        var result = await _authenticationService.AuthenticateAsync("test@example.com", "password123");
        
        // Assert
        Assert.IsTrue(result.Success);
        Assert.AreEqual("jwt_token", result.AccessToken);
        _activityLoggerMock.Verify(l => l.LogUserActivityAsync(user, "Login"), Times.Once);
    }
}

Преимущества этого подхода

АспектБыло (God Class)Стало (SRP)
ТестируемостьСложно, нужны реальные БД и Email провайдерыЛегко, используем mocks
МодификацияИзменение в одной фиче может сломать другуюНезависимые модули, изменения локальны
ПереиспользованиеНельзя использовать PasswordService отдельноКаждый сервис переиспользуется
МасштабируемостьСложно добавить OAuth или SAMLПросто создаём новый AuthenticationProvider
Maintainability~500 строк в одном классе~50-80 строк на каждый сервис
ПонятностьСложно ориентироваться в кодеСразу понимаешь, за что отвечает класс

Итоговая архитектура

Controller
    ↓
UserService (Facade) ← координирует остальные
    ├─→ IUserRepository (Data Access)
    ├─→ IPasswordService (Business Logic: Password)
    ├─→ IAuthenticationService (Business Logic: Auth)
    ├─→ IEmailService (External: Email)
    ├─→ IJwtTokenService (Business Logic: Tokens)
    ├─→ IActivityLogger (Cross-cutting: Logging)
    └─→ IUserStatisticsService (Reporting)

Каждый интерфейс можно заменить на альтернативную реализацию без изменения остального кода.

Refactoring: Устранение God Class | PrepBro