Single Responsibility Principle (SRP)
“A class should have one, and only one, reason to change.” — Robert C. Martin
The Single Responsibility Principle states that a class should be responsible for only one thing. If a class has multiple responsibilities, changes to one responsibility may break the others.
Understanding “Responsibility”
A “responsibility” is a reason to change. Ask yourself:
- “Who might request changes to this class?”
- “What different things could cause this class to change?”
If multiple stakeholders or concerns could cause changes, the class has too many responsibilities.
Violation Example
// BAD: Multiple responsibilities in one class
public class UserManager
{
// Responsibility 1: User management
public void RegisterUser(string email, string password) { /* ... */ }
public void LoginUser(string email, string password) { /* ... */ }
public void UpdateUserProfile(int userId, string name) { /* ... */ }
// Responsibility 2: Email handling
public void SendWelcomeEmail(string email) { /* ... */ }
public void SendPasswordResetEmail(string email) { /* ... */ }
// Responsibility 3: Database operations
public void SaveToDatabase(User user) { /* ... */ }
public void DeleteFromDatabase(int userId) { /* ... */ }
// Responsibility 4: Validation
public bool ValidateEmail(string email) { /* ... */ }
public bool ValidatePassword(string password) { /* ... */ }
// Responsibility 5: Logging
public void LogUserActivity(int userId, string activity) { /* ... */ }
}
// Problems:
// - Changes to email service affect this class
// - Changes to database schema affect this class
// - Changes to validation rules affect this class
// - Changes to logging affect this class
// - Hard to test - need to mock everything
// - Hard to reuse - email sending tied to user management
Applying SRP
Split responsibilities into separate, focused classes:
// Responsibility: Validation
public interface IUserValidator
{
ValidationResult ValidateEmail(string email);
ValidationResult ValidatePassword(string password);
ValidationResult ValidateRegistration(string email, string password);
}
public class UserValidator : IUserValidator
{
public ValidationResult ValidateEmail(string email)
{
var errors = new List<string>();
if (string.IsNullOrWhiteSpace(email))
errors.Add("Email is required");
else if (!email.Contains("@") || !email.Contains("."))
errors.Add("Invalid email format");
return new ValidationResult { IsValid = !errors.Any(), Errors = errors };
}
public ValidationResult ValidatePassword(string password)
{
var errors = new List<string>();
if (string.IsNullOrWhiteSpace(password))
errors.Add("Password is required");
if (password?.Length < 8)
errors.Add("Password must be at least 8 characters");
if (!password?.Any(char.IsDigit) ?? true)
errors.Add("Password must contain a digit");
return new ValidationResult { IsValid = !errors.Any(), Errors = errors };
}
public ValidationResult ValidateRegistration(string email, string password)
{
var emailResult = ValidateEmail(email);
var passwordResult = ValidatePassword(password);
return new ValidationResult
{
IsValid = emailResult.IsValid && passwordResult.IsValid,
Errors = emailResult.Errors.Concat(passwordResult.Errors).ToList()
};
}
}
// Responsibility: Data persistence
public interface IUserRepository
{
Task<User> GetByIdAsync(int id);
Task<User> GetByEmailAsync(string email);
Task AddAsync(User user);
Task UpdateAsync(User user);
Task DeleteAsync(int id);
}
public class UserRepository : IUserRepository
{
private readonly DbContext _context;
public UserRepository(DbContext context)
{
_context = context;
}
public async Task<User> GetByIdAsync(int id)
=> await _context.Users.FindAsync(id);
public async Task<User> GetByEmailAsync(string email)
=> await _context.Users.FirstOrDefaultAsync(u => u.Email == email);
public async Task AddAsync(User user)
{
await _context.Users.AddAsync(user);
await _context.SaveChangesAsync();
}
public async Task UpdateAsync(User user)
{
_context.Users.Update(user);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var user = await GetByIdAsync(id);
if (user != null)
{
_context.Users.Remove(user);
await _context.SaveChangesAsync();
}
}
}
// Responsibility: Email notifications
public interface IEmailService
{
Task SendWelcomeEmailAsync(string email, string name);
Task SendPasswordResetEmailAsync(string email, string resetToken);
}
public class EmailService : IEmailService
{
private readonly IEmailSender _sender;
private readonly IEmailTemplateEngine _templates;
public EmailService(IEmailSender sender, IEmailTemplateEngine templates)
{
_sender = sender;
_templates = templates;
}
public async Task SendWelcomeEmailAsync(string email, string name)
{
var body = await _templates.RenderAsync("Welcome", new { Name = name });
await _sender.SendAsync(email, "Welcome to Our Platform!", body);
}
public async Task SendPasswordResetEmailAsync(string email, string resetToken)
{
var body = await _templates.RenderAsync("PasswordReset", new { Token = resetToken });
await _sender.SendAsync(email, "Reset Your Password", body);
}
}
// Responsibility: User registration orchestration
public class UserRegistrationService
{
private readonly IUserValidator _validator;
private readonly IUserRepository _repository;
private readonly IPasswordHasher _passwordHasher;
private readonly IEmailService _emailService;
private readonly ILogger<UserRegistrationService> _logger;
public UserRegistrationService(
IUserValidator validator,
IUserRepository repository,
IPasswordHasher passwordHasher,
IEmailService emailService,
ILogger<UserRegistrationService> logger)
{
_validator = validator;
_repository = repository;
_passwordHasher = passwordHasher;
_emailService = emailService;
_logger = logger;
}
public async Task<RegistrationResult> RegisterAsync(string email, string password, string name)
{
// Validate
var validation = _validator.ValidateRegistration(email, password);
if (!validation.IsValid)
return RegistrationResult.Failed(validation.Errors);
// Check if user exists
var existingUser = await _repository.GetByEmailAsync(email);
if (existingUser != null)
return RegistrationResult.Failed("Email already registered");
// Create user
var user = new User
{
Email = email,
Name = name,
PasswordHash = _passwordHasher.Hash(password),
CreatedAt = DateTime.UtcNow
};
await _repository.AddAsync(user);
// Send welcome email (fire and forget, or use background job)
_ = _emailService.SendWelcomeEmailAsync(email, name);
_logger.LogInformation("User registered: {Email}", email);
return RegistrationResult.Success(user.Id);
}
}
Benefits of SRP
| Benefit | Description |
|---|---|
| Easier Testing | Test each class in isolation |
| Better Reuse | EmailService can be used anywhere, not just for users |
| Easier Changes | Changing email provider only affects EmailService |
| Clearer Code | Each class has a clear, focused purpose |
| Team Collaboration | Different developers can work on different components |
How to Identify SRP Violations
Ask these questions about your class:
-
Does it have multiple “actors” that could request changes?
- Business: validation rules
- IT: database structure
- Marketing: email templates
-
Does it mix different levels of abstraction?
- High-level: business logic
- Low-level: database queries, HTTP calls
-
Is the class hard to name?
- Names like
UserManager,OrderProcessor,DataHandleroften indicate multiple responsibilities
- Names like
-
Does it have many private methods?
- Often indicates hidden responsibilities that should be extracted
Interview Tips
Common Questions:
- “Explain SRP with an example”
- “How do you decide when to split a class?”
- “Can SRP lead to too many classes?”
Key Points:
- SRP is about reasons to change, not just “doing one thing”
- Related to cohesion - methods in a class should be related
- Yes, it can lead to more classes, but each is simpler
- Balance: don’t create a class for every method
Code Smell Indicators:
- Class names ending in “Manager”, “Processor”, “Handler”
- Classes with many dependencies (> 5-7)
- Classes with unrelated methods
- Long classes (> 200-300 lines is a warning sign)