SOLID

Single Responsibility Principle

Each class should have only one reason to change, meaning it should have only one job or responsibility. This makes classes more focused and easier to maintain.

// Bad example - multiple responsibilities
public class Customer
{
    public void SaveToDatabase()
    {
        // database logic
    }

    public void GenerateReport()
    {
        // report logic
    }

    public void CalculateMetrics()
    {
        // metrics logic
    }
}

// Good example - single responsibility
public class Customer
{
    public string Name { get; set; }
    public string Email { get; set; }
}

public class CustomerRepository
{
    public void Save(Customer customer)
    {
        // database logic
    }
}

public class CustomerReportGenerator
{
    public void Generate(Customer customer)
    {
        // report logic
    }
}

Open/Closed Principle

Software entities should be open for extension but closed for modification. You should be able to add new functionality without changing existing code.

// Bad example
public class PaymentProcessor
{
    public void ProcessPayment(string paymentType)
    {
        if (paymentType == "Credit")
        {
            // process credit payment
        }
        else if (paymentType == "Debit")
        {
            // process debit payment
        }
        // Adding new payment types requires modifying this class
    }
}

// Good example
public interface IPaymentMethod
{
    void Process();
}

public class CreditPayment : IPaymentMethod
{
    public void Process()
    {
        // process credit payment
    }
}

public class DebitPayment : IPaymentMethod
{
    public void Process()
    {
        // process debit payment
    }
}

Liskov Substitution Principle

Objects of a superclass should be replaceable with objects of its subclasses without breaking the application. Subclasses should extend, not change, the base class behavior.

// Bad example
public class Bird
{
    public virtual void Fly()
    {
        // flying implementation
    }
}

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

// Good example
public abstract class Bird
{
    public abstract void Move();
}

public class FlyingBird : Bird
{
    public override void Move()
    {
        // flying implementation
    }
}

public class SwimmingBird : Bird
{
    public override void Move()
    {
        // swimming implementation
    }
}

Interface Segregation Principle

Clients should not be forced to depend on interfaces they don’t use. It’s better to have many specific interfaces rather than one general-purpose interface.

// Bad example
public interface IWorker
{
    void Work();
    void Eat();
    void Sleep();
}

// Good example
public interface IWorkable
{
    void Work();
}

public interface IEatable
{
    void Eat();
}

public class Human : IWorkable, IEatable
{
    public void Work()
    {
        // work implementation
    }

    public void Eat()
    {
        // eat implementation
    }
}

Dependency Inversion Principle

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.

// Bad example
public class EmailSender
{
    public void SendEmail(string message)
    {
        // send email implementation
    }
}

public class NotificationService
{
    private readonly EmailSender _emailSender;

    public NotificationService()
    {
        _emailSender = new EmailSender(); // Direct dependency
    }
}

// Good example
public interface IMessageSender
{
    void Send(string message);
}

public class EmailSender : IMessageSender
{
    public void Send(string message)
    {
        // send email implementation
    }
}

public class NotificationService
{
    private readonly IMessageSender _sender;

    public NotificationService(IMessageSender sender) // Dependency injection
    {
        _sender = sender;
    }
}

Following these principles helps create code that is:

  • Easier to maintain and test
  • More flexible and adaptable to changes
  • More reusable and modular
  • Less prone to bugs when making modifications

The key is to find the right balance - while these principles are valuable guidelines, they shouldn’t be followed so rigidly that they make your code overly complex for simple problems.




Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • Google Gemini updates: Flash 1.5, Gemma 2 and Project Astra
  • Displaying External Posts on Your al-folio Blog
  • TMUX