As developers, writing clean, maintainable, and scalable code is always a top priority. One of the best ways to achieve this is by following the SOLID principles. SOLID is an acronym for five design principles that help us write better object-oriented code. Let's break them down one by one with simple examples in C#.

1. S – Single Responsibility Principle (SRP)

The Single Responsibility Principle (SRP) states that a class should have only one reason to change. In simple words, each class should focus on one specific job and do it well. For example, if a class handles both database operations and report generation, any change in either functionality will force you to modify the same class. This makes the code harder to maintain and more prone to errors. By splitting responsibilities into separate classes, your code becomes cleaner, easier to test, and more flexible for future changes.

Example

// Poor Practice
class Employee 
{
    public void CalculateSalary() { /* ... */ }
    public void SaveToDatabase() { /* ... */ }
}

// Good Example
class Employee 
{
    public void CalculateSalary() 
     { /* ... */ }
}

class EmployeeRepository 
{
    public void SaveToDatabase(Employee employee) 
     { /* ... */ }
}

Each class now has one clear reason to change, making the code cleaner, safer, and easier to maintain.

2. O – Open/Closed Principle (OCP)

The Open/Closed Principle means your code should be open for extension but closed for modification. In other words, you should be able to add new features without changing the existing code that already works. Keeping existing code "closed" prevents breaking anything, while being "open" allows your software to grow and adapt safely. Using interfaces, inheritance, or polymorphism is a practical way to achieve this in C#.

// Poor Practice
class Discount 
{
    public double Calculate(double price, string type) 
    {
        if(type == "Gold") return price * 0.9;
        if(type == "Silver") return price * 0.95;
        return price;
    }
}

// Good Practice
interface IDiscount 
{
    double Apply(double price);
}

class GoldDiscount : IDiscount 
{
    public double Apply(double price) => price * 0.9;
}

class SilverDiscount : IDiscount 
{
    public double Apply(double price) => price * 0.95;
}

3. L – Liskov Substitution Principle (LSP)

The Liskov Substitution Principle (LSP) says that derived classes should be able to replace their base classes without breaking the program. In other words, if your code works with a base class, it should also work correctly with any subclass. This ensures that adding new types through inheritance doesn't cause unexpected errors, keeping your software reliable, predictable, and easy to extend.

Example

// Poor Practice
class Bird 
{
    public virtual void Fly() 
     {
        /* ... */  
     }
}

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

// Good Practice : Interface Declearation
interface IFlyable 
{
    void Fly();
}

class Sparrow : IFlyable 
{
    public void Fly()
     { 
       /* Flying logic */ 
     }
}

class Ostrich 
{
    // No Fly method needed
}

4. I – Interface Segregation Principle (ISP)

The Interface Segregation Principle (ISP) means that clients (classes) should only be required to use the methods they actually need. In other words, a class shouldn't be forced to implement methods it will never use. By creating smaller, more focused interfaces, you keep your code clean, easier to maintain, and less confusing.

Example

// Poor Practice
interface IMachine 
{
    void Print();
    void Scan();
    void Fax();
}

class OldPrinter : IMachine 
{
    public void Print() 
     { 
        /*...*/ 
     }
    public void Scan() 
     { 
        throw new NotImplementedException(); 
     }
    public void Fax() 
     { 
        throw new NotImplementedException(); 
     }
}

// Good Practice : using Interface
interface IPrinter 
{ 
  void Print(); 
}
interface IScanner 
{ 
  void Scan(); 
}

class ModernPrinter : IPrinter, IScanner 
{
    public void Print() 
     {
        / * Logic * /
     }
    public void Scan() 
     { 
       / * Logic * /
     }
}

💡 Note: In C#, a class cannot inherit from multiple classes, but it can implement multiple interfaces. This allows each class to pick only the features it needs, which is exactly what the Interface Segregation Principle promotes.

5. D – Dependency Inversion Principle (DIP)

The Dependency Inversion Principle (DIP) means that high-level code should not directly depend on low-level details. Instead, both should rely on abstractions, like interfaces. This way, your high-level modules (like business logic) don't break if you change low-level modules (like databases or services). By depending on abstractions, your code becomes more flexible, easier to maintain, and simpler to test.

Example

// Poor Practice
class MySQLDatabase 
{
    public void Connect() 
     {
        /* DB Connection Logic */ 
     }
}

class UserService 
{
    private MySQLDatabase _db = new MySQLDatabase();
}

// Good Practice
interface IDatabase 
{
    void Connect();
}

class MySQLDatabase : IDatabase 
{
    public void Connect()
     { 
       /* DB Connection Logic */ 
     }
}

class UserService 
{
    private readonly IDatabase _db;
    public UserService(IDatabase db) 
     {
        _db = db;
     }
}

Conclusion

Applying the SOLID principles makes writing C# code easier, cleaner, and more maintainable. By keeping each class focused on one task, relying on abstractions instead of concrete implementations, and designing simple, clear interfaces, your code becomes flexible, safe to change, and less error-prone. Using these principles regularly will not only improve your code but also make you a stronger, more confident C# developer.

Coming Up Next: Ready to take your C# skills further? In the next article, we'll explore Dependency Injection (DI) with clear examples, showing how to make your code cleaner, more flexible, and easier to maintain. Don't miss it!