← Назад к вопросам
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 */ }
}
Задание:
- Выявите нарушения Single Responsibility Principle
- Разбейте класс на несколько с чёткими ответственностями
- Определите интерфейсы для каждого класса
- Покажите как они будут взаимодействовать
Критерии оценки:
- Логичное разделение ответственностей
- Правильное выделение интерфейсов
- Соблюдение SOLID принципов
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение: Refactoring God Class - UserManager
Анализ проблемы
Класс UserManager нарушает принцип Single Responsibility Principle (SRP). Он отвечает за 10+ различных обязанностей:
- Data Access (DB операции)
- Password Validation
- Password Hashing
- Email отправка
- Authentication
- JWT Token генерация
- Logging активности
- 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)
Каждый интерфейс можно заменить на альтернативную реализацию без изменения остального кода.