📄

Solid Principles Detailed

Intermediate 3 min read 600 words

SOLID Principles with C# Examples

Introduction

SOLID is an acronym for five design principles that make software designs more understandable, flexible, and maintainable. These principles were introduced by Robert C. Martin (Uncle Bob) and form the foundation of good object-oriented design.


Table of Contents


Single Responsibility Principle (SRP)

“A class should have one, and only one, reason to change.”

The Problem

// ❌ Violation: This class has multiple responsibilities
public class Employee
{
    public string Name { get; set; }
    public decimal Salary { get; set; }

    // Responsibility 1: Employee data management
    public void UpdateName(string name) { Name = name; }

    // Responsibility 2: Salary calculations
    public decimal CalculateBonus() => Salary * 0.1m;
    public decimal CalculateTax() => Salary * 0.3m;

    // Responsibility 3: Persistence
    public void SaveToDatabase()
    {
        using var connection = new SqlConnection("...");
        // SQL operations
    }

    // Responsibility 4: Reporting
    public string GeneratePayslip()
    {
        return $"Employee: {Name}\nSalary: {Salary}\nBonus: {CalculateBonus()}";
    }

    // Responsibility 5: Email notifications
    public void SendPayslipByEmail(string email)
    {
        // SMTP logic
    }
}

This class will change if:

  • Employee data requirements change
  • Tax/bonus calculations change
  • Database schema changes
  • Report format changes
  • Email system changes

The Solution

// ✅ Each class has a single responsibility

// Responsibility 1: Employee data
public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Salary { get; set; }
    public string Email { get; set; }
}

// Responsibility 2: Salary calculations
public class SalaryCalculator
{
    public decimal CalculateBonus(Employee employee)
        => employee.Salary * 0.1m;

    public decimal CalculateTax(Employee employee)
        => employee.Salary * 0.3m;

    public decimal CalculateNetSalary(Employee employee)
        => employee.Salary - CalculateTax(employee) + CalculateBonus(employee);
}

// Responsibility 3: Persistence
public interface IEmployeeRepository
{
    Task<Employee> GetByIdAsync(int id);
    Task SaveAsync(Employee employee);
    Task DeleteAsync(int id);
}

public class SqlEmployeeRepository : IEmployeeRepository
{
    private readonly ApplicationDbContext _context;

    public SqlEmployeeRepository(ApplicationDbContext context)
        => _context = context;

    public async Task<Employee> GetByIdAsync(int id)
        => await _context.Employees.FindAsync(id);

    public async Task SaveAsync(Employee employee)
    {
        _context.Employees.Update(employee);
        await _context.SaveChangesAsync();
    }

    public async Task DeleteAsync(int id)
    {
        var employee = await GetByIdAsync(id);
        if (employee != null)
        {
            _context.Employees.Remove(employee);
            await _context.SaveChangesAsync();
        }
    }
}

// Responsibility 4: Reporting
public class PayslipGenerator
{
    private readonly SalaryCalculator _calculator;

    public PayslipGenerator(SalaryCalculator calculator)
        => _calculator = calculator;

    public string Generate(Employee employee)
    {
        var bonus = _calculator.CalculateBonus(employee);
        var tax = _calculator.CalculateTax(employee);
        var net = _calculator.CalculateNetSalary(employee);

        return $"""
            PAYSLIP
            -------
            Employee: {employee.Name}
            Gross Salary: {employee.Salary:C}
            Bonus: {bonus:C}
            Tax: {tax:C}
            Net Salary: {net:C}
            """;
    }
}

// Responsibility 5: Notifications
public interface INotificationService
{
    Task SendAsync(string recipient, string subject, string body);
}

public class EmailNotificationService : INotificationService
{
    private readonly IEmailClient _emailClient;

    public EmailNotificationService(IEmailClient emailClient)
        => _emailClient = emailClient;

    public async Task SendAsync(string recipient, string subject, string body)
    {
        await _emailClient.SendEmailAsync(recipient, subject, body);
    }
}

Benefits of SRP

Benefit Explanation
Easier testing Each class can be tested in isolation
Lower coupling Changes in one area don’t affect others
Better organization Code is easier to navigate
Reusability Small, focused classes are more reusable

Open/Closed Principle (OCP)

“Software entities should be open for extension, but closed for modification.”

The Problem

// ❌ Violation: Must modify this class to add new discount types
public class DiscountCalculator
{
    public decimal CalculateDiscount(Order order, string discountType)
    {
        switch (discountType)
        {
            case "percentage":
                return order.Total * 0.1m;
            case "fixed":
                return 10m;
            case "loyalty":
                return order.Total * 0.15m;
            // Must add new case for every discount type
            case "seasonal":
                return order.Total * 0.2m;
            default:
                return 0m;
        }
    }
}

Adding a new discount type requires modifying the existing class, which risks breaking existing functionality.

The Solution

// ✅ Open for extension (new discount types), closed for modification

public interface IDiscountStrategy
{
    decimal CalculateDiscount(Order order);
    bool IsApplicable(Order order);
}

// Each discount type is a separate class
public class PercentageDiscount : IDiscountStrategy
{
    private readonly decimal _percentage;

    public PercentageDiscount(decimal percentage) => _percentage = percentage;

    public decimal CalculateDiscount(Order order) => order.Total * _percentage;
    public bool IsApplicable(Order order) => true;
}

public class FixedDiscount : IDiscountStrategy
{
    private readonly decimal _amount;

    public FixedDiscount(decimal amount) => _amount = amount;

    public decimal CalculateDiscount(Order order) => Math.Min(_amount, order.Total);
    public bool IsApplicable(Order order) => order.Total >= _amount;
}

public class LoyaltyDiscount : IDiscountStrategy
{
    private readonly decimal _percentage;
    private readonly int _requiredPurchases;

    public LoyaltyDiscount(decimal percentage, int requiredPurchases)
    {
        _percentage = percentage;
        _requiredPurchases = requiredPurchases;
    }

    public decimal CalculateDiscount(Order order) => order.Total * _percentage;
    public bool IsApplicable(Order order) => order.Customer.PurchaseCount >= _requiredPurchases;
}

// New discount types can be added without modifying existing code
public class BulkDiscount : IDiscountStrategy
{
    private readonly int _minimumQuantity;
    private readonly decimal _percentage;

    public BulkDiscount(int minimumQuantity, decimal percentage)
    {
        _minimumQuantity = minimumQuantity;
        _percentage = percentage;
    }

    public decimal CalculateDiscount(Order order) => order.Total * _percentage;
    public bool IsApplicable(Order order) => order.TotalItems >= _minimumQuantity;
}

// Calculator is closed for modification
public class DiscountCalculator
{
    private readonly IEnumerable<IDiscountStrategy> _strategies;

    public DiscountCalculator(IEnumerable<IDiscountStrategy> strategies)
    {
        _strategies = strategies;
    }

    public decimal CalculateBestDiscount(Order order)
    {
        return _strategies
            .Where(s => s.IsApplicable(order))
            .Select(s => s.CalculateDiscount(order))
            .DefaultIfEmpty(0)
            .Max();
    }

    public decimal CalculateTotalDiscount(Order order)
    {
        return _strategies
            .Where(s => s.IsApplicable(order))
            .Sum(s => s.CalculateDiscount(order));
    }
}

OCP with Inheritance

// Base class is closed for modification
public abstract class Report
{
    public string Title { get; set; }
    public DateTime GeneratedAt { get; set; }

    // Template method pattern
    public void Generate()
    {
        GatherData();
        FormatReport();
        Export();
    }

    protected abstract void GatherData();
    protected abstract void FormatReport();

    // Can be overridden but has default behavior
    protected virtual void Export()
    {
        Console.WriteLine($"Report '{Title}' generated at {GeneratedAt}");
    }
}

// Extension through inheritance
public class SalesReport : Report
{
    private List<Sale> _sales;

    protected override void GatherData()
    {
        // Fetch sales data
    }

    protected override void FormatReport()
    {
        // Format as sales report
    }
}

public class InventoryReport : Report
{
    protected override void GatherData()
    {
        // Fetch inventory data
    }

    protected override void FormatReport()
    {
        // Format as inventory report
    }

    protected override void Export()
    {
        // Custom export to Excel
    }
}

Liskov Substitution Principle (LSP)

“Objects of a superclass shall be replaceable with objects of a subclass without affecting the correctness of the program.”

The Problem

// ❌ Classic LSP violation: Rectangle/Square problem
public class Rectangle
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }

    public int CalculateArea() => Width * Height;
}

public class Square : Rectangle
{
    private int _side;

    public override int Width
    {
        get => _side;
        set => _side = value;  // Also sets height!
    }

    public override int Height
    {
        get => _side;
        set => _side = value;  // Also sets width!
    }
}

// This code breaks with Square
public void TestRectangle(Rectangle rect)
{
    rect.Width = 5;
    rect.Height = 4;

    // Expected: 20 for Rectangle
    // Actual: 16 for Square (because setting Height changed Width!)
    Debug.Assert(rect.CalculateArea() == 20);  // FAILS for Square
}

The Solution

// ✅ Solution 1: Use an interface for what they have in common
public interface IShape
{
    int CalculateArea();
}

public class Rectangle : IShape
{
    public int Width { get; }
    public int Height { get; }

    public Rectangle(int width, int height)
    {
        Width = width;
        Height = height;
    }

    public int CalculateArea() => Width * Height;
}

public class Square : IShape
{
    public int Side { get; }

    public Square(int side) => Side = side;

    public int CalculateArea() => Side * Side;
}

// Now code works correctly with any IShape
public void ProcessShape(IShape shape)
{
    Console.WriteLine($"Area: {shape.CalculateArea()}");
}

Real-World LSP Violation

// ❌ Violation: Not all birds can fly
public class Bird
{
    public virtual void Fly()
    {
        Console.WriteLine("Flying...");
    }
}

public class Penguin : Bird
{
    public override void Fly()
    {
        throw new NotSupportedException("Penguins can't fly!");
    }
}

// Code that expects all birds to fly will break
public void MakeBirdsFly(IEnumerable<Bird> birds)
{
    foreach (var bird in birds)
    {
        bird.Fly();  // Crashes on Penguin!
    }
}

// ✅ Solution: Separate flying behavior
public abstract class Bird
{
    public abstract void Move();
}

public interface IFlyable
{
    void Fly();
}

public class Sparrow : Bird, IFlyable
{
    public override void Move() => Fly();
    public void Fly() => Console.WriteLine("Flying through the air");
}

public class Penguin : Bird
{
    public override void Move() => Console.WriteLine("Swimming through water");
}

// Now we can safely work with flying birds
public void MakeBirdsFly(IEnumerable<IFlyable> flyingBirds)
{
    foreach (var bird in flyingBirds)
    {
        bird.Fly();  // Safe - only flyable birds
    }
}

LSP Contract Rules

public class BaseService
{
    // Precondition: amount > 0
    // Postcondition: returns balance >= 0
    public virtual decimal Withdraw(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Amount must be positive");

        // ... withdraw logic
        return balance;
    }
}

// ✅ Valid: Same or weaker preconditions, same or stronger postconditions
public class PremiumService : BaseService
{
    public override decimal Withdraw(decimal amount)
    {
        // Weaker precondition: allows amount == 0 (more permissive)
        if (amount < 0)
            throw new ArgumentException("Amount cannot be negative");

        // ... withdraw logic
        return balance;  // Same postcondition
    }
}

// ❌ Invalid: Stronger precondition
public class RestrictedService : BaseService
{
    public override decimal Withdraw(decimal amount)
    {
        // Stronger precondition: breaks existing code!
        if (amount < 100)
            throw new ArgumentException("Minimum withdrawal is $100");

        return balance;
    }
}

Interface Segregation Principle (ISP)

“Clients should not be forced to depend on interfaces they do not use.”

The Problem

// ❌ Violation: Fat interface forces implementations of unused methods
public interface IWorker
{
    void Work();
    void Eat();
    void Sleep();
    void TakeBreak();
    void AttendMeeting();
    void SubmitTimesheet();
}

// Robot must implement methods that don't apply
public class Robot : IWorker
{
    public void Work() => Console.WriteLine("Working...");

    // These don't make sense for a robot!
    public void Eat() => throw new NotImplementedException();
    public void Sleep() => throw new NotImplementedException();
    public void TakeBreak() => throw new NotImplementedException();
    public void AttendMeeting() => throw new NotImplementedException();
    public void SubmitTimesheet() => throw new NotImplementedException();
}

The Solution

// ✅ Segregated interfaces - each client gets only what it needs

public interface IWorkable
{
    void Work();
}

public interface IFeedable
{
    void Eat();
}

public interface IRestable
{
    void Sleep();
    void TakeBreak();
}

public interface IMeetingAttendee
{
    void AttendMeeting();
}

public interface ITimesheetSubmitter
{
    void SubmitTimesheet();
}

// Human implements all relevant interfaces
public class HumanWorker : IWorkable, IFeedable, IRestable, IMeetingAttendee, ITimesheetSubmitter
{
    public void Work() => Console.WriteLine("Human working...");
    public void Eat() => Console.WriteLine("Human eating...");
    public void Sleep() => Console.WriteLine("Human sleeping...");
    public void TakeBreak() => Console.WriteLine("Human taking break...");
    public void AttendMeeting() => Console.WriteLine("Human in meeting...");
    public void SubmitTimesheet() => Console.WriteLine("Human submitting timesheet...");
}

// Robot only implements what applies
public class Robot : IWorkable
{
    public void Work() => Console.WriteLine("Robot working efficiently 24/7...");
}

// Contractor might not attend all meetings
public class Contractor : IWorkable, IFeedable, ITimesheetSubmitter
{
    public void Work() => Console.WriteLine("Contractor working...");
    public void Eat() => Console.WriteLine("Contractor eating...");
    public void SubmitTimesheet() => Console.WriteLine("Contractor billing hours...");
}

Role Interfaces Pattern

// Instead of one large repository interface...
// ❌ Fat interface
public interface IRepository<T>
{
    T GetById(int id);
    IEnumerable<T> GetAll();
    IEnumerable<T> Find(Expression<Func<T, bool>> predicate);
    void Add(T entity);
    void AddRange(IEnumerable<T> entities);
    void Update(T entity);
    void Remove(T entity);
    void RemoveRange(IEnumerable<T> entities);
    int Count();
    bool Exists(int id);
}

// ✅ Segregated role interfaces
public interface IReadRepository<T>
{
    T GetById(int id);
    IEnumerable<T> GetAll();
    IEnumerable<T> Find(Expression<Func<T, bool>> predicate);
}

public interface IWriteRepository<T>
{
    void Add(T entity);
    void Update(T entity);
    void Remove(T entity);
}

public interface IBulkRepository<T>
{
    void AddRange(IEnumerable<T> entities);
    void RemoveRange(IEnumerable<T> entities);
}

// Read-only service only needs read interface
public class ReportService
{
    private readonly IReadRepository<Order> _orderRepository;

    public ReportService(IReadRepository<Order> orderRepository)
    {
        _orderRepository = orderRepository;
    }

    public IEnumerable<Order> GetOrdersForReport()
    {
        return _orderRepository.GetAll();
    }
}

// CQRS: Commands use write, Queries use read
public class OrderCommandHandler
{
    private readonly IWriteRepository<Order> _repository;

    public OrderCommandHandler(IWriteRepository<Order> repository)
    {
        _repository = repository;
    }

    public void Handle(CreateOrderCommand command)
    {
        var order = new Order { /* ... */ };
        _repository.Add(order);
    }
}

Dependency Inversion Principle (DIP)

“High-level modules should not depend on low-level modules. Both should depend on abstractions.”

The Problem

// ❌ Violation: High-level module depends on low-level implementation
public class EmailService  // Low-level
{
    public void SendEmail(string to, string subject, string body)
    {
        // SMTP implementation
    }
}

public class OrderService  // High-level
{
    private readonly EmailService _emailService = new EmailService();  // Direct dependency!

    public void PlaceOrder(Order order)
    {
        // Process order...
        _emailService.SendEmail(order.CustomerEmail, "Order Placed", "...");
    }
}

// Problems:
// 1. Can't test OrderService without sending real emails
// 2. Can't switch to SMS or push notifications without modifying OrderService
// 3. OrderService is tightly coupled to EmailService

The Solution

// ✅ Both high-level and low-level modules depend on abstractions

// Abstraction (owned by high-level module conceptually)
public interface INotificationService
{
    Task SendAsync(string recipient, string subject, string message);
}

// Low-level implementation
public class EmailNotificationService : INotificationService
{
    private readonly ISmtpClient _smtpClient;

    public EmailNotificationService(ISmtpClient smtpClient)
    {
        _smtpClient = smtpClient;
    }

    public async Task SendAsync(string recipient, string subject, string message)
    {
        await _smtpClient.SendEmailAsync(recipient, subject, message);
    }
}

public class SmsNotificationService : INotificationService
{
    private readonly ITwilioClient _twilioClient;

    public SmsNotificationService(ITwilioClient twilioClient)
    {
        _twilioClient = twilioClient;
    }

    public async Task SendAsync(string recipient, string subject, string message)
    {
        await _twilioClient.SendSmsAsync(recipient, $"{subject}: {message}");
    }
}

// High-level module depends on abstraction
public class OrderService
{
    private readonly IOrderRepository _orderRepository;
    private readonly INotificationService _notificationService;
    private readonly ILogger<OrderService> _logger;

    // Dependencies injected through constructor
    public OrderService(
        IOrderRepository orderRepository,
        INotificationService notificationService,
        ILogger<OrderService> logger)
    {
        _orderRepository = orderRepository;
        _notificationService = notificationService;
        _logger = logger;
    }

    public async Task PlaceOrderAsync(Order order)
    {
        await _orderRepository.SaveAsync(order);
        await _notificationService.SendAsync(
            order.CustomerEmail,
            "Order Placed",
            $"Your order #{order.Id} has been placed.");
        _logger.LogInformation("Order {OrderId} placed", order.Id);
    }
}

// DI Container registration (ASP.NET Core)
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<INotificationService, EmailNotificationService>();
builder.Services.AddScoped<OrderService>();

Dependency Injection Patterns

// 1. Constructor Injection (Preferred)
public class UserService
{
    private readonly IUserRepository _repository;
    private readonly ILogger<UserService> _logger;

    public UserService(IUserRepository repository, ILogger<UserService> logger)
    {
        _repository = repository ?? throw new ArgumentNullException(nameof(repository));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }
}

// 2. Method Injection (When dependency varies per call)
public class ReportGenerator
{
    public Report Generate(IDataSource dataSource)
    {
        var data = dataSource.GetData();
        return new Report(data);
    }
}

// 3. Property Injection (Optional dependencies)
public class NotificationHandler
{
    // Optional - has default behavior
    public ILogger Logger { get; set; } = NullLogger.Instance;

    public void Handle(Notification notification)
    {
        Logger.LogInformation("Handling notification");
        // ...
    }
}

Testing with DIP

// Easy to test because dependencies are injected
public class OrderServiceTests
{
    [Fact]
    public async Task PlaceOrder_SendsNotification()
    {
        // Arrange
        var mockRepository = new Mock<IOrderRepository>();
        var mockNotification = new Mock<INotificationService>();
        var mockLogger = new Mock<ILogger<OrderService>>();

        var service = new OrderService(
            mockRepository.Object,
            mockNotification.Object,
            mockLogger.Object);

        var order = new Order
        {
            Id = 1,
            CustomerEmail = "test@example.com"
        };

        // Act
        await service.PlaceOrderAsync(order);

        // Assert
        mockNotification.Verify(n => n.SendAsync(
            "test@example.com",
            "Order Placed",
            It.IsAny<string>()),
            Times.Once);
    }
}

SOLID in Real-World Applications

Before: Violating SOLID

// ❌ A typical "everything in one place" service
public class UserManager
{
    private readonly string _connectionString;

    public UserManager(string connectionString)
    {
        _connectionString = connectionString;
    }

    public User GetUser(int id)
    {
        using var connection = new SqlConnection(_connectionString);
        // Direct SQL query
        return connection.QueryFirstOrDefault<User>(
            "SELECT * FROM Users WHERE Id = @Id", new { Id = id });
    }

    public void CreateUser(User user)
    {
        // Validation
        if (string.IsNullOrEmpty(user.Email))
            throw new ArgumentException("Email required");

        if (!user.Email.Contains("@"))
            throw new ArgumentException("Invalid email");

        // Save to database
        using var connection = new SqlConnection(_connectionString);
        connection.Execute(
            "INSERT INTO Users (Name, Email) VALUES (@Name, @Email)", user);

        // Send welcome email
        using var smtpClient = new SmtpClient("smtp.example.com");
        smtpClient.Send("noreply@example.com", user.Email, "Welcome!", "...");

        // Log
        File.AppendAllText("log.txt", $"User created: {user.Email}");
    }

    public List<User> GetActiveUsers()
    {
        using var connection = new SqlConnection(_connectionString);
        return connection.Query<User>(
            "SELECT * FROM Users WHERE IsActive = 1").ToList();
    }

    // ... 50 more methods
}

After: Following SOLID

// ✅ Refactored following SOLID principles

// Domain entity
public class User
{
    public int Id { get; private set; }
    public string Name { get; private set; }
    public Email Email { get; private set; }
    public bool IsActive { get; private set; }

    public User(string name, Email email)
    {
        Name = name ?? throw new ArgumentNullException(nameof(name));
        Email = email ?? throw new ArgumentNullException(nameof(email));
        IsActive = true;
    }
}

// Value object with validation (SRP for email validation)
public record Email
{
    public string Value { get; }

    public Email(string value)
    {
        if (string.IsNullOrWhiteSpace(value))
            throw new ArgumentException("Email cannot be empty");

        if (!value.Contains('@'))
            throw new ArgumentException("Invalid email format");

        Value = value;
    }
}

// Repository interface (DIP)
public interface IUserRepository
{
    Task<User?> GetByIdAsync(int id);
    Task<IEnumerable<User>> GetActiveUsersAsync();
    Task AddAsync(User user);
}

// Notification interface (ISP, DIP)
public interface IWelcomeNotificationService
{
    Task SendWelcomeAsync(User user);
}

// Command/Handler pattern (SRP)
public class CreateUserCommand
{
    public string Name { get; set; }
    public string Email { get; set; }
}

public class CreateUserHandler
{
    private readonly IUserRepository _repository;
    private readonly IWelcomeNotificationService _welcomeService;
    private readonly ILogger<CreateUserHandler> _logger;

    public CreateUserHandler(
        IUserRepository repository,
        IWelcomeNotificationService welcomeService,
        ILogger<CreateUserHandler> logger)
    {
        _repository = repository;
        _welcomeService = welcomeService;
        _logger = logger;
    }

    public async Task<User> HandleAsync(CreateUserCommand command)
    {
        var email = new Email(command.Email);  // Validation in value object
        var user = new User(command.Name, email);

        await _repository.AddAsync(user);
        await _welcomeService.SendWelcomeAsync(user);

        _logger.LogInformation("User {Email} created", user.Email.Value);

        return user;
    }
}

// Query handler (SRP, CQRS)
public class GetActiveUsersHandler
{
    private readonly IUserRepository _repository;

    public GetActiveUsersHandler(IUserRepository repository)
    {
        _repository = repository;
    }

    public async Task<IEnumerable<User>> HandleAsync()
    {
        return await _repository.GetActiveUsersAsync();
    }
}

Common Violations in Production Code

1. Service Locator Anti-Pattern

// ❌ Violates DIP - hidden dependencies
public class BadService
{
    public void DoWork()
    {
        var repository = ServiceLocator.Get<IRepository>();  // Hidden dependency!
        var logger = ServiceLocator.Get<ILogger>();
    }
}

// ✅ Explicit dependencies via constructor
public class GoodService
{
    private readonly IRepository _repository;
    private readonly ILogger _logger;

    public GoodService(IRepository repository, ILogger logger)
    {
        _repository = repository;
        _logger = logger;
    }
}

2. Leaky Abstractions

// ❌ Interface exposes implementation details (violates ISP, LSP)
public interface IFileStorage
{
    void SaveToLocalDisk(string path, byte[] data);
    void SaveToS3(string bucket, string key, byte[] data);
    void SaveToAzureBlob(string container, string blobName, byte[] data);
}

// ✅ Abstract interface
public interface IFileStorage
{
    Task SaveAsync(string identifier, byte[] data);
    Task<byte[]> LoadAsync(string identifier);
}

3. Over-Engineering

// ❌ Too many abstractions for simple code
public interface IStringConcatenatorFactory
{
    IStringConcatenator Create();
}

// ✅ Sometimes simple is better
public string Concatenate(string a, string b) => a + b;

Interview Questions

1. Explain SRP with a real example.

Answer: SRP states a class should have one reason to change. Example: An Order class should handle order data, not email notifications. If email requirements change, it shouldn’t affect the Order class.

// Bad: Order handles notification
public class Order {
    public void PlaceOrder() {
        // Save order
        // Send email <-- Violation
    }
}

// Good: Separate concerns
public class Order { public void Place() { /* save only */ } }
public class OrderNotificationService { public void NotifyPlaced(Order o) { } }

2. How does OCP help with maintenance?

Answer: OCP means we can add new features by adding new code, not modifying existing code. This reduces regression risk.

Example: Payment types. Instead of adding cases to a switch statement, create new classes implementing IPaymentMethod. Existing code stays untouched.


3. What is an LSP violation you’ve seen?

Answer: Common example: A ReadOnlyCollection<T> that inherits from IList<T>. The Add method throws NotSupportedException, breaking code that expects any IList<T> to support adding items.

Better approach: Use IReadOnlyList<T> interface that doesn’t promise add capability.


4. When would you NOT follow ISP?

Answer: When creating highly cohesive interfaces where all methods are genuinely related and always used together. Over-segregation leads to too many tiny interfaces.

Example: IDisposable has only Dispose(). Adding CanDispose or IsDisposed to the same interface is fine since they’re closely related.


5. How do you explain DIP to a junior developer?

Answer: “Don’t create the things you depend on. Ask for them.”

Instead of new EmailService() inside your class, accept IEmailService in the constructor. This lets you:

  1. Swap implementations (email → SMS)
  2. Test with mocks
  3. Configure different services for dev/prod

Key Takeaways

  1. SRP: One class = one responsibility = one reason to change
  2. OCP: Add new features by adding code, not modifying existing code
  3. LSP: Subtypes must honor base type contracts
  4. ISP: Many specific interfaces beat one general-purpose interface
  5. DIP: Depend on abstractions, inject dependencies

Further Reading