SOLID Design Principles

Dhirendra Singh Negi - Sep 18 - - Dev Community

SOLID Design Principles

The SOLID design principles are fundamental guidelines that help developers create robust, maintainable, and scalable software. Here’s an overview of each principle, explained with real-time examples in C#.

1. Single Responsibility Principle (SRP)

Definition: A class should have only one reason to change, meaning it should have only one job or responsibility.

Example:
Imagine you have a class that handles both user authentication and sending email notifications. This violates SRP because the class is doing two unrelated things.

Violation Example:


public class UserService
{
    public void RegisterUser(string username, string password)
    {
        // Code to register the user

        // Code to send email notification
        SendEmailNotification(username);
    }

    private void SendEmailNotification(string username)
    {
        // Email sending logic
    }
}
Enter fullscreen mode Exit fullscreen mode

Refactored (SRP Applied):

public class UserService
{
    private readonly EmailService _emailService;

    public UserService(EmailService emailService)
    {
        _emailService = emailService;
    }

    public void RegisterUser(string username, string password)
    {
        // Code to register the user
        _emailService.SendEmailNotification(username);
    }
}

public class EmailService
{
    public void SendEmailNotification(string username)
    {
        // Email sending logic
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, UserService is only responsible for user registration, and EmailService is responsible for sending email notifications.

2. Open-Closed Principle (OCP)

Definition: Classes should be open for extension but closed for modification. You should be able to extend the behavior of a class without altering its source code.

Example:
A payment system that supports only one type of payment method.

Violation Example:

public class PaymentService
{
    public void ProcessPayment(string paymentType)
    {
        if (paymentType == "CreditCard")
        {
            // Process credit card payment
        }
        else if (paymentType == "PayPal")
        {
            // Process PayPal payment
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Adding a new payment method requires modifying the PaymentService class, which violates OCP.

Refactored (OCP Applied):

public abstract class PaymentMethod
{
    public abstract void ProcessPayment();
}

public class CreditCardPayment : PaymentMethod
{
    public override void ProcessPayment()
    {
        // Process credit card payment
    }
}

public class PayPalPayment : PaymentMethod
{
    public override void ProcessPayment()
    {
        // Process PayPal payment
    }
}

public class PaymentService
{
    public void ProcessPayment(PaymentMethod paymentMethod)
    {
        paymentMethod.ProcessPayment();
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, adding a new payment method involves extending the PaymentMethod class without changing the PaymentService class.

3. Liskov Substitution Principle (LSP)

Definition: Objects of a base class should be replaceable with objects of a derived class without affecting the correctness of the program.

Example:
Consider a base class Bird and a derived class Penguin. If Penguin cannot substitute Bird in a meaningful way, this violates LSP.

Violation Example:


public class Bird
{
    public virtual void Fly()
    {
        Console.WriteLine("Bird is flying");
    }
}

public class Penguin : Bird
{
    public override void Fly()
    {
        throw new NotImplementedException("Penguins can't fly");
    }
}
Enter fullscreen mode Exit fullscreen mode

Calling the Fly method on a Penguin object will throw an exception, violating LSP.

Refactored (LSP Applied):

public abstract class Bird
{
    public abstract void Move();
}

public class Sparrow : Bird
{
    public override void Move()
    {
        Console.WriteLine("Sparrow is flying");
    }
}

public class Penguin : Bird
{
    public override void Move()
    {
        Console.WriteLine("Penguin is swimming");
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, both Sparrow and Penguin can substitute Bird, and each class correctly implements the Move method according to its behavior.

4. Interface Segregation Principle (ISP)

Definition: Clients should not be forced to depend on methods they do not use. Instead of one large interface, split it into smaller, specific ones.

Example:
A large interface that forces classes to implement methods they don’t need.

Violation Example:

public interface IMultiFunctionDevice
{
    void Print();
    void Scan();
    void Fax();
}

public class BasicPrinter : IMultiFunctionDevice
{
    public void Print() { /* Print logic */ }
    public void Scan() { throw new NotImplementedException(); }
    public void Fax() { throw new NotImplementedException(); }
}
Enter fullscreen mode Exit fullscreen mode

BasicPrinter is forced to implement Scan and Fax, even though it doesn't need them.

Refactored (ISP Applied):

public interface IPrinter
{
    void Print();
}

public interface IScanner
{
    void Scan();
}

public interface IFax
{
    void Fax();
}

public class BasicPrinter : IPrinter
{
    public void Print() { /* Print logic */ }
}
Enter fullscreen mode Exit fullscreen mode

Now, BasicPrinter only implements the IPrinter interface, which is what it actually needs.

5. Dependency Inversion Principle (DIP)

Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions (interfaces). Also, abstractions should not depend on details; details should depend on abstractions.

Example:
A class directly depends on another class instead of an abstraction.

Violation Example:

public class EmailService
{
    public void SendEmail(string message) { /* Email logic */ }
}

public class Notification
{
    private readonly EmailService _emailService = new EmailService();

    public void SendNotification(string message)
    {
        _emailService.SendEmail(message);
    }
}
Enter fullscreen mode Exit fullscreen mode

Notification depends directly on EmailService, making it difficult to switch to another notification method.

Refactored (DIP Applied):

public interface INotificationService
{
    void Send(string message);
}

public class EmailService : INotificationService
{
    public void Send(string message) { /* Email logic */ }
}

public class Notification
{
    private readonly INotificationService _notificationService;

    public Notification(INotificationService notificationService)
    {
        _notificationService = notificationService;
    }

    public void SendNotification(string message)
    {
        _notificationService.Send(message);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, Notification depends on the INotificationService abstraction, making it flexible to switch between different notification services (e.g., SMS, Push).

Summary
The SOLID design principles in C# make software design more modular, maintainable, and adaptable. Here's a quick recap:

  • SRP: One responsibility per class.
  • OCP: Extend classes without modifying existing code.
  • LSP: Subclasses should behave as their parent class.
  • ISP: Avoid large interfaces; split them based on functionality.
  • DIP: Depend on abstractions, not concrete implementations. These principles help developers create software that is easier to maintain, extend, and scale over time.
.
Terabox Video Player