SOLID Principles in C#

 


Understanding SOLID Principles in C# 

As a C# developer, writing clean, maintainable, and scalable code is essential. One of the best ways to achieve this is by following the SOLID principles. These five principles help us design better software by making our code more modular, understandable, and easier to modify in the future. In this blog, we'll break down each SOLID principle in simple terms with easy-to-understand C# examples.


1. Single Responsibility Principle (SRP)

"A class should have only one reason to change."

This means that each class should only have one job or responsibility. If a class has multiple responsibilities, changes in one area might impact other functionalities, leading to bugs and complications.

Example:

public class Invoice
{
    public void GenerateInvoice() { /* Code to generate invoice */ }
    public void PrintInvoice() { /* Code to print invoice */ }
}

Problem: The Invoice class has two responsibilities: generating and printing invoices. This violates SRP.

Better Approach:

public class InvoiceGenerator
{
    public void GenerateInvoice() { /* Code to generate invoice */ }
}

public class InvoicePrinter
{
    public void PrintInvoice() { /* Code to print invoice */ }
}

Now, each class has only one responsibility, making the code more maintainable.


2. Open/Closed Principle (OCP)

"A class should be open for extension but closed for modification."

This means that we should be able to add new functionality without changing the existing code.

Example (Bad Practice):

public class PaymentProcessor
{
    public void ProcessPayment(string paymentType)
    {
        if (paymentType == "CreditCard") { /* Process credit card payment */ }
        else if (paymentType == "PayPal") { /* Process PayPal payment */ }
    }
}

Problem: Every time a new payment type is added, we have to modify the existing ProcessPayment method, which violates OCP.

Better Approach (Using Polymorphism):

public interface IPayment
{
    void ProcessPayment();
}

public class CreditCardPayment : IPayment
{
    public void ProcessPayment() { /* Process credit card payment */ }
}

public class PayPalPayment : IPayment
{
    public void ProcessPayment() { /* Process PayPal payment */ }
}

public class PaymentProcessor
{
    public void ProcessPayment(IPayment payment)
    {
        payment.ProcessPayment();
    }
}

Now, we can add new payment types without modifying the existing code!


3. Liskov Substitution Principle (LSP)

"Subtypes must be substitutable for their base types."

This principle ensures that objects of a derived class should be able to replace objects of the base class without breaking the application.

Example (Bad Practice):

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

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

Problem: Penguin violates LSP because it overrides the Fly method in a way that breaks the expected behavior.

Better Approach:

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

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

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

Now, each subclass behaves as expected without breaking the behavior of the base class.


4. Interface Segregation Principle (ISP)

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

This principle encourages creating smaller, specific interfaces rather than one large general-purpose interface.

Example (Bad Practice):

public interface IWorker
{
    void Work();
    void Eat();
}

public class Robot : IWorker
{
    public void Work() { Console.WriteLine("Working"); }
    public void Eat() { throw new NotImplementedException(); }
}

Problem: Robot is forced to implement Eat(), which it doesn’t need.

Better Approach:

public interface IWorkable
{
    void Work();
}

public interface IEatable
{
    void Eat();
}

public class Human : IWorkable, IEatable
{
    public void Work() { Console.WriteLine("Working"); }
    public void Eat() { Console.WriteLine("Eating"); }
}

public class Robot : IWorkable
{
    public void Work() { Console.WriteLine("Working"); }
}

Now, classes only implement the interfaces relevant to them.


5. Dependency Inversion Principle (DIP)

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

This principle encourages using dependency injection and abstraction to reduce coupling.

Example (Bad Practice):

public class EmailService
{
    public void SendEmail() { Console.WriteLine("Sending Email"); }
}

public class Notification
{
    private EmailService _emailService = new EmailService();
    public void NotifyUser() { _emailService.SendEmail(); }
}

Problem: Notification is tightly coupled to EmailService.

Better Approach:

public interface IMessageService
{
    void SendMessage();
}

public class EmailService : IMessageService
{
    public void SendMessage() { Console.WriteLine("Sending Email"); }
}

public class SMSService : IMessageService
{
    public void SendMessage() { Console.WriteLine("Sending SMS"); }
}

public class Notification
{
    private readonly IMessageService _messageService;
    public Notification(IMessageService messageService)
    {
        _messageService = messageService;
    }

    public void NotifyUser() { _messageService.SendMessage(); }
}

Now, Notification can use any messaging service without being tightly coupled.


Conclusion

By applying SOLID principles in our C# projects, we make our code more readable, maintainable, and scalable. These principles help us write better software that is easier to extend and modify with minimal changes. Start implementing SOLID principles today and see the difference in your code quality! 🚀

0 Comments