What are SOLID Principles?
SOLID is an acronym for five object-oriented design principles that make software more understandable, flexible, and maintainable.
1. S - Single Responsibility Principle (SRP)
A class should have only one reason to change.
Bad Example (Violating SRP):
public class UserService { public void RegisterUser(string username, string email, string password) { // Validate user data if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(email)) throw new ArgumentException("Username and email are required"); if (!email.Contains("@")) throw new ArgumentException("Invalid email format"); // Hash password string hashedPassword = BCrypt.Net.BCrypt.HashPassword(password); // Save to database using (var connection = new SqlConnection("connection_string")) { connection.Open(); var command = new SqlCommand( "INSERT INTO Users (Username, Email, Password) VALUES (@username, @email, @password)", connection); command.Parameters.AddWithValue("@username", username); command.Parameters.AddWithValue("@email", email); command.Parameters.AddWithValue("@password", hashedPassword); command.ExecuteNonQuery(); } // Send welcome email var smtpClient = new SmtpClient("smtp.example.com"); var mailMessage = new MailMessage { From = new MailAddress("noreply@example.com"), Subject = "Welcome to our platform!", Body = "Thank you for registering!" }; mailMessage.To.Add(email); smtpClient.Send(mailMessage); // Log the registration File.WriteAllText("log.txt", $"User {username} registered at {DateTime.Now}"); } }
Good Example (Following SRP):
// User entity public class User { public string Username { get; set; } public string Email { get; set; } public string PasswordHash { get; set; } } // Validation responsibility public class UserValidator { public bool Validate(User user) { if (string.IsNullOrEmpty(user.Username) || string.IsNullOrEmpty(user.Email)) return false; return user.Email.Contains("@"); } } // Password handling responsibility public class PasswordHasher { public string HashPassword(string password) => BCrypt.Net.BCrypt.HashPassword(password); public bool VerifyPassword(string password, string hash) => BCrypt.Net.BCrypt.Verify(password, hash); } // Database operations responsibility public class UserRepository { public void Save(User user) { using (var connection = new SqlConnection("connection_string")) { connection.Open(); var command = new SqlCommand( "INSERT INTO Users (Username, Email, PasswordHash) VALUES (@username, @email, @password)", connection); command.Parameters.AddWithValue("@username", user.Username); command.Parameters.AddWithValue("@email", user.Email); command.Parameters.AddWithValue("@password", user.PasswordHash); command.ExecuteNonQuery(); } } } // Email service responsibility public class EmailService { public void SendWelcomeEmail(string email) { var smtpClient = new SmtpClient("smtp.example.com"); var mailMessage = new MailMessage { From = new MailAddress("noreply@example.com"), Subject = "Welcome to our platform!", Body = "Thank you for registering!" }; mailMessage.To.Add(email); smtpClient.Send(mailMessage); } } // Logging responsibility public class Logger { public void Log(string message) { File.WriteAllText("log.txt", $"{DateTime.Now}: {message}"); } } // Main service that coordinates everything public class UserService { private readonly UserValidator _validator; private readonly PasswordHasher _hasher; private readonly UserRepository _repository; private readonly EmailService _emailService; private readonly Logger _logger; public UserService() { _validator = new UserValidator(); _hasher = new PasswordHasher(); _repository = new UserRepository(); _emailService = new EmailService(); _logger = new Logger(); } public void RegisterUser(string username, string email, string password) { var user = new User { Username = username, Email = email }; if (!_validator.Validate(user)) throw new ArgumentException("Invalid user data"); user.PasswordHash = _hasher.HashPassword(password); _repository.Save(user); _emailService.SendWelcomeEmail(email); _logger.Log($"User {username} registered successfully"); } }
2. O - Open/Closed Principle (OCP)
Software entities should be open for extension but closed for modification.
Bad Example (Violating OCP):
public class PriceCalculator
{
public decimal CalculatePrice(string productType, decimal basePrice)
{
return productType switch
{
"Electronics" => basePrice * 1.2m, // 20% tax
"Clothing" => basePrice * 1.1m, // 10% tax
"Food" => basePrice * 1.05m, // 5% tax
_ => basePrice
};
// If we need to add a new product type, we have to modify this method
}
}
Good Example (Following OCP):
// Abstract base class
public abstract class Product
{
public string Name { get; set; }
public decimal BasePrice { get; set; }
public abstract decimal CalculatePrice();
}
// Concrete implementations
public class Electronics : Product
{
public override decimal CalculatePrice() => BasePrice * 1.2m;
}
public class Clothing : Product
{
public override decimal CalculatePrice() => BasePrice * 1.1m;
}
public class Food : Product
{
public override decimal CalculatePrice() => BasePrice * 1.05m;
}
// New product type can be added without modifying existing code
public class Books : Product
{
public override decimal CalculatePrice() => BasePrice * 1.08m; // 8% tax
}
// Price calculator that works with any Product type
public class PriceCalculator
{
public decimal CalculatePrice(Product product)
{
return product.CalculatePrice();
}
}
// Usage
var electronics = new Electronics { Name = "Laptop", BasePrice = 1000 };
var clothing = new Clothing { Name = "T-Shirt", BasePrice = 50 };
var calculator = new PriceCalculator();
Console.WriteLine($"Electronics price: {calculator.CalculatePrice(electronics)}");
Console.WriteLine($"Clothing price: {calculator.CalculatePrice(clothing)}");
3. L - Liskov Substitution Principle (LSP)
Objects of a superclass should be replaceable with objects of its subclasses without breaking the application.
Bad Example (Violating LSP):
public class Rectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public int Area => Width * Height;
}
public class Square : Rectangle
{
public override int Width
{
set { base.Width = value; base.Height = value; }
}
public override int Height
{
set { base.Width = value; base.Height = value; }
}
}
// This breaks LSP because Square changes the behavior of Rectangle
public class AreaCalculator
{
public void CalculateArea(Rectangle rectangle)
{
rectangle.Width = 5;
rectangle.Height = 4;
// For Rectangle, we expect area = 20
// For Square, we get area = 16 (not what we expect!)
Console.WriteLine($"Expected: 20, Actual: {rectangle.Area}");
}
}
Good Example (Following LSP):
// Base interface or abstract class
public abstract class Shape
{
public abstract int Area { get; }
}
// Concrete implementations
public class Rectangle : Shape
{
public int Width { get; set; }
public int Height { get; set; }
public override int Area => Width * Height;
}
public class Square : Shape
{
public int Side { get; set; }
public override int Area => Side * Side;
}
// Calculator that works with any Shape
public class AreaCalculator
{
public void CalculateArea(Shape shape)
{
if (shape is Rectangle rect)
{
rect.Width = 5;
rect.Height = 4;
}
else if (shape is Square sq)
{
sq.Side = 4;
}
Console.WriteLine($"Area: {shape.Area}");
}
}
// Even better - using polymorphism
public class BetterAreaCalculator
{
public void DisplayArea(Shape shape)
{
Console.WriteLine($"The area is: {shape.Area}");
}
}
4. I - Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they do not use.
Bad Example (Violating ISP):
public interface IWorker
{
void Work();
void Eat();
void Sleep();
}
public class HumanWorker : IWorker
{
public void Work() => Console.WriteLine("Human working...");
public void Eat() => Console.WriteLine("Human eating...");
public void Sleep() => Console.WriteLine("Human sleeping...");
}
public class RobotWorker : IWorker
{
public void Work() => Console.WriteLine("Robot working...");
// Robots don't eat or sleep!
public void Eat() => throw new NotImplementedException();
public void Sleep() => throw new NotImplementedException();
}
Good Example (Following ISP):
// Segregated interfaces
public interface IWorkable
{
void Work();
}
public interface IEatable
{
void Eat();
}
public interface ISleepable
{
void Sleep();
}
// Classes implement only what they need
public class HumanWorker : IWorkable, IEatable, ISleepable
{
public void Work() => Console.WriteLine("Human working...");
public void Eat() => Console.WriteLine("Human eating...");
public void Sleep() => Console.WriteLine("Human sleeping...");
}
public class RobotWorker : IWorkable
{
public void Work() => Console.WriteLine("Robot working...");
}
// Even more specific interfaces
public interface IAdvancedWorker : IWorkable, IEatable, ISleepable
{
void AttendMeeting();
}
public class Manager : IAdvancedWorker
{
public void Work() => Console.WriteLine("Manager working...");
public void Eat() => Console.WriteLine("Manager eating...");
public void Sleep() => Console.WriteLine("Manager sleeping...");
public void AttendMeeting() => Console.WriteLine("Manager in meeting...");
}
5. D - Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Bad Example (Violating DIP):
// High-level module
public class OrderService
{
// Directly depends on low-level module (tight coupling)
private readonly SqlServerDatabase _database;
public OrderService()
{
_database = new SqlServerDatabase(); // Hard dependency
}
public void SaveOrder(Order order)
{
_database.Save(order);
}
}
// Low-level module
public class SqlServerDatabase
{
public void Save(object data)
{
// SQL Server specific implementation
Console.WriteLine("Saving to SQL Server...");
}
}
Good Example (Following DIP):
// Abstraction
public interface IRepository
{
void Save(object data);
object GetById(int id);
}
// High-level module
public class OrderService
{
private readonly IRepository _repository; // Depends on abstraction
// Dependency injection
public OrderService(IRepository repository)
{
_repository = repository;
}
public void SaveOrder(Order order)
{
_repository.Save(order);
}
public Order GetOrder(int id)
{
return (Order)_repository.GetById(id);
}
}
// Low-level modules implementing the abstraction
public class SqlServerRepository : IRepository
{
public void Save(object data)
{
Console.WriteLine("Saving to SQL Server...");
}
public object GetById(int id)
{
Console.WriteLine("Getting from SQL Server...");
return new Order();
}
}
public class MongoDbRepository : IRepository
{
public void Save(object data)
{
Console.WriteLine("Saving to MongoDB...");
}
public object GetById(int id)
{
Console.WriteLine("Getting from MongoDB...");
return new Order();
}
}
// Usage with Dependency Injection
class Program
{
static void Main()
{
// We can easily switch between different repositories
IRepository repository = new SqlServerRepository();
// IRepository repository = new MongoDbRepository();
var orderService = new OrderService(repository);
orderService.SaveOrder(new Order());
}
}
Complete Example Applying All SOLID Principles:
// SRP: Separate interfaces for different responsibilities
public interface IOrderProcessor
{
ProcessResult Process(Order order);
}
public interface IPaymentProcessor
{
PaymentResult ProcessPayment(Order order);
}
public interface IShippingService
{
ShippingResult ScheduleShipping(Order order);
}
public interface INotificationService
{
void SendConfirmation(Order order);
}
// OCP: Open for extension through interfaces
// LSP: All implementations can be substituted
public class CreditCardPaymentProcessor : IPaymentProcessor
{
public PaymentResult ProcessPayment(Order order)
{
// Process credit card payment
return new PaymentResult { Success = true };
}
}
public class PayPalPaymentProcessor : IPaymentProcessor
{
public PaymentResult ProcessPayment(Order order)
{
// Process PayPal payment
return new PaymentResult { Success = true };
}
}
// ISP: Segregated interfaces
// DIP: Depending on abstractions
public class OrderProcessor : IOrderProcessor
{
private readonly IPaymentProcessor _paymentProcessor;
private readonly IShippingService _shippingService;
private readonly INotificationService _notificationService;
private readonly ILogger _logger;
// Constructor injection - DIP in action
public OrderProcessor(
IPaymentProcessor paymentProcessor,
IShippingService shippingService,
INotificationService notificationService,
ILogger logger)
{
_paymentProcessor = paymentProcessor;
_shippingService = shippingService;
_notificationService = notificationService;
_logger = logger;
}
public ProcessResult Process(Order order)
{
try
{
// Process payment
var paymentResult = _paymentProcessor.ProcessPayment(order);
if (!paymentResult.Success)
return ProcessResult.Failure("Payment failed");
// Schedule shipping
var shippingResult = _shippingService.ScheduleShipping(order);
if (!shippingResult.Success)
return ProcessResult.Failure("Shipping scheduling failed");
// Send notification
_notificationService.SendConfirmation(order);
// Log success
_logger.Log($"Order {order.Id} processed successfully");
return ProcessResult.Success();
}
catch (Exception ex)
{
_logger.Log($"Error processing order {order.Id}: {ex.Message}");
return ProcessResult.Failure($"Processing error: {ex.Message}");
}
}
}
// Supporting classes
public class Order
{
public int Id { get; set; }
public decimal TotalAmount { get; set; }
public string CustomerEmail { get; set; }
}
public class ProcessResult
{
public bool Success { get; set; }
public string Message { get; set; }
public static ProcessResult Success() => new ProcessResult { Success = true, Message = "Success" };
public static ProcessResult Failure(string message) => new ProcessResult { Success = false, Message = message };
}
public class PaymentResult
{
public bool Success { get; set; }
public string TransactionId { get; set; }
}
public class ShippingResult
{
public bool Success { get; set; }
public string TrackingNumber { get; set; }
}
public interface ILogger
{
void Log(string message);
}
public class ConsoleLogger : ILogger
{
public void Log(string message) => Console.WriteLine($"[LOG] {DateTime.Now}: {message}");
}
Key Benefits of SOLID Principles:
Maintainability: Easier to understand and modify
Testability: Classes are easier to test in isolation
Flexibility: Easy to extend with new functionality
Reusability: Components can be reused across the system
Reduced Coupling: Components are loosely connected
By following SOLID principles, you create code that's more robust, scalable, and easier to work with as your application grows.
Comments
Post a Comment