SOLID Principles for LLD - IndianTechnoEra
Latest update Android YouTube

SOLID Principles for LLD

Chapter 2: SOLID Principles - The Foundation of Clean Code

Series: Low Level Design for .NET Developers | Previous: Chapter 1: OOP Deep Dive | Level: Intermediate to Advanced


📖 Introduction

The SOLID principles are the cornerstone of object-oriented design. Introduced by Robert C. Martin (Uncle Bob), these five principles guide developers toward creating software that is maintainable, scalable, testable, and resilient to change.

In this chapter, we'll explore each principle in depth with:

  • Real-world analogies to understand the concept
  • ❌ Violations showing what NOT to do
  • ✅ Correct implementations with .NET/C# code
  • Practical scenarios where each principle applies
  • Common pitfalls and how to avoid them

By the end of this chapter, you'll be able to look at any code and identify SOLID violations, and more importantly, know how to fix them.


The SOLID Principles at a Glance

Principle Definition One-Liner
Single Responsibility A class should have only one reason to change One job, one class
Open/Closed Open for extension, closed for modification Add features without changing existing code
Liskov Substitution Derived classes must be substitutable for base classes Subtypes must behave like their base types
Interface Segregation Clients should not be forced to depend on methods they don't use Keep interfaces small and focused
Dependency Inversion Depend on abstractions, not concretions Code to interfaces, not implementations

1. Single Responsibility Principle (SRP)

1.1 Understanding SRP

Definition: A class should have only one reason to change. This means a class should have exactly one responsibility or job.

Real-World Analogy: Think of a restaurant. One person takes orders, another cooks, another handles payments, and another cleans tables. If one person did everything, any change (new menu, new payment system, new cleaning procedure) would affect that one person. By separating responsibilities, each person can change independently.

1.2 Why SRP Matters

When a class has multiple responsibilities:

  • Changes to one responsibility affect other responsibilities
  • Harder to understand and maintain
  • Difficult to test
  • More likely to introduce bugs when modifying
  • Code reuse becomes impossible

1.3 ❌ Violating SRP

Here's a classic example - an Invoice class doing too much:

public class Invoice
{
    public int Id { get; set; }
    public string CustomerName { get; set; }
    public string CustomerEmail { get; set; }
    public List<InvoiceItem> Items { get; set; }
    public decimal TaxRate { get; set; }
    
    // Responsibility 1: Business Logic - Calculate totals
    public decimal CalculateSubtotal()
    {
        return Items.Sum(i => i.Quantity * i.UnitPrice);
    }
    
    public decimal CalculateTax()
    {
        return CalculateSubtotal() * TaxRate;
    }
    
    public decimal CalculateTotal()
    {
        return CalculateSubtotal() + CalculateTax();
    }
    
    // Responsibility 2: Persistence - Save to database
    public void SaveToDatabase()
    {
        // Database connection logic
        // SQL INSERT command
        Console.WriteLine($"Saving invoice {Id} to database");
    }
    
    // Responsibility 3: File Handling - Save to file
    public void SaveToFile(string filePath)
    {
        // File I/O logic
        File.WriteAllText(filePath, this.ToString());
        Console.WriteLine($"Saving invoice {Id} to file");
    }
    
    // Responsibility 4: Notification - Send email
    public void SendEmail()
    {
        // SMTP logic
        Console.WriteLine($"Sending invoice {Id} email to {CustomerEmail}");
    }
    
    // Responsibility 5: Formatting - Generate PDF
    public byte[] GeneratePdf()
    {
        // PDF generation logic
        Console.WriteLine($"Generating PDF for invoice {Id}");
        return new byte[0];
    }
}
// This class has 5 reasons to change:
// 1. Tax calculation rules change
// 2. Database schema changes
// 3. File format changes
// 4. Email template changes
// 5. PDF format changes

1.4 ✅ Following SRP - Separation of Concerns

Let's split the Invoice class into separate classes, each with a single responsibility:

// Domain Model - Represents business data
public class Invoice
{
    public int Id { get; set; }
    public string CustomerName { get; set; }
    public string CustomerEmail { get; set; }
    public List<InvoiceItem> Items { get; set; }
    public decimal TaxRate { get; set; }
    public DateTime CreatedDate { get; set; }
    public bool IsPaid { get; private set; }
    
    public Invoice()
    {
        Items = new List<InvoiceItem>();
        CreatedDate = DateTime.Now;
    }
    
    public void MarkAsPaid()
    {
        if (IsPaid)
            throw new InvalidOperationException("Invoice already paid");
        IsPaid = true;
    }
}

// Responsibility 1: Business Logic / Calculations
public class InvoiceCalculator
{
    public decimal CalculateSubtotal(Invoice invoice)
    {
        return invoice.Items.Sum(i => i.Quantity * i.UnitPrice);
    }
    
    public decimal CalculateTax(Invoice invoice)
    {
        return CalculateSubtotal(invoice) * invoice.TaxRate;
    }
    
    public decimal CalculateTotal(Invoice invoice)
    {
        return CalculateSubtotal(invoice) + CalculateTax(invoice);
    }
}

// Responsibility 2: Persistence - Database operations
public class InvoiceRepository
{
    private readonly ApplicationDbContext _context;
    
    public InvoiceRepository(ApplicationDbContext context)
    {
        _context = context;
    }
    
    public async Task SaveAsync(Invoice invoice)
    {
        await _context.Invoices.AddAsync(invoice);
        await _context.SaveChangesAsync();
    }
    
    public async Task<Invoice> GetByIdAsync(int id)
    {
        return await _context.Invoices.FindAsync(id);
    }
    
    public async Task<List<Invoice>> GetByCustomerAsync(string customerName)
    {
        return await _context.Invoices
            .Where(i => i.CustomerName == customerName)
            .ToListAsync();
    }
}

// Responsibility 3: File Export
public class InvoiceFileExporter
{
    public void SaveToFile(Invoice invoice, string filePath)
    {
        var content = FormatInvoice(invoice);
        File.WriteAllText(filePath, content);
    }
    
    private string FormatInvoice(Invoice invoice)
    {
        return $"Invoice #{invoice.Id}\n" +
               $"Customer: {invoice.CustomerName}\n" +
               $"Date: {invoice.CreatedDate}\n" +
               $"Total: {new InvoiceCalculator().CalculateTotal(invoice):C}";
    }
}

// Responsibility 4: PDF Generation
public class InvoicePdfGenerator
{
    public byte[] GeneratePdf(Invoice invoice)
    {
        // Use a PDF library like iTextSharp or PdfSharp
        Console.WriteLine($"Generating PDF for invoice {invoice.Id}");
        return new byte[0];
    }
}

// Responsibility 5: Email Notification
public class InvoiceEmailService
{
    private readonly IEmailSender _emailSender;
    
    public InvoiceEmailService(IEmailSender emailSender)
    {
        _emailSender = emailSender;
    }
    
    public async Task SendInvoiceEmailAsync(Invoice invoice)
    {
        var subject = $"Your Invoice #{invoice.Id}";
        var body = $"Dear {invoice.CustomerName},\n\n" +
                   $"Thank you for your business. Your invoice total is " +
                   $"{new InvoiceCalculator().CalculateTotal(invoice):C}\n\n" +
                   $"Regards,\nCompany Name";
        
        await _emailSender.SendEmailAsync(invoice.CustomerEmail, subject, body);
    }
}

// Responsibility 6: Logging (Cross-cutting concern)
public class InvoiceLogger
{
    private readonly ILogger<InvoiceLogger> _logger;
    
    public InvoiceLogger(ILogger<InvoiceLogger> logger)
    {
        _logger = logger;
    }
    
    public void LogInvoiceCreation(Invoice invoice)
    {
        _logger.LogInformation("Invoice {InvoiceId} created for {CustomerName}",
            invoice.Id, invoice.CustomerName);
    }
}

1.5 Benefits of SRP in Action

Now when requirements change:

  • Tax calculation changes → Only modify InvoiceCalculator
  • Database changes from SQL to MongoDB → Only modify InvoiceRepository
  • Email template redesign → Only modify InvoiceEmailService
  • New export format (Excel) → Add a new class InvoiceExcelExporter

💡 Key Insight: SRP doesn't mean a class should only have one method. It means a class should have one reason to change. A class can have multiple methods if they all serve the same cohesive responsibility.


2. Open/Closed Principle (OCP)

2.1 Understanding OCP

Definition: Software entities (classes, modules, functions) should be open for extension but closed for modification. You should be able to add new functionality without changing existing code.

Real-World Analogy: Think of a smartphone. You can install new apps (extend functionality) without changing the phone's operating system or hardware. The phone is "closed" for modification but "open" for extension through apps.

2.2 ❌ Violating OCP

Here's a discount calculator that requires modification every time a new discount type is added:

public class DiscountCalculator
{
    public decimal Calculate(string customerType, decimal amount)
    {
        if (customerType == "Regular")
        {
            return amount * 0.05m;  // 5% discount
        }
        else if (customerType == "Premium")
        {
            return amount * 0.1m;   // 10% discount
        }
        else if (customerType == "VIP")
        {
            return amount * 0.2m;   // 20% discount
        }
        else if (customerType == "Seasonal")
        {
            return amount * 0.15m;  // 15% discount - added by modifying existing code!
        }
        else if (customerType == "Employee")
        {
            return amount * 0.25m;  // 25% discount - more modifications!
        }
        return 0;
    }
}
// Every new discount requires modifying the existing Calculate method
// This violates OCP and increases risk of bugs

2.3 ✅ Following OCP - Using Abstraction

// Strategy interface - Open for extension
public interface IDiscountStrategy
{
    string StrategyName { get; }
    decimal CalculateDiscount(decimal amount);
    bool IsApplicable(string customerType);
}

// Regular discount - new class, no modification to existing code
public class RegularDiscountStrategy : IDiscountStrategy
{
    public string StrategyName => "Regular";
    
    public decimal CalculateDiscount(decimal amount)
    {
        return amount * 0.05m;
    }
    
    public bool IsApplicable(string customerType)
    {
        return customerType == "Regular";
    }
}

// Premium discount - new class
public class PremiumDiscountStrategy : IDiscountStrategy
{
    public string StrategyName => "Premium";
    
    public decimal CalculateDiscount(decimal amount)
    {
        return amount * 0.1m;
    }
    
    public bool IsApplicable(string customerType)
    {
        return customerType == "Premium";
    }
}

// VIP discount - new class
public class VipDiscountStrategy : IDiscountStrategy
{
    public string StrategyName => "VIP";
    
    public decimal CalculateDiscount(decimal amount)
    {
        return amount * 0.2m;
    }
    
    public bool IsApplicable(string customerType)
    {
        return customerType == "VIP";
    }
}

// Adding new discount - just create a new class, no existing code changes!
public class SeasonalDiscountStrategy : IDiscountStrategy
{
    private readonly DateTime _currentDate;
    
    public SeasonalDiscountStrategy(DateTime currentDate)
    {
        _currentDate = currentDate;
    }
    
    public string StrategyName => "Seasonal";
    
    public decimal CalculateDiscount(decimal amount)
    {
        return amount * 0.15m;
    }
    
    public bool IsApplicable(string customerType)
    {
        // Seasonal discount applies to everyone during holiday season
        return customerType == "Seasonal" || IsHolidaySeason();
    }
    
    private bool IsHolidaySeason()
    {
        return _currentDate.Month == 12 || _currentDate.Month == 1;
    }
}

// Discount calculator - Closed for modification
public class DiscountCalculator
{
    private readonly List<IDiscountStrategy> _strategies;
    
    public DiscountCalculator(IEnumerable<IDiscountStrategy> strategies)
    {
        _strategies = strategies.ToList();
    }
    
    public decimal CalculateDiscount(string customerType, decimal amount)
    {
        var strategy = _strategies.FirstOrDefault(s => s.IsApplicable(customerType));
        
        if (strategy == null)
            return 0;
            
        Console.WriteLine($"Applying {strategy.StrategyName} discount");
        return strategy.CalculateDiscount(amount);
    }
}

// Usage with Dependency Injection
public class CheckoutService
{
    private readonly DiscountCalculator _discountCalculator;
    
    public CheckoutService(IEnumerable<IDiscountStrategy> strategies)
    {
        _discountCalculator = new DiscountCalculator(strategies);
    }
    
    public decimal CalculateFinalTotal(string customerType, decimal subtotal)
    {
        var discount = _discountCalculator.CalculateDiscount(customerType, subtotal);
        return subtotal - discount;
    }
}

// Register in DI container
// services.AddScoped<IDiscountStrategy, RegularDiscountStrategy>();
// services.AddScoped<IDiscountStrategy, PremiumDiscountStrategy>();
// services.AddScoped<IDiscountStrategy, VipDiscountStrategy>();
// services.AddScoped<IDiscountStrategy, SeasonalDiscountStrategy>();

2.4 Another OCP Example - Specification Pattern

The Specification Pattern is a great way to follow OCP for filtering logic:

// Base specification - Open for extension
public interface ISpecification<T>
{
    bool IsSatisfiedBy(T entity);
}

// Concrete specifications - Each is a new class
public class ActiveCustomerSpecification : ISpecification<Customer>
{
    public bool IsSatisfiedBy(Customer customer)
    {
        return customer.IsActive && !customer.IsDeleted;
    }
}

public class HighValueCustomerSpecification : ISpecification<Customer>
{
    private readonly decimal _minimumSpend;
    
    public HighValueCustomerSpecification(decimal minimumSpend)
    {
        _minimumSpend = minimumSpend;
    }
    
    public bool IsSatisfiedBy(Customer customer)
    {
        return customer.TotalSpent >= _minimumSpend;
    }
}

public class CustomerInRegionSpecification : ISpecification<Customer>
{
    private readonly string _region;
    
    public CustomerInRegionSpecification(string region)
    {
        _region = region;
    }
    
    public bool IsSatisfiedBy(Customer customer)
    {
        return customer.Region == _region;
    }
}

// Composite specifications - Combine multiple specifications
public class AndSpecification<T> : ISpecification<T>
{
    private readonly ISpecification<T> _left;
    private readonly ISpecification<T> _right;
    
    public AndSpecification(ISpecification<T> left, ISpecification<T> right)
    {
        _left = left;
        _right = right;
    }
    
    public bool IsSatisfiedBy(T entity)
    {
        return _left.IsSatisfiedBy(entity) && _right.IsSatisfiedBy(entity);
    }
}

public class OrSpecification<T> : ISpecification<T>
{
    private readonly ISpecification<T> _left;
    private readonly ISpecification<T> _right;
    
    public OrSpecification(ISpecification<T> left, ISpecification<T> right)
    {
        _left = left;
        _right = right;
    }
    
    public bool IsSatisfiedBy(T entity)
    {
        return _left.IsSatisfiedBy(entity) || _right.IsSatisfiedBy(entity);
    }
}

// Usage - Compose specifications without modifying existing code
var activeSpec = new ActiveCustomerSpecification();
var highValueSpec = new HighValueCustomerSpecification(10000);
var regionSpec = new CustomerInRegionSpecification("North America");

// Combine specifications
var targetCustomers = new AndSpecification<Customer>(
    activeSpec,
    new OrSpecification<Customer>(highValueSpec, regionSpec)
);

var eligibleCustomers = customers.Where(c => targetCustomers.IsSatisfiedBy(c));

⚠️ Common Mistake: Many developers think OCP means "never modify code." That's unrealistic. OCP means you should design your code so that new features require adding new code rather than modifying stable, working code.


3. Liskov Substitution Principle (LSP)

3.1 Understanding LSP

Definition: If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering the correctness of the program.

In simpler terms: Derived classes must be completely substitutable for their base classes.

Real-World Analogy: If you have a function that works with a "Bird" object, it should work correctly with any specific bird like "Sparrow" or "Eagle". If you pass a "Penguin" (which can't fly) to a function that expects birds to fly, you violate LSP.

3.2 ❌ Violating LSP - Classic Rectangle/Square Problem

// Base class
public class Rectangle
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }
    
    public int CalculateArea()
    {
        return Width * Height;
    }
}

// Square inherits from Rectangle - violates LSP
public class Square : Rectangle
{
    private int _side;
    
    public override int Width
    {
        get { return _side; }
        set { _side = value; }
    }
    
    public override int Height
    {
        get { return _side; }
        set { _side = value; }
    }
}

// Client code that expects Rectangle behavior
public class AreaCalculator
{
    public void TestRectangleArea()
    {
        Rectangle rectangle = new Square();
        rectangle.Width = 5;
        rectangle.Height = 10;
        
        // Expecting 50 (5 * 10), but Square forces both dimensions equal
        // Actual area = 10 * 10 = 100!
        Console.WriteLine($"Area: {rectangle.CalculateArea()}");  // Outputs 100, not 50
        // This violates LSP - Square cannot substitute Rectangle
    }
}

3.3 ✅ Fixing LSP - Better Abstraction

// Abstract base class for all shapes
public abstract class Shape
{
    public abstract int CalculateArea();
}

// Rectangle as a shape
public class Rectangle : Shape
{
    public int Width { get; set; }
    public int Height { get; set; }
    
    public override int CalculateArea()
    {
        return Width * Height;
    }
}

// Square as a shape (no inheritance from Rectangle)
public class Square : Shape
{
    public int Side { get; set; }
    
    public override int CalculateArea()
    {
        return Side * Side;
    }
}

// Client code works with any Shape
public class AreaCalculator
{
    public void PrintArea(Shape shape)
    {
        Console.WriteLine($"Area: {shape.CalculateArea()}");
        // Works correctly with any shape
    }
}

3.4 ❌ Another LSP Violation - Overriding with Stronger Preconditions

public class BankAccount
{
    public decimal Balance { get; protected set; }
    
    public virtual void Withdraw(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Amount must be positive");
            
        Balance -= amount;
    }
}

public class FixedTermDepositAccount : BankAccount
{
    private DateTime _maturityDate;
    
    public FixedTermDepositAccount(DateTime maturityDate)
    {
        _maturityDate = maturityDate;
    }
    
    public override void Withdraw(decimal amount)
    {
        // Stronger precondition - can't withdraw before maturity
        if (DateTime.Now < _maturityDate)
            throw new InvalidOperationException("Cannot withdraw before maturity");
            
        base.Withdraw(amount);
    }
}

// Client code that works with BankAccount
public class AccountService
{
    public void ProcessWithdrawal(BankAccount account, decimal amount)
    {
        // This code assumes all BankAccounts support withdrawal
        account.Withdraw(amount);  // Throws exception for FixedTermDepositAccount
        Console.WriteLine($"Withdrawal successful. New balance: {account.Balance}");
    }
}

3.5 ✅ Fixing LSP - Rethinking the Hierarchy

// Better design - separate interfaces for different capabilities
public interface IWithdrawable
{
    void Withdraw(decimal amount);
}

public interface ITransferable
{
    void Transfer(decimal amount, IWithdrawable destination);
}

public class SavingsAccount : IWithdrawable, ITransferable
{
    public decimal Balance { get; private set; }
    
    public void Withdraw(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Amount must be positive");
        if (amount > Balance)
            throw new InvalidOperationException("Insufficient funds");
            
        Balance -= amount;
    }
    
    public void Transfer(decimal amount, IWithdrawable destination)
    {
        Withdraw(amount);
        destination.Deposit(amount);
    }
    
    public void Deposit(decimal amount)
    {
        Balance += amount;
    }
}

public class FixedTermDepositAccount : IWithdrawable
{
    private DateTime _maturityDate;
    public decimal Balance { get; private set; }
    
    public FixedTermDepositAccount(DateTime maturityDate, decimal initialDeposit)
    {
        _maturityDate = maturityDate;
        Balance = initialDeposit;
    }
    
    public void Withdraw(decimal amount)
    {
        if (DateTime.Now < _maturityDate)
            throw new InvalidOperationException("Cannot withdraw before maturity");
            
        if (amount <= 0)
            throw new ArgumentException("Amount must be positive");
        if (amount > Balance)
            throw new InvalidOperationException("Insufficient funds");
            
        Balance -= amount;
    }
    
    public void Deposit(decimal amount)  // Fixed term accounts may allow deposits
    {
        Balance += amount;
    }
}

// Now clients can specify exactly what they need
public class WithdrawalService
{
    public void ProcessWithdrawal(IWithdrawable account, decimal amount)
    {
        // This service only works with withdrawable accounts
        // FixedTermDepositAccount implements IWithdrawable, but may have restrictions
        // Those restrictions are part of the contract, not a surprise
        try
        {
            account.Withdraw(amount);
            Console.WriteLine($"Withdrawal successful");
        }
        catch (InvalidOperationException ex)
        {
            Console.WriteLine($"Withdrawal failed: {ex.Message}");
        }
    }
}

💡 LSP Checklist: When designing inheritance hierarchies, ask these questions:

  • Can I replace the base class with a derived class without breaking the program?
  • Does the derived class maintain the base class's behavioral contract?
  • Does the derived class weaken preconditions or strengthen postconditions?
  • Does the derived class throw exceptions that the base class doesn't?

4. Interface Segregation Principle (ISP)

4.1 Understanding ISP

Definition: No client should be forced to depend on methods it does not use. Instead of one large interface, create multiple smaller, focused interfaces.

Real-World Analogy: A restaurant menu shouldn't force all customers to see every possible item. A breakfast menu, lunch menu, and dinner menu are separate interfaces that serve different client needs.

4.2 ❌ Violating ISP - Fat Interface

// Fat interface - forces all implementers to have all methods
public interface IWorker
{
    void Work();
    void Eat();
    void Sleep();
    void AttendMeeting();
    void TakeBreak();
    void SubmitReport();
}

// Human worker - needs all methods
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 AttendMeeting() => Console.WriteLine("Human in meeting");
    public void TakeBreak() => Console.WriteLine("Human taking break");
    public void SubmitReport() => Console.WriteLine("Human submitting report");
}

// Robot worker - forced to implement irrelevant methods!
public class RobotWorker : IWorker
{
    public void Work() => Console.WriteLine("Robot working");
    
    // These methods make no sense for a robot!
    public void Eat() => throw new NotImplementedException("Robots don't eat!");
    public void Sleep() => throw new NotImplementedException("Robots don't sleep!");
    public void AttendMeeting() => throw new NotImplementedException("Robots don't attend meetings!");
    public void TakeBreak() => throw new NotImplementedException("Robots don't take breaks!");
    public void SubmitReport() => Console.WriteLine("Robot submitting report");  // Maybe possible
}
// RobotWorker is forced to implement methods it doesn't need

4.3 ✅ Following ISP - Segregated Interfaces

// Segregated, focused interfaces
public interface IWorkable
{
    void Work();
}

public interface IEatable
{
    void Eat();
}

public interface ISleepable
{
    void Sleep();
}

public interface IMeetingAttendable
{
    void AttendMeeting();
}

public interface IBreakTaker
{
    void TakeBreak();
}

public interface IReportSubmitter
{
    void SubmitReport();
}

// Human implements only relevant interfaces
public class HumanWorker : IWorkable, IEatable, ISleepable, 
                           IMeetingAttendable, IBreakTaker, IReportSubmitter
{
    public void Work() => Console.WriteLine("Human working");
    public void Eat() => Console.WriteLine("Human eating");
    public void Sleep() => Console.WriteLine("Human sleeping");
    public void AttendMeeting() => Console.WriteLine("Human in meeting");
    public void TakeBreak() => Console.WriteLine("Human taking break");
    public void SubmitReport() => Console.WriteLine("Human submitting report");
}

// Robot implements only what it needs
public class RobotWorker : IWorkable, IReportSubmitter
{
    public void Work() => Console.WriteLine("Robot working");
    public void SubmitReport() => Console.WriteLine("Robot submitting report");
}

// Another robot type - only works, no reports
public class SimpleRobot : IWorkable
{
    public void Work() => Console.WriteLine("Simple robot working");
}

// Client code depends only on what it needs
public class WorkManager
{
    public void AssignWork(IWorkable worker)
    {
        worker.Work();  // Works for any IWorkable
    }
}

public class ReportManager
{
    public void CollectReports(IReportSubmitter reporter)
    {
        reporter.SubmitReport();  // Only cares about report submission
    }
}

4.4 Another ISP Example - API Clients

// ❌ Fat interface for different API clients
public interface IApiClient
{
    Task<User> GetUserAsync(int id);
    Task<List<User>> GetAllUsersAsync();
    Task<User> CreateUserAsync(User user);
    Task UpdateUserAsync(User user);
    Task DeleteUserAsync(int id);
    Task<Product> GetProductAsync(int id);
    Task<List<Product>> GetAllProductsAsync();
    Task<Product> CreateProductAsync(Product product);
    Task<Order> CreateOrderAsync(Order order);
    Task<Payment> ProcessPaymentAsync(Payment payment);
}

// Client that only needs user operations
public class UserService
{
    private readonly IApiClient _client;  // Forced to depend on all methods
    
    public UserService(IApiClient client)
    {
        _client = client;
    }
    
    public async Task<User> GetUser(int id)
    {
        return await _client.GetUserAsync(id);  // Works fine
        // But this service can also access product, order, payment methods!
    }
}
// ✅ Segregated interfaces
public interface IUserReader
{
    Task<User> GetUserAsync(int id);
    Task<List<User>> GetAllUsersAsync();
}

public interface IUserWriter
{
    Task<User> CreateUserAsync(User user);
    Task UpdateUserAsync(User user);
    Task DeleteUserAsync(int id);
}

public interface IProductReader
{
    Task<Product> GetProductAsync(int id);
    Task<List<Product>> GetAllProductsAsync();
}

public interface IOrderCreator
{
    Task<Order> CreateOrderAsync(Order order);
}

public interface IPaymentProcessor
{
    Task<Payment> ProcessPaymentAsync(Payment payment);
}

// Combined interfaces for convenience (but still segregated)
public interface IUserOperations : IUserReader, IUserWriter { }

// Client depends only on what it needs
public class UserService
{
    private readonly IUserReader _userReader;
    private readonly IUserWriter _userWriter;
    
    public UserService(IUserReader userReader, IUserWriter userWriter)
    {
        _userReader = userReader;
        _userWriter = userWriter;
    }
    
    public async Task<User> GetUser(int id)
    {
        return await _userReader.GetUserAsync(id);
    }
    
    public async Task CreateUser(User user)
    {
        await _userWriter.CreateUserAsync(user);
    }
}

💡 ISP Benefits:

  • Reduces the impact of changes - changes to one interface don't affect clients of other interfaces
  • Improves readability - interfaces clearly communicate their purpose
  • Enhances testability - easier to mock focused interfaces
  • Promotes loose coupling - clients depend only on what they need

5. Dependency Inversion Principle (DIP)

5.1 Understanding DIP

Definition: 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.

Real-World Analogy: A laptop (high-level module) doesn't connect directly to a specific brand of power outlet (low-level module). Instead, it uses a power adapter (abstraction) that works with any power source that meets the specifications.

5.2 ❌ Violating DIP - Tight Coupling

// Low-level module - Email sender implementation
public class SmtpEmailSender
{
    public void SendEmail(string to, string subject, string body)
    {
        // SMTP specific logic
        Console.WriteLine($"Sending email via SMTP to {to}");
    }
}

// High-level module - Depends directly on low-level implementation
public class NotificationService
{
    private readonly SmtpEmailSender _emailSender;  // Direct dependency
    
    public NotificationService()
    {
        _emailSender = new SmtpEmailSender();  // Tight coupling
    }
    
    public void SendWelcomeEmail(string email)
    {
        _emailSender.SendEmail(email, "Welcome!", "Thank you for joining");
    }
}
// Problem: 
// 1. Can't change email provider without modifying NotificationService
// 2. Hard to test (can't mock the email sender)
// 3. Violates DIP - high level depends on low level

5.3 ✅ Following DIP - Dependency Inversion

// Abstraction - high level and low level both depend on this
public interface IEmailSender
{
    Task SendEmailAsync(string to, string subject, string body);
}

// Low-level module 1 - SMTP implementation
public class SmtpEmailSender : IEmailSender
{
    private readonly SmtpClient _smtpClient;
    
    public SmtpEmailSender(string smtpServer, int port, string username, string password)
    {
        _smtpClient = new SmtpClient(smtpServer, port);
        // Configure credentials
    }
    
    public async Task SendEmailAsync(string to, string subject, string body)
    {
        // SMTP specific implementation
        await _smtpClient.SendMailAsync(new MailMessage("noreply@company.com", to, subject, body));
        Console.WriteLine($"Email sent via SMTP to {to}");
    }
}

// Low-level module 2 - SendGrid implementation
public class SendGridEmailSender : IEmailSender
{
    private readonly SendGridClient _sendGridClient;
    
    public SendGridEmailSender(string apiKey)
    {
        _sendGridClient = new SendGridClient(apiKey);
    }
    
    public async Task SendEmailAsync(string to, string subject, string body)
    {
        var msg = new SendGridMessage();
        msg.SetFrom(new EmailAddress("noreply@company.com"));
        msg.AddTo(new EmailAddress(to));
        msg.SetSubject(subject);
        msg.AddContent(MimeType.Text, body);
        await _sendGridClient.SendEmailAsync(msg);
        Console.WriteLine($"Email sent via SendGrid to {to}");
    }
}

// Low-level module 3 - Mock for testing
public class MockEmailSender : IEmailSender
{
    public List<EmailMessage> SentMessages { get; } = new List<EmailMessage>();
    
    public Task SendEmailAsync(string to, string subject, string body)
    {
        SentMessages.Add(new EmailMessage { To = to, Subject = subject, Body = body });
        Console.WriteLine($"Mock email sent to {to}");
        return Task.CompletedTask;
    }
}

public class EmailMessage
{
    public string To { get; set; }
    public string Subject { get; set; }
    public string Body { get; set; }
}

// High-level module - Depends on abstraction
public class NotificationService
{
    private readonly IEmailSender _emailSender;  // Depends on abstraction
    private readonly ILogger<NotificationService> _logger;
    
    // Dependency injection via constructor
    public NotificationService(IEmailSender emailSender, ILogger<NotificationService> logger)
    {
        _emailSender = emailSender;
        _logger = logger;
    }
    
    public async Task SendWelcomeEmailAsync(string email, string userName)
    {
        var subject = "Welcome to Our Platform!";
        var body = $"Hello {userName},\n\nThank you for joining us!";
        
        try
        {
            await _emailSender.SendEmailAsync(email, subject, body);
            _logger.LogInformation("Welcome email sent to {Email}", email);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to send email to {Email}", email);
            throw;
        }
    }
    
    public async Task SendOrderConfirmationAsync(string email, int orderId)
    {
        var subject = $"Order #{orderId} Confirmation";
        var body = $"Your order #{orderId} has been confirmed.";
        
        await _emailSender.SendEmailAsync(email, subject, body);
    }
}

// Usage - Configuration in DI container
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Easily swap implementations without changing NotificationService
        if (Environment.IsProduction())
        {
            services.AddScoped<IEmailSender, SendGridEmailSender>();
        }
        else if (Environment.IsStaging())
        {
            services.AddScoped<IEmailSender, SmtpEmailSender>();
        }
        else
        {
            services.AddScoped<IEmailSender, MockEmailSender>();
        }
        
        services.AddScoped<NotificationService>();
    }
}

// Testing with mock
[Fact]
public async Task SendWelcomeEmail_ShouldSendEmail()
{
    // Arrange
    var mockEmailSender = new MockEmailSender();
    var logger = Mock.Of<ILogger<NotificationService>>();
    var notificationService = new NotificationService(mockEmailSender, logger);
    
    // Act
    await notificationService.SendWelcomeEmailAsync("test@example.com", "John");
    
    // Assert
    Assert.Single(mockEmailSender.SentMessages);
    Assert.Equal("test@example.com", mockEmailSender.SentMessages[0].To);
    Assert.Contains("Welcome", mockEmailSender.SentMessages[0].Subject);
}

5.4 DIP in Practice - Repository Pattern

// Abstraction - Repository interface
public interface IRepository<T> where T : class
{
    Task<T> GetByIdAsync(int id);
    Task<IEnumerable<T>> GetAllAsync();
    Task AddAsync(T entity);
    Task UpdateAsync(T entity);
    Task DeleteAsync(int id);
}

// High-level business service - depends on abstraction
public class ProductService
{
    private readonly IRepository<Product> _productRepository;
    private readonly ICacheService _cacheService;
    
    public ProductService(IRepository<Product> productRepository, ICacheService cacheService)
    {
        _productRepository = productRepository;
        _cacheService = cacheService;
    }
    
    public async Task<Product> GetProductAsync(int id)
    {
        // Try cache first
        var cached = await _cacheService.GetAsync<Product>($"product_{id}");
        if (cached != null)
            return cached;
            
        // Fallback to repository
        var product = await _productRepository.GetByIdAsync(id);
        
        if (product != null)
        {
            await _cacheService.SetAsync($"product_{id}", product, TimeSpan.FromMinutes(5));
        }
        
        return product;
    }
    
    public async Task CreateProductAsync(Product product)
    {
        await _productRepository.AddAsync(product);
        await _cacheService.RemoveAsync($"product_{product.Id}");
    }
}

// Low-level SQL implementation
public class SqlProductRepository : IRepository<Product>
{
    private readonly ApplicationDbContext _context;
    
    public SqlProductRepository(ApplicationDbContext context)
    {
        _context = context;
    }
    
    public async Task<Product> GetByIdAsync(int id)
    {
        return await _context.Products.FindAsync(id);
    }
    
    public async Task<IEnumerable<Product>> GetAllAsync()
    {
        return await _context.Products.ToListAsync();
    }
    
    public async Task AddAsync(Product entity)
    {
        await _context.Products.AddAsync(entity);
        await _context.SaveChangesAsync();
    }
    
    public async Task UpdateAsync(Product entity)
    {
        _context.Products.Update(entity);
        await _context.SaveChangesAsync();
    }
    
    public async Task DeleteAsync(int id)
    {
        var product = await GetByIdAsync(id);
        if (product != null)
        {
            _context.Products.Remove(product);
            await _context.SaveChangesAsync();
        }
    }
}

// Low-level MongoDB implementation
public class MongoProductRepository : IRepository<Product>
{
    private readonly IMongoCollection<Product> _collection;
    
    public MongoProductRepository(IMongoDatabase database)
    {
        _collection = database.GetCollection<Product>("Products");
    }
    
    public async Task<Product> GetByIdAsync(int id)
    {
        var filter = Builders<Product>.Filter.Eq(p => p.Id, id);
        return await _collection.Find(filter).FirstOrDefaultAsync();
    }
    
    public async Task<IEnumerable<Product>> GetAllAsync()
    {
        return await _collection.Find(_ => true).ToListAsync();
    }
    
    public async Task AddAsync(Product entity)
    {
        await _collection.InsertOneAsync(entity);
    }
    
    public async Task UpdateAsync(Product entity)
    {
        var filter = Builders<Product>.Filter.Eq(p => p.Id, entity.Id);
        await _collection.ReplaceOneAsync(filter, entity);
    }
    
    public async Task DeleteAsync(int id)
    {
        var filter = Builders<Product>.Filter.Eq(p => p.Id, id);
        await _collection.DeleteOneAsync(filter);
    }
}

💡 DIP Benefits:

  • Loose coupling: High-level modules are independent of low-level implementations
  • Testability: Easy to mock dependencies for unit testing
  • Flexibility: Swap implementations without changing high-level code
  • Parallel development: Different teams can work on different layers simultaneously

6. Putting It All Together - Real-World Example

Let's see how all SOLID principles work together in a real-world e-commerce checkout system:

// 1. Single Responsibility Principle - Each class has one reason to change
// 2. Open/Closed Principle - New payment methods can be added without changing existing code
// 3. Liskov Substitution Principle - All payment processors are substitutable
// 4. Interface Segregation Principle - Small, focused interfaces
// 5. Dependency Inversion Principle - High-level checkout depends on abstractions

// ===== Abstraction Layer (DIP) =====
public interface IPaymentProcessor
{
    Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request);
}

public interface IInventoryService
{
    Task<bool> CheckAvailabilityAsync(int productId, int quantity);
    Task<bool> ReserveStockAsync(int productId, int quantity);
}

public interface INotificationService
{
    Task SendOrderConfirmationAsync(string email, Order order);
}

public interface IOrderRepository
{
    Task SaveAsync(Order order);
    Task<Order> GetByIdAsync(int id);
}

// ===== Segregated Interfaces (ISP) =====
public interface IPaymentValidator
{
    bool Validate(PaymentRequest request);
}

public interface IPaymentLogger
{
    void LogPaymentAttempt(PaymentRequest request);
    void LogPaymentSuccess(PaymentRequest request, PaymentResult result);
    void LogPaymentFailure(PaymentRequest request, Exception ex);
}

// ===== Payment Processors (OCP - Open for extension) =====
public class CreditCardProcessor : IPaymentProcessor, IPaymentValidator
{
    private readonly IPaymentLogger _logger;
    
    public CreditCardProcessor(IPaymentLogger logger)
    {
        _logger = logger;
    }
    
    public bool Validate(PaymentRequest request)
    {
        if (string.IsNullOrEmpty(request.CardNumber))
            return false;
        if (request.CardNumber.Length != 16)
            return false;
        if (request.Amount <= 0)
            return false;
        return true;
    }
    
    public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
    {
        _logger.LogPaymentAttempt(request);
        
        if (!Validate(request))
            return PaymentResult.Failed("Invalid credit card details");
        
        // Simulate payment processing
        await Task.Delay(100);
        
        var result = PaymentResult.Success(Guid.NewGuid().ToString());
        _logger.LogPaymentSuccess(request, result);
        return result;
    }
}

public class PayPalProcessor : IPaymentProcessor, IPaymentValidator
{
    private readonly IPaymentLogger _logger;
    
    public PayPalProcessor(IPaymentLogger _logger)
    {
        _logger = _logger;
    }
    
    public bool Validate(PaymentRequest request)
    {
        if (string.IsNullOrEmpty(request.PayPalEmail))
            return false;
        if (!request.PayPalEmail.Contains("@"))
            return false;
        if (request.Amount <= 0)
            return false;
        return true;
    }
    
    public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
    {
        _logger.LogPaymentAttempt(request);
        
        if (!Validate(request))
            return PaymentResult.Failed("Invalid PayPal email");
        
        await Task.Delay(100);
        
        var result = PaymentResult.Success(Guid.NewGuid().ToString());
        _logger.LogPaymentSuccess(request, result);
        return result;
    }
}

// Adding new payment method - just add a new class (OCP)
public class CryptoProcessor : IPaymentProcessor, IPaymentValidator
{
    public bool Validate(PaymentRequest request)
    {
        return !string.IsNullOrEmpty(request.WalletAddress) && request.Amount > 0;
    }
    
    public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
    {
        if (!Validate(request))
            return PaymentResult.Failed("Invalid wallet address");
        
        await Task.Delay(100);
        return PaymentResult.Success(Guid.NewGuid().ToString());
    }
}

// ===== Value Objects and Entities =====
public class PaymentRequest
{
    public decimal Amount { get; set; }
    public string Currency { get; set; }
    public string CardNumber { get; set; }
    public string PayPalEmail { get; set; }
    public string WalletAddress { get; set; }
    public PaymentMethod Method { get; set; }
}

public class PaymentResult
{
    public bool IsSuccess { get; }
    public string TransactionId { get; }
    public string ErrorMessage { get; }
    
    private PaymentResult(bool isSuccess, string transactionId, string errorMessage)
    {
        IsSuccess = isSuccess;
        TransactionId = transactionId;
        ErrorMessage = errorMessage;
    }
    
    public static PaymentResult Success(string transactionId) 
        => new PaymentResult(true, transactionId, null);
    
    public static PaymentResult Failed(string errorMessage) 
        => new PaymentResult(false, null, errorMessage);
}

public enum PaymentMethod
{
    CreditCard,
    PayPal,
    Crypto
}

public class Order
{
    public int Id { get; set; }
    public string CustomerEmail { get; set; }
    public List<OrderItem> Items { get; set; }
    public decimal TotalAmount { get; set; }
    public OrderStatus Status { get; private set; }
    public DateTime CreatedAt { get; set; }
    
    public void Confirm()
    {
        if (Status != OrderStatus.Pending)
            throw new InvalidOperationException("Only pending orders can be confirmed");
        Status = OrderStatus.Confirmed;
    }
    
    public void MarkAsPaid()
    {
        if (Status != OrderStatus.Confirmed)
            throw new InvalidOperationException("Only confirmed orders can be paid");
        Status = OrderStatus.Paid;
    }
}

public enum OrderStatus
{
    Pending,
    Confirmed,
    Paid,
    Shipped,
    Delivered,
    Cancelled
}

// ===== High-level Checkout Service (DIP) =====
public class CheckoutService
{
    private readonly IPaymentProcessor _paymentProcessor;
    private readonly IInventoryService _inventoryService;
    private readonly INotificationService _notificationService;
    private readonly IOrderRepository _orderRepository;
    private readonly ILogger<CheckoutService> _logger;
    
    public CheckoutService(
        IPaymentProcessor paymentProcessor,
        IInventoryService inventoryService,
        INotificationService notificationService,
        IOrderRepository orderRepository,
        ILogger<CheckoutService> logger)
    {
        _paymentProcessor = paymentProcessor;
        _inventoryService = inventoryService;
        _notificationService = notificationService;
        _orderRepository = orderRepository;
        _logger = logger;
    }
    
    public async Task<CheckoutResult> ProcessCheckoutAsync(CheckoutRequest request)
    {
        _logger.LogInformation("Processing checkout for {Email}", request.CustomerEmail);
        
        try
        {
            // Step 1: Validate inventory (SRP - inventory service handles this)
            foreach (var item in request.Items)
            {
                var isAvailable = await _inventoryService.CheckAvailabilityAsync(item.ProductId, item.Quantity);
                if (!isAvailable)
                {
                    return CheckoutResult.Failed($"Product {item.ProductId} is out of stock");
                }
            }
            
            // Step 2: Create order (SRP - order creation)
            var order = new Order
            {
                CustomerEmail = request.CustomerEmail,
                Items = request.Items,
                TotalAmount = request.TotalAmount,
                CreatedAt = DateTime.UtcNow,
                Status = OrderStatus.Pending
            };
            
            await _orderRepository.SaveAsync(order);
            _logger.LogInformation("Order {OrderId} created", order.Id);
            
            // Step 3: Process payment (DIP - abstraction used)
            var paymentRequest = new PaymentRequest
            {
                Amount = request.TotalAmount,
                Currency = "USD",
                Method = request.PaymentMethod
            };
            
            // Map payment method specific details (LSP - all processors work the same)
            switch (request.PaymentMethod)
            {
                case PaymentMethod.CreditCard:
                    paymentRequest.CardNumber = request.CardNumber;
                    break;
                case PaymentMethod.PayPal:
                    paymentRequest.PayPalEmail = request.PayPalEmail;
                    break;
                case PaymentMethod.Crypto:
                    paymentRequest.WalletAddress = request.WalletAddress;
                    break;
            }
            
            var paymentResult = await _paymentProcessor.ProcessPaymentAsync(paymentRequest);
            
            if (!paymentResult.IsSuccess)
            {
                _logger.LogWarning("Payment failed for order {OrderId}: {Error}", 
                    order.Id, paymentResult.ErrorMessage);
                return CheckoutResult.Failed(paymentResult.ErrorMessage);
            }
            
            // Step 4: Update order status
            order.Confirm();
            order.MarkAsPaid();
            await _orderRepository.SaveAsync(order);
            
            // Step 5: Reserve inventory
            foreach (var item in request.Items)
            {
                await _inventoryService.ReserveStockAsync(item.ProductId, item.Quantity);
            }
            
            // Step 6: Send confirmation (SRP - notification service handles this)
            await _notificationService.SendOrderConfirmationAsync(request.CustomerEmail, order);
            
            _logger.LogInformation("Checkout completed successfully for order {OrderId}", order.Id);
            
            return CheckoutResult.Success(order.Id, paymentResult.TransactionId);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Checkout failed for {Email}", request.CustomerEmail);
            return CheckoutResult.Failed("An unexpected error occurred");
        }
    }
}

public class CheckoutRequest
{
    public string CustomerEmail { get; set; }
    public List<OrderItem> Items { get; set; }
    public decimal TotalAmount { get; set; }
    public PaymentMethod PaymentMethod { get; set; }
    public string CardNumber { get; set; }
    public string PayPalEmail { get; set; }
    public string WalletAddress { get; set; }
}

public class CheckoutResult
{
    public bool IsSuccess { get; }
    public int OrderId { get; }
    public string TransactionId { get; }
    public string ErrorMessage { get; }
    
    private CheckoutResult(bool isSuccess, int orderId, string transactionId, string errorMessage)
    {
        IsSuccess = isSuccess;
        OrderId = orderId;
        TransactionId = transactionId;
        ErrorMessage = errorMessage;
    }
    
    public static CheckoutResult Success(int orderId, string transactionId)
        => new CheckoutResult(true, orderId, transactionId, null);
    
    public static CheckoutResult Failed(string errorMessage)
        => new CheckoutResult(false, 0, null, errorMessage);
}

public class OrderItem
{
    public int ProductId { get; set; }
    public string ProductName { get; set; }
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
}

// ===== Dependency Injection Configuration =====
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Register repositories (DIP)
        services.AddScoped<IOrderRepository, OrderRepository>();
        
        // Register services (DIP)
        services.AddScoped<IInventoryService, InventoryService>();
        services.AddScoped<INotificationService, EmailNotificationService>();
        services.AddScoped<IPaymentLogger, PaymentLogger>();
        
        // Register payment processors - can be swapped easily (OCP)
        services.AddScoped<IPaymentProcessor, CreditCardProcessor>();
        // services.AddScoped<IPaymentProcessor, PayPalProcessor>(); // Swap easily
        // services.AddScoped<IPaymentProcessor, CryptoProcessor>();  // Add new without changes
        
        // Register high-level service
        services.AddScoped<CheckoutService>();
    }
}

7. SOLID Principles - Quick Reference Card

Principle Key Question Red Flags .NET Implementation Tip
SRP Does this class have only one reason to change? Classes named "Manager", "Helper", "Utils"
Methods that access database, send emails, and calculate
Use partial classes for large files
Inject dependencies for cross-cutting concerns
OCP Can I add new features without modifying existing code? Long if-else chains or switch statements
Frequent modifications to existing classes
Use interfaces, abstract classes, strategy pattern
Leverage .NET dependency injection
LSP Can I substitute a derived class without breaking the program? Derived classes throwing NotImplementedException
Overridden methods doing nothing
Use "is" and "as" operators carefully
Consider composition over inheritance
ISP Are clients forced to depend on methods they don't use? Large interfaces with many methods
Empty implementations of interface methods
Create small, focused interfaces
Use interface inheritance to combine when needed
DIP Do high-level modules depend on abstractions, not concretions? Direct instantiation of dependencies with "new"
Static method calls to external services
Use constructor injection
Register interfaces in DI container
Avoid "new" for complex dependencies

8. Common SOLID Anti-Patterns to Avoid

8.1 The "God Class" (Violates SRP)

// ❌ AVOID: One class that does everything
public class ApplicationManager
{
    public void CreateUser() { }
    public void DeleteUser() { }
    public void ProcessOrder() { }
    public void GenerateReport() { }
    public void SendEmail() { }
    public void BackupDatabase() { }
    public void LogError() { }
}
// Instead, split into UserService, OrderService, ReportService, etc.

8.2 "Speculative Generality" (Violates YAGNI and sometimes OCP)

// ❌ AVOID: Adding abstractions for future needs that may never come
public interface IMaybeFutureInterface { }
public class OverlyComplexBase { }
// Don't add layers "just in case" - add them when you actually need them

8.3 "Constant Modifications" (Violates OCP)

// ❌ AVOID: Every new type requires modifying existing code
public enum NotificationType { Email, Sms, Push }
public class NotificationService
{
    public void Send(NotificationType type, string message)
    {
        switch(type)  // Every new type adds a case here
        {
            case NotificationType.Email: SendEmail(message); break;
            case NotificationType.Sms: SendSms(message); break;
            // Add new case for Push - modifies existing code!
        }
    }
}
// Instead, use strategy pattern or factory pattern

8.4 "Leaky Abstractions" (Violates LSP and DIP)

// ❌ AVOID: Abstractions that expose implementation details
public interface IDataStorage
{
    void Save(string connectionString, string tableName, object data);
    // ConnectionString and TableName are SQL-specific!
}

9. Practice Exercises

Exercise 1: Identify SOLID Violations

Review the following code and identify which SOLID principles are violated:

public class UserManager
{
    private SqlConnection _connection;
    
    public UserManager()
    {
        _connection = new SqlConnection("Server=.;Database=Users");
    }
    
    public void CreateUser(string name, string email)
    {
        // Database logic
        _connection.Open();
        var cmd = new SqlCommand("INSERT INTO Users...", _connection);
        cmd.ExecuteNonQuery();
        
        // Email logic
        var smtp = new SmtpClient("smtp.company.com");
        smtp.Send("admin@company.com", email, "Welcome", "Hello " + name);
        
        // Logging logic
        File.WriteAllText("log.txt", $"User {name} created at {DateTime.Now}");
    }
    
    public void DeleteUser(int id)
    {
        // Similar mixed responsibilities
    }
    
    public void GenerateUserReport()
    {
        // Report generation logic
    }
}
Click to see analysis

Violations found:

  • SRP: UserManager handles database, email, logging, and reporting
  • DIP: Direct dependencies on SqlConnection, SmtpClient, File system
  • OCP: Can't add new notification methods without modifying code
  • ISP: Clients forced to depend on all methods even if they only need some

Exercise 2: Refactor to Follow SOLID

Refactor the above code to follow all SOLID principles. (Try it yourself, then check the solution)

Click to see solution
// Abstraction layer
public interface IUserRepository
{
    Task CreateAsync(User user);
    Task DeleteAsync(int id);
}

public interface INotificationService
{
    Task SendWelcomeEmailAsync(string email, string name);
}

public interface ILogger
{
    void Log(string message);
}

public interface IReportGenerator
{
    Task<byte[]> GenerateUserReportAsync();
}

// Concrete implementations
public class SqlUserRepository : IUserRepository
{
    private readonly string _connectionString;
    
    public SqlUserRepository(IConfiguration config)
    {
        _connectionString = config.GetConnectionString("Default");
    }
    
    public async Task CreateAsync(User user)
    {
        using var connection = new SqlConnection(_connectionString);
        // SQL implementation
    }
    
    public async Task DeleteAsync(int id) { /* implementation */ }
}

public class EmailNotificationService : INotificationService
{
    private readonly IEmailSender _emailSender;
    
    public EmailNotificationService(IEmailSender emailSender)
    {
        _emailSender = emailSender;
    }
    
    public async Task SendWelcomeEmailAsync(string email, string name)
    {
        await _emailSender.SendAsync(email, "Welcome", $"Hello {name}");
    }
}

public class FileLogger : ILogger
{
    private readonly string _logPath;
    
    public FileLogger(string logPath)
    {
        _logPath = logPath;
    }
    
    public void Log(string message)
    {
        File.AppendAllText(_logPath, $"{DateTime.Now}: {message}\n");
    }
}

// High-level service
public class UserService
{
    private readonly IUserRepository _repository;
    private readonly INotificationService _notificationService;
    private readonly ILogger _logger;
    
    public UserService(
        IUserRepository repository,
        INotificationService notificationService,
        ILogger logger)
    {
        _repository = repository;
        _notificationService = notificationService;
        _logger = logger;
    }
    
    public async Task CreateUserAsync(string name, string email)
    {
        var user = new User { Name = name, Email = email };
        
        await _repository.CreateAsync(user);
        await _notificationService.SendWelcomeEmailAsync(email, name);
        _logger.Log($"User {name} created");
    }
}

10. Summary and Key Takeaways

  • S - Single Responsibility: Each class should have one reason to change. Split large classes into focused ones.
  • O - Open/Closed: Design for extension. Use interfaces, abstract classes, and design patterns to add features without modifying existing code.
  • L - Liskov Substitution: Derived classes must be substitutable for base classes. Don't break expectations.
  • I - Interface Segregation: Create small, focused interfaces. Don't force clients to depend on what they don't use.
  • D - Dependency Inversion: Depend on abstractions, not concretions. Use dependency injection to decouple layers.

🎯 The SOLID Mindset:

  • Write code that is easy to understand
  • Write code that is easy to test
  • Write code that is easy to change
  • Write code that is easy to reuse
  • Write code that is easy to maintain

11. What's Next?

In Chapter 3, we'll dive into Creational Design Patterns - Factory, Abstract Factory, Builder, Prototype, and Singleton. We'll see how these patterns help us create objects in a way that follows SOLID principles.

Chapter 3 Preview:

  • Singleton - Ensuring a class has only one instance
  • Factory Method - Creating objects without specifying the exact class
  • Abstract Factory - Creating families of related objects
  • Builder - Constructing complex objects step by step
  • Prototype - Creating objects by cloning existing instances

📝 Practice Assignments:

  1. Review your current project's codebase and identify SOLID violations
  2. Refactor a class with multiple responsibilities into multiple single-responsibility classes
  3. Implement a discount system using the Open/Closed Principle with strategies
  4. Create a repository pattern implementation that follows Dependency Inversion
  5. Design a notification system with multiple channels (email, SMS, push) following all SOLID principles

Happy Coding! 🚀

Post a Comment

Feel free to ask your query...
Cookie Consent
We serve cookies on this site to analyze traffic, remember your preferences, and optimize your experience.
Oops!
It seems there is something wrong with your internet connection. Please connect to the internet and start browsing again.
AdBlock Detected!
We have detected that you are using adblocking plugin in your browser.
The revenue we earn by the advertisements is used to manage this website, we request you to whitelist our website in your adblocking plugin.
Site is Blocked
Sorry! This site is not available in your country.