DEV Community

ClĂłvis Chakrian
ClĂłvis Chakrian

Posted on

🚀 SOLID: The Practical Guide to Maintainable Code

If you've ever worked on a project that started off simple but quickly became a maintenance nightmare, you’ve likely dealt with rigid, tightly coupled, and hard-to-scale code.

The SOLID principles exist to prevent exactly that. They help you write code that’s more organized, flexible, and easier to maintain. Let’s go through them with clear explanations and practical examples!


đź“Ś S - Single Responsibility Principle (SRP)

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

Imagine a class that does everything: processes orders, generates invoices, and sends emails.

public class OrderService
{
    public void ProcessOrder() { /* logic */ }
    public void GenerateInvoice() { /* logic */ }
    public void SendEmail() { /* logic */ }
}
Enter fullscreen mode Exit fullscreen mode

❌ The problem? Any change in the invoice logic could unintentionally impact email sending.

âś… Solution: Separate responsibilities:

public class OrderProcessor { /* Processes the order */ }
public class InvoiceService { /* Generates invoices */ }
public class EmailService { /* Sends emails */ }
Enter fullscreen mode Exit fullscreen mode

Now, each class has a single responsibility, making maintenance much easier.


đź“Ś O - Open/Closed Principle (OCP)

"Code should be open for extension but closed for modification."

Imagine a system that calculates discounts for different customers:

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

❌ The problem? Adding a new customer type requires modifying this class, breaking OCP.

âś… Solution: Use an extensible structure:

public interface IDiscount { decimal Apply(); }

public class PremiumDiscount : IDiscount { public decimal Apply() => 0.2m; }
public class RegularDiscount : IDiscount { public decimal Apply() => 0.1m; }

public class Order
{
    private readonly IDiscount _discount;
    public Order(IDiscount discount) { _discount = discount; }
    public decimal CalculateDiscount() => _discount.Apply();
}
Enter fullscreen mode Exit fullscreen mode

Now, we can add new discount types without modifying existing code.


đź“Ś L - Liskov Substitution Principle (LSP)

"Subclass objects should be replaceable with superclass objects without breaking the code."

Let’s say we have a Bird class defining common behaviors:

public class Bird
{
    public virtual void Fly()
    {
        Console.WriteLine("The bird is flying!");
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, we want to add a penguin, but it can’t fly! If we override Fly() in a Penguin subclass, we get an issue:

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

❌ The problem? If a function expects a Bird but gets a Penguin, calling Fly() could crash the application.

âś… Solution: Restructure the hierarchy:

public abstract class Bird { }

public interface IFlyable
{
    void Fly();
}

public class Swallow : Bird, IFlyable
{
    public void Fly() => Console.WriteLine("The swallow is flying!");
}

public class Penguin : Bird
{
    public void Swim() => Console.WriteLine("The penguin is swimming!");
}
Enter fullscreen mode Exit fullscreen mode

Now, only birds that actually fly implement IFlyable, ensuring correct behavior.


đź“Ś I - Interface Segregation Principle (ISP)

"A class should not be forced to implement methods it doesn’t use."

Suppose we have an interface IEmployeeActions that forces all classes to implement unnecessary methods:

public interface IEmployeeActions
{
    void Work();
    void Manage();
}

public class Developer : IEmployeeActions
{
    public void Work() { /* logic */ }
    public void Manage() { throw new NotImplementedException(); }
}
Enter fullscreen mode Exit fullscreen mode

❌ The problem? A developer doesn’t manage, yet it’s forced to implement Manage().

âś… Solution: Create smaller, more specific interfaces:

public interface IWork { void Work(); }
public interface IManage { void Manage(); }

public class Developer : IWork
{
    public void Work() { /* logic */ }
}
Enter fullscreen mode Exit fullscreen mode

Now, each class implements only what it actually needs.


đź“Ś D - Dependency Inversion Principle (DIP)

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

Suppose an order service directly depends on a payment service:

public class OrderService
{
    private readonly PaymentService _paymentService = new PaymentService();

    public void ProcessOrder() { _paymentService.ProcessPayment(); }
}
Enter fullscreen mode Exit fullscreen mode

❌ The problem? If PaymentService changes, OrderService might break.

âś… Solution: Use dependency injection and abstractions:

public interface IPaymentService { void ProcessPayment(); }

public class OrderService
{
    private readonly IPaymentService _paymentService;

    public OrderService(IPaymentService paymentService)
    {
        _paymentService = paymentService;
    }

    public void ProcessOrder() { _paymentService.ProcessPayment(); }
}
Enter fullscreen mode Exit fullscreen mode

Now, we can swap payment service implementations without modifying OrderService.


🎯 Conclusion

SOLID principles aren’t just theoretical—they help create flexible and maintainable code. But remember: not everything needs to be over-abstracted. The key is to apply SOLID wisely, keeping code clean without overcomplicating it.

And you? Have you ever dealt with a project that completely ignored SOLID? Share your experience in the comments! 🚀

Playwright CLI Flags Tutorial

5 Playwright CLI Flags That Will Transform Your Testing Workflow

  • --last-failed: Zero in on just the tests that failed in your previous run
  • --only-changed: Test only the spec files you've modified in git
  • --repeat-each: Run tests multiple times to catch flaky behavior before it reaches production
  • --forbid-only: Prevent accidental test.only commits from breaking your CI pipeline
  • --ui --headed --workers 1: Debug visually with browser windows and sequential test execution

Learn how these powerful command-line options can save you time, strengthen your test suite, and streamline your Playwright testing experience. Practical examples included!

Watch Video 📹️

Top comments (0)

AWS Q Developer image

Your AI Code Assistant

Automate your code reviews. Catch bugs before your coworkers. Fix security issues in your code. Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE

đź‘‹ Kindness is contagious

Explore a trove of insights in this engaging article, celebrated within our welcoming DEV Community. Developers from every background are invited to join and enhance our shared wisdom.

A genuine "thank you" can truly uplift someone’s day. Feel free to express your gratitude in the comments below!

On DEV, our collective exchange of knowledge lightens the road ahead and strengthens our community bonds. Found something valuable here? A small thank you to the author can make a big difference.

Okay