What is the SOLID principle? Explain each letter with examples.
Quick Answer
SOLID principles are: S-Single Responsibility (one reason to change), O-Open/Closed (open for extension, closed for modification), L-Liskov Substitution (subtypes must be substitutable), I-Interface Segregation (no forced dependencies), D-Dependency Inversion (depend on abstractions). These principles make code more maintainable, flexible, and testable.
Detailed Answer
SOLID is an acronym for five design principles that make software designs more understandable, flexible, and maintainable.
S - Single Responsibility Principle (SRP)
A class should have only one reason to change, meaning it should have only one job or responsibility.
Bad Example (Violates SRP):
// This class has multiple responsibilities
public class User
{
public string Name { get; set; }
public string Email { get; set; }
// Responsibility 1: User data validation
public bool ValidateEmail()
{
return Email.Contains("@");
}
// Responsibility 2: Database operations
public void SaveToDatabase()
{
// Database save logic
Console.WriteLine("Saving to database...");
}
// Responsibility 3: Email notifications
public void SendWelcomeEmail()
{
// Email sending logic
Console.WriteLine($"Sending email to {Email}");
}
// Responsibility 4: Report generation
public string GenerateReport()
{
return $"User Report: {Name}";
}
}
Good Example (Follows SRP):
// Each class has a single responsibility
public class User
{
public string Name { get; set; }
public string Email { get; set; }
}
public class UserValidator
{
public bool ValidateEmail(User user)
{
return !string.IsNullOrEmpty(user.Email) && user.Email.Contains("@");
}
public bool ValidateName(User user)
{
return !string.IsNullOrEmpty(user.Name);
}
}
public class UserRepository
{
public void Save(User user)
{
// Database save logic
Console.WriteLine($"Saving {user.Name} to database...");
}
public User GetById(int id)
{
// Database retrieval logic
return new User();
}
}
public class EmailService
{
public void SendWelcomeEmail(User user)
{
Console.WriteLine($"Sending welcome email to {user.Email}");
}
}
public class UserReportGenerator
{
public string GenerateReport(User user)
{
return $"User Report: {user.Name} ({user.Email})";
}
}
// Usage
User user = new User { Name = "John", Email = "john@example.com" };
UserValidator validator = new UserValidator();
if (validator.ValidateEmail(user))
{
UserRepository repo = new UserRepository();
repo.Save(user);
EmailService emailService = new EmailService();
emailService.SendWelcomeEmail(user);
}
O - Open/Closed Principle (OCP)
Software entities should be open for extension but closed for modification. You should be able to add new functionality without changing existing code.
Bad Example (Violates OCP):
public class PaymentProcessor
{
public void ProcessPayment(string paymentType, decimal amount)
{
if (paymentType == "CreditCard")
{
Console.WriteLine($"Processing credit card payment: ${amount}");
}
else if (paymentType == "PayPal")
{
Console.WriteLine($"Processing PayPal payment: ${amount}");
}
else if (paymentType == "Crypto")
{
// Need to modify existing code to add new payment type!
Console.WriteLine($"Processing crypto payment: ${amount}");
}
// Adding new payment type requires modifying this method
}
}
Good Example (Follows OCP):
// Abstraction - closed for modification
public interface IPaymentMethod
{
void ProcessPayment(decimal amount);
}
// Open for extension - add new payment methods without changing existing code
public class CreditCardPayment : IPaymentMethod
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing credit card payment: ${amount}");
}
}
public class PayPalPayment : IPaymentMethod
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing PayPal payment: ${amount}");
}
}
public class CryptoPayment : IPaymentMethod
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing crypto payment: ${amount}");
}
}
// Can add new payment methods like ApplePay without modifying existing code
public class ApplePayPayment : IPaymentMethod
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing Apple Pay payment: ${amount}");
}
}
// Processor doesn't need to change when adding new payment types
public class PaymentProcessor
{
public void Process(IPaymentMethod paymentMethod, decimal amount)
{
paymentMethod.ProcessPayment(amount);
}
}
// Usage
PaymentProcessor processor = new PaymentProcessor();
processor.Process(new CreditCardPayment(), 100);
processor.Process(new PayPalPayment(), 50);
processor.Process(new CryptoPayment(), 200);
processor.Process(new ApplePayPayment(), 75); // New payment type, no changes needed!
L - Liskov Substitution Principle (LSP)
Objects of a superclass should be replaceable with objects of its subclasses without breaking the application. Derived classes must be substitutable for their base classes.
Bad Example (Violates LSP):
public class Bird
{
public virtual void Fly()
{
Console.WriteLine("Flying...");
}
}
public class Sparrow : Bird
{
public override void Fly()
{
Console.WriteLine("Sparrow flying");
}
}
public class Penguin : Bird
{
public override void Fly()
{
// Penguins can't fly! This violates LSP
throw new NotImplementedException("Penguins can't fly!");
}
}
// Usage - breaks LSP
void MakeBirdFly(Bird bird)
{
bird.Fly(); // Throws exception if bird is a Penguin!
}
Good Example (Follows LSP):
// Better abstraction
public abstract class Bird
{
public abstract void Move();
}
public interface IFlyable
{
void Fly();
}
public interface ISwimmable
{
void Swim();
}
public class Sparrow : Bird, IFlyable
{
public override void Move()
{
Fly();
}
public void Fly()
{
Console.WriteLine("Sparrow flying");
}
}
public class Penguin : Bird, ISwimmable
{
public override void Move()
{
Swim();
}
public void Swim()
{
Console.WriteLine("Penguin swimming");
}
}
public class Duck : Bird, IFlyable, ISwimmable
{
public override void Move()
{
Fly();
}
public void Fly()
{
Console.WriteLine("Duck flying");
}
public void Swim()
{
Console.WriteLine("Duck swimming");
}
}
// Usage - respects LSP
void MakeBirdMove(Bird bird)
{
bird.Move(); // Works for all birds!
}
void MakeFlyableFly(IFlyable flyable)
{
flyable.Fly(); // Only called on birds that can fly
}
I - Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they don't use. Many specific interfaces are better than one general-purpose interface.
Bad Example (Violates ISP):
// Fat interface - forces implementations to implement methods they don't need
public interface IWorker
{
void Work();
void Eat();
void Sleep();
void GetPaid();
}
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 void GetPaid()
{
Console.WriteLine("Human getting paid");
}
}
public class RobotWorker : IWorker
{
public void Work()
{
Console.WriteLine("Robot working");
}
// Robots don't eat or sleep!
public void Eat()
{
throw new NotImplementedException(); // Forced to implement
}
public void Sleep()
{
throw new NotImplementedException(); // Forced to implement
}
public void GetPaid()
{
throw new NotImplementedException(); // Robots don't get paid
}
}
Good Example (Follows ISP):
// Segregated interfaces - clients only depend on what they need
public interface IWorkable
{
void Work();
}
public interface IEatable
{
void Eat();
}
public interface ISleepable
{
void Sleep();
}
public interface IPayable
{
void GetPaid();
}
public class HumanWorker : IWorkable, IEatable, ISleepable, IPayable
{
public void Work()
{
Console.WriteLine("Human working");
}
public void Eat()
{
Console.WriteLine("Human eating");
}
public void Sleep()
{
Console.WriteLine("Human sleeping");
}
public void GetPaid()
{
Console.WriteLine("Human getting paid");
}
}
public class RobotWorker : IWorkable
{
public void Work()
{
Console.WriteLine("Robot working");
}
// Only implements what it needs!
}
public class ContractWorker : IWorkable, IPayable
{
public void Work()
{
Console.WriteLine("Contractor working");
}
public void GetPaid()
{
Console.WriteLine("Contractor getting paid");
}
// Doesn't need Eat or Sleep
}
// Usage
void ManageWorkable(IWorkable worker)
{
worker.Work(); // Can work with any workable, don't care about other behaviors
}
D - Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
Bad Example (Violates DIP):
// Low-level module
public class EmailService
{
public void SendEmail(string message)
{
Console.WriteLine($"Sending email: {message}");
}
}
// High-level module depends on low-level module directly
public class UserService
{
private EmailService emailService; // Tight coupling!
public UserService()
{
emailService = new EmailService(); // Creates concrete instance
}
public void RegisterUser(string username)
{
// Registration logic
emailService.SendEmail($"Welcome {username}");
// If we want to use SMS instead, we need to modify this class!
}
}
Good Example (Follows DIP):
// Abstraction
public interface IMessageService
{
void SendMessage(string message);
}
// Low-level modules implement abstraction
public class EmailService : IMessageService
{
public void SendMessage(string message)
{
Console.WriteLine($"Sending email: {message}");
}
}
public class SmsService : IMessageService
{
public void SendMessage(string message)
{
Console.WriteLine($"Sending SMS: {message}");
}
}
public class PushNotificationService : IMessageService
{
public void SendMessage(string message)
{
Console.WriteLine($"Sending push notification: {message}");
}
}
// High-level module depends on abstraction
public class UserService
{
private readonly IMessageService messageService;
// Dependency injected through constructor
public UserService(IMessageService messageService)
{
this.messageService = messageService;
}
public void RegisterUser(string username)
{
// Registration logic
messageService.SendMessage($"Welcome {username}");
// Works with any IMessageService implementation!
}
}
// Usage with Dependency Injection
IMessageService emailService = new EmailService();
UserService userService1 = new UserService(emailService);
userService1.RegisterUser("John");
IMessageService smsService = new SmsService();
UserService userService2 = new UserService(smsService);
userService2.RegisterUser("Jane");
// Easy to switch implementations without modifying UserService
Complete Example Demonstrating All SOLID Principles:
// S - Single Responsibility
public class Order
{
public int OrderId { get; set; }
public List<OrderItem> Items { get; set; }
public decimal TotalAmount { get; set; }
}
// S - Single Responsibility for validation
public class OrderValidator
{
public bool Validate(Order order)
{
return order != null && order.Items?.Count > 0;
}
}
// O & D - Open for extension, depends on abstraction
public interface IOrderRepository
{
void Save(Order order);
}
public interface IPaymentProcessor
{
bool ProcessPayment(decimal amount);
}
public interface INotificationService
{
void Notify(string message);
}
// O - Closed for modification, open for extension
public class SqlOrderRepository : IOrderRepository
{
public void Save(Order order)
{
Console.WriteLine("Saving order to SQL database");
}
}
public class MongoOrderRepository : IOrderRepository
{
public void Save(Order order)
{
Console.WriteLine("Saving order to MongoDB");
}
}
// I - Interface Segregation - specific interfaces
public class StripePaymentProcessor : IPaymentProcessor
{
public bool ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing ${amount} via Stripe");
return true;
}
}
public class EmailNotificationService : INotificationService
{
public void Notify(string message)
{
Console.WriteLine($"Email notification: {message}");
}
}
// D - High-level module depends on abstractions
public class OrderService
{
private readonly IOrderRepository repository;
private readonly IPaymentProcessor paymentProcessor;
private readonly INotificationService notificationService;
private readonly OrderValidator validator;
// D - Dependencies injected
public OrderService(
IOrderRepository repository,
IPaymentProcessor paymentProcessor,
INotificationService notificationService)
{
this.repository = repository;
this.paymentProcessor = paymentProcessor;
this.notificationService = notificationService;
this.validator = new OrderValidator(); // S - Single responsibility
}
public bool PlaceOrder(Order order)
{
// S - Single responsibility for each step
if (!validator.Validate(order))
return false;
if (!paymentProcessor.ProcessPayment(order.TotalAmount))
return false;
repository.Save(order);
notificationService.Notify($"Order {order.OrderId} placed successfully");
return true;
}
}
// Usage - easy to test and extend
var orderService = new OrderService(
new SqlOrderRepository(),
new StripePaymentProcessor(),
new EmailNotificationService()
);
Order order = new Order
{
OrderId = 1,
Items = new List<OrderItem>(),
TotalAmount = 99.99m
};
orderService.PlaceOrder(order);
Benefits of SOLID:
- Easier to maintain and extend
- More testable code
- Reduced coupling
- Better code organization
- Easier to understand
- Facilitates refactoring