DEV Community

Mark Jack
Mark Jack

Posted on

YASA - Yet another S.O.L.I.D. article

Image description

Understanding SOLID Principles in .NET: Write Better, Scalable, and Maintainable Code

The SOLID principles are the cornerstone of good software design. Whether you're building small applications or large enterprise systems in .NET, applying these principles can help you write code that is easier to understand, maintain, and extend.

In this article, we'll explore each of the SOLID principles with practical C# examples in the .NET ecosystem.


🧱 What is SOLID?

SOLID is an acronym for five object-oriented design principles:

  • S – Single Responsibility Principle
  • O – Open/Closed Principle
  • L – Liskov Substitution Principle
  • I – Interface Segregation Principle
  • D – Dependency Inversion Principle

These principles were introduced by Robert C. Martin and have become a standard for writing clean and maintainable software.


1. Single Responsibility Principle (SRP)

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

This means that a class should only have one job or responsibility.

❌ Bad Example

public class Invoice
{
    public void CalculateTotal() { /* ... */ }
    public void SaveToDatabase() { /* ... */ }
    public void PrintInvoice() { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

Here, Invoice is doing too much — it calculates, persists data, and handles printing.

✅ Good Example

public class InvoiceCalculator
{
    public void CalculateTotal() { /* ... */ }
}

public class InvoiceRepository
{
    public void SaveToDatabase() { /* ... */ }
}

public class InvoicePrinter
{
    public void PrintInvoice() { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

Each class now has a single responsibility.


2. Open/Closed Principle (OCP)

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

This principle promotes the use of abstractions so that the behavior of a module can be extended without modifying its source code.

❌ Bad Example

public class Discount
{
    public double CalculateDiscount(string customerType)
    {
        if (customerType == "Regular") return 0.1;
        if (customerType == "Premium") return 0.2;
        return 0;
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ Good Example

public interface IDiscountStrategy
{
    double GetDiscount();
}

public class RegularCustomerDiscount : IDiscountStrategy
{
    public double GetDiscount() => 0.1;
}

public class PremiumCustomerDiscount : IDiscountStrategy
{
    public double GetDiscount() => 0.2;
}
Enter fullscreen mode Exit fullscreen mode

Now you can add new strategies without modifying existing code.


3. Liskov Substitution Principle (LSP)

"Derived classes must be substitutable for their base classes."

This principle asserts that objects of a superclass should be replaceable with objects of its subclasses without altering the correctness of the program. In simpler terms, subclasses should enhance, not weaken or break, the behavior promised by the base class.

Violating LSP often introduces unexpected behavior, especially in polymorphic code, and can lead to fragile systems.

❌ Bad Example

public class Bird
{
    public virtual void Fly() { /* default flying logic */ }
}

public class Ostrich : Bird
{
    public override void Fly()
    {
        throw new NotImplementedException();
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, Ostrich inherits from Bird and overrides the Fly method by throwing an exception. This violates LSP because any code that expects a Bird to fly safely might crash or behave incorrectly when passed an Ostrich.

✅ Better Design

public interface IBird { }

public interface IFlyingBird : IBird
{
    void Fly();
}

public class Sparrow : IFlyingBird
{
    public void Fly() { /* flying logic */ }
}

public class Ostrich : IBird
{
    // No Fly method; ostriches do not fly
}
Enter fullscreen mode Exit fullscreen mode

This approach respects LSP by not requiring Ostrich to implement functionality it does not possess. Clients depending on IFlyingBird know all implementations can fly, while those dealing with general IBird don't make assumptions about flying.

🧠 Key Takeaways

  • Avoid forcing subclasses to override or disable base class functionality.
  • Design with appropriate abstractions that reflect real capabilities.
  • Ensure derived classes honor the expectations set by the base class.

By following LSP, you ensure that components remain interchangeable, reducing bugs and making your code more robust and extensible.


4. Interface Segregation Principle (ISP)

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

❌ Bad Example

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

public class Robot : IWorker
{
    public void Work() { /* ... */ }
    public void Eat() => throw new NotImplementedException();
}
Enter fullscreen mode Exit fullscreen mode

✅ Good Example

public interface IWorkable
{
    void Work();
}

public interface IEatable
{
    void Eat();
}

public class Human : IWorkable, IEatable
{
    public void Work() { /* ... */ }
    public void Eat() { /* ... */ }
}

public class Robot : IWorkable
{
    public void Work() { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

Separate interfaces make code more flexible.


5. Dependency Inversion Principle (DIP)

"Depend on abstractions, not on concretions."

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

❌ Bad Example

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

public class Notification
{
    private EmailService _emailService = new EmailService();
    public void Send(string message)
    {
        _emailService.SendEmail(message);
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ Good Example

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

public class EmailService : IMessageService
{
    public void Send(string message) { /* ... */ }
}

public class Notification
{
    private readonly IMessageService _messageService;

    public Notification(IMessageService messageService)
    {
        _messageService = messageService;
    }

    public void Notify(string message)
    {
        _messageService.Send(message);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now Notification is not tightly coupled to EmailService.


✅ Final Thoughts

Applying the SOLID principles in .NET helps you build:

  • Modular and testable components
  • Easier-to-understand codebases
  • Scalable applications ready for change

While not every class needs to follow all principles strictly, being mindful of SOLID will gradually elevate the quality of your code.


If you found this article helpful follow me on GitHub, Twitter and Bluesky for more content.

Thanks for reading!

Top comments (0)