Overview
SOLID principles are fundamental to writing professional C# code and are frequently discussed in technical interviews, especially for mid-level and senior positions. Understanding not just what they are, but how to apply them in real scenarios, is essential.
Core Concepts
Single Responsibility Principle (SRP)
A class should have only one reason to change. This means a class should have only one job or responsibility.
// Bad - multiple responsibilities
public class Report
{
public string Generate(List<Order> orders)
{
// Calculate totals
var total = orders.Sum(o => o.Total);
// Format report
var report = $"Total: {total:C}\n";
report += string.Join("\n", orders.Select(o => o.ToString()));
// Save to file
File.WriteAllText("report.txt", report);
// Send email
var smtp = new SmtpClient();
smtp.Send("admin@company.com", "Report", report);
return report;
}
}
// Good - single responsibilities
public class ReportGenerator
{
public ReportData Generate(List<Order> orders)
{
return new ReportData
{
Total = orders.Sum(o => o.Total),
Orders = orders
};
}
}
public class ReportFormatter
{
public string Format(ReportData data)
{
var report = $"Total: {data.Total:C}\n";
report += string.Join("\n", data.Orders.Select(o => o.ToString()));
return report;
}
}
public class ReportSaver
{
public void Save(string content, string filePath)
{
File.WriteAllText(filePath, content);
}
}
public class ReportEmailer
{
private readonly IEmailService _emailService;
public async Task SendAsync(string content, string recipient)
{
await _emailService.SendAsync(recipient, "Report", content);
}
}
Open/Closed Principle (OCP)
Classes should be open for extension but closed for modification. Use inheritance or composition to add functionality.
// Bad - must modify class to add new discount types
public class PriceCalculator
{
public decimal Calculate(Product product, string discountType)
{
if (discountType == "Percentage")
return product.Price * 0.9m;
else if (discountType == "Fixed")
return product.Price - 10;
else if (discountType == "BOGO") // Modifying existing code!
return product.Price * 0.5m;
return product.Price;
}
}
// Good - extend through abstraction
public interface IDiscountStrategy
{
decimal Apply(decimal price);
}
public class PercentageDiscount : IDiscountStrategy
{
private readonly decimal _percentage;
public PercentageDiscount(decimal percentage)
{
_percentage = percentage;
}
public decimal Apply(decimal price) => price * (1 - _percentage);
}
public class FixedDiscount : IDiscountStrategy
{
private readonly decimal _amount;
public FixedDiscount(decimal amount)
{
_amount = amount;
}
public decimal Apply(decimal price) => Math.Max(0, price - _amount);
}
public class BOGODiscount : IDiscountStrategy // New class, no modifications
{
public decimal Apply(decimal price) => price * 0.5m;
}
public class PriceCalculator
{
public decimal Calculate(Product product, IDiscountStrategy discount)
{
return discount.Apply(product.Price);
}
}
Liskov Substitution Principle (LSP)
Objects of a superclass should be replaceable with objects of its subclasses without breaking the application.
// Bad - violates LSP
public class Rectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public int GetArea() => Width * Height;
}
public class Square : Rectangle
{
public override int Width
{
get => base.Width;
set
{
base.Width = value;
base.Height = value; // Side effect!
}
}
public override int Height
{
get => base.Height;
set
{
base.Height = value;
base.Width = value; // Side effect!
}
}
}
// Problem:
void TestArea(Rectangle rect)
{
rect.Width = 5;
rect.Height = 4;
Assert.Equal(20, rect.GetArea()); // Fails for Square!
}
// Good - proper abstraction
public interface IShape
{
int GetArea();
}
public class Rectangle : IShape
{
public int Width { get; set; }
public int Height { get; set; }
public int GetArea() => Width * Height;
}
public class Square : IShape
{
public int Size { get; set; }
public int GetArea() => Size * Size;
}
Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they don't use.
// Bad - fat interface
public interface IWorker
{
void Work();
void Eat();
void Sleep();
}
public class Human : IWorker
{
public void Work() { /* ... */ }
public void Eat() { /* ... */ }
public void Sleep() { /* ... */ }
}
public class Robot : IWorker
{
public void Work() { /* ... */ }
public void Eat() { throw new NotImplementedException(); } // Forced to implement!
public void Sleep() { throw new NotImplementedException(); } // Forced to implement!
}
// Good - segregated interfaces
public interface IWorkable
{
void Work();
}
public interface IFeedable
{
void Eat();
}
public interface ISleepable
{
void Sleep();
}
public class Human : IWorkable, IFeedable, ISleepable
{
public void Work() { /* ... */ }
public void Eat() { /* ... */ }
public void Sleep() { /* ... */ }
}
public class Robot : IWorkable
{
public void Work() { /* ... */ } // Only implements what it needs
}
Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
// Bad - high-level depends on low-level
public class EmailService // Low-level
{
public void SendEmail(string to, string message)
{
// SMTP logic
}
}
public class UserService // High-level
{
private readonly EmailService _emailService = new EmailService(); // Direct dependency!
public void RegisterUser(User user)
{
// Save user
_emailService.SendEmail(user.Email, "Welcome!");
}
}
// Good - both depend on abstraction
public interface IMessageService // Abstraction
{
Task SendAsync(string to, string message);
}
public class EmailService : IMessageService // Low-level
{
public Task SendAsync(string to, string message)
{
// SMTP logic
return Task.CompletedTask;
}
}
public class SmsService : IMessageService // Another low-level implementation
{
public Task SendAsync(string to, string message)
{
// SMS logic
return Task.CompletedTask;
}
}
public class UserService // High-level
{
private readonly IMessageService _messageService;
public UserService(IMessageService messageService) // Depends on abstraction
{
_messageService = messageService;
}
public async Task RegisterUserAsync(User user)
{
// Save user
await _messageService.SendAsync(user.Email, "Welcome!");
}
}
Interview Tips
Tip 1: Use real-world examples when explaining SOLID. Don't just recite definitions - show code violations and fixes.
Tip 2: Know that SOLID principles sometimes conflict. Be ready to discuss trade-offs and when to pragmatically bend rules.
Tip 3: Connect SOLID to practical benefits: testability, maintainability, flexibility. Interviewers care about outcomes, not just theory.
Common Interview Questions
-
Explain SOLID principles with examples.
- See detailed explanations above. Focus on one principle at a time with before/after code.
-
How does SRP relate to microservices?
- SRP at class level → one reason to change. Microservices apply SRP at service level → one business capability per service.
-
When might you violate SOLID principles?
- Pragmatism: simple CRUD apps, prototypes, performance-critical code. Over-engineering is also a problem.
-
How does DIP enable testability?
- Depending on abstractions (interfaces) allows injecting mocks/fakes in tests instead of real implementations.
-
What's the relationship between OCP and the Strategy pattern?
- Strategy pattern is an implementation of OCP - add new strategies (extensions) without modifying existing code.
-
How do you identify SRP violations?
- Look for: multiple reasons to change, large classes (>300 lines), words like "And" in class names (UserAndEmailService).
-
Can you have too many interfaces (ISP)?
- Yes, over-segregation creates complexity. Balance: cohesive interfaces without forcing unnecessary dependencies.