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):

csharp
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):

csharp
// 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):

csharp
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):

csharp
// 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):

csharp
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):

csharp
// 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):

csharp
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):

csharp
// 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):

csharp
// 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):

csharp
// 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:

csharp
// 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:

  1. Maintainability: Easier to understand and modify

  2. Testability: Classes are easier to test in isolation

  3. Flexibility: Easy to extend with new functionality

  4. Reusability: Components can be reused across the system

  5. 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

Popular posts from this blog

.NET Core Interview Questions and Answers for 10+ Years Experienced Professionals

.NET Core Senior Interview Q&A