DEV Community

Cover image for Learn SOLID Principles in C# with Examples for Better Code
DI Solutions
DI Solutions

Posted on • Updated on

Learn SOLID Principles in C# with Examples for Better Code

In software development, writing clean, maintainable, and scalable code is paramount. The SOLID principles are a set of design guidelines that help developers achieve these goals by promoting good practices in object-oriented design.

Originally introduced by Robert C. Martin, these five SOLID principles in C# with examples help developers avoid common pitfalls in software design, making code more understandable, flexible, and easier to maintain.
In this article, we will dive deep into each of the SOLID principles, providing clear explanations and practical C# examples to illustrate how they can be applied in real-world scenarios.

What Are SOLID Principles?

The term SOLID stands for the following five object-oriented programming
principles:

Single Responsibility Principle (SRP)
Open/Closed Principle (OCP)
Liskov Substitution Principle (LSP)
Interface Segregation Principle (ISP)
Dependency Inversion Principle (DIP)
Let’s break down each principle, understand its importance, and see how it can be implemented in C#.

1. Single Responsibility Principle (SRP)

Definition:

There should only be one cause for a class to change, which translates to one task or obligation.

Why It Matters:

The SRP encourages the division of code into smaller, more focused classes, making it easier to maintain and test. When a class is responsible for only one task, changes in the application’s requirements are less likely to introduce bugs in unrelated areas.

Example:

// Violation of SRP
public class Employee
{
public string Name { get; set; }
public double Salary { get; set; }

public void CalculateSalary() 
{
    // Calculate salary logic
}

public void GenerateReport() 
{
    // Generate report logic
}
Enter fullscreen mode Exit fullscreen mode

}
In the example above, the Employee class has more than one responsibility: it calculates the salary and generates a report. This violates the SRP.

Solution:

public class Employee
{
public string Name { get; set; }
public double Salary { get; set; }
}

public class SalaryCalculator
{
public double CalculateSalary(Employee employee)
{
// Salary calculation logic
}
}

public class ReportGenerator
{
public void GenerateReport(Employee employee)
{
// Report generation logic
}
}
By separating the responsibilities into different classes, we adhere to the SRP, making our code easier to maintain and extend.

2. Open/Closed Principle (OCP)

Definition:

Classes, modules, functions, and other software entities should be closed to alteration yet available for extension.

Why It Matters:

The OCP ensures that we can extend the behavior of our software without altering existing code, reducing the risk of introducing bugs and making the system more flexible.

Example:

// Violation of OCP
public class Rectangle
{
public double Width { get; set; }
public double Height { get; set; }
}

public class AreaCalculator
{
public double CalculateArea(Rectangle rectangle)
{
return rectangle.Width * rectangle.Height;
}
}

If we need to add support for calculating the area of a circle, we would have to modify the AreaCalculator class, violating the OCP.

Solution:

public abstract class Shape
{
public abstract double CalculateArea();
}

public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }

public override double CalculateArea()
{
    return Width * Height;
}
Enter fullscreen mode Exit fullscreen mode

}

public class Circle : Shape
{
public double Radius { get; set; }

public override double CalculateArea()
{
    return Math.PI * Radius * Radius;
}
Enter fullscreen mode Exit fullscreen mode

}

public class AreaCalculator
{
public double CalculateArea(Shape shape)
{
return shape.CalculateArea();
}
}

Now, the AreaCalculator can handle any shape without needing modification, adhering to the OCP.

3. Liskov Substitution Principle (LSP)
Definition:

It should be possible to swap out objects of a superclass for objects of a subclass without compromising the program's correctness.

Why It Matters:

The LSP ensures that derived classes can stand in for their base classes without causing unexpected behavior, making the code more robust and easier to understand.

Example:

// Violation of LSP
public class Bird
{
public virtual void Fly()
{
// Flying logic
}
}

public class Ostrich : Bird
{
public override void Fly()
{
throw new NotImplementedException("Ostriches can't fly.");
}
}
In this example, substituting Ostrich for Bird would cause an exception, violating the LSP.

Solution:

public abstract class Bird
{
// Common bird properties and methods
}

public class FlyingBird : Bird
{
public virtual void Fly()
{
// Flying logic
}
}

public class Ostrich : Bird
{
// Ostrich-specific properties and methods
}

By restructuring the hierarchy, we ensure that all subclasses can be substituted for their base classes without breaking the program’s logic.

4. Interface Segregation Principle (ISP)

Definition:

It is not appropriate to make a client implement interfaces it does not use. Many little interfaces are favored over one large interface.

Why It Matters:

It is not appropriate to make a client implement interfaces it does not use. Many little interfaces are favored over one large interface.

Example:

// Violation of ISP
public interface IWorker
{
void Work();
void Eat();
}

public class Robot : IWorker
{
public void Work()
{
// Work logic
}

public void Eat()
{
    throw new NotImplementedException("Robots don't eat.");
}
Enter fullscreen mode Exit fullscreen mode

}
Here, the Robot class is forced to implement the Eat method, even though it doesn't need it.

Solution:

public interface IWorkable
{
void Work();
}

public interface IFeedable
{
void Eat();
}

public class Human : IWorkable, IFeedable
{
public void Work()
{
// Work logic
}

public void Eat()
{
    // Eating logic
}
Enter fullscreen mode Exit fullscreen mode

}

public class Robot : IWorkable
{
public void Work()
{
// Work logic
}
}

By splitting the interface into smaller, more specific interfaces, we adhere to the ISP, making the code more flexible and easier to maintain.

5. Dependency Inversion Principle (DIP)
Definition:

High-level modules should not depend on low-level modules. Both should depend on abstractions. Additionally, abstractions should not depend on details. Details should depend on abstractions.

Why It Matters:

The DIP reduces the coupling between high-level and low-level components, making the system more modular and easier to modify or extend.

Example:

// Violation of DIP
public class LightBulb
{
public void TurnOn()
{
// Turn on the light
}

public void TurnOff()
{
    // Turn off the light
}
Enter fullscreen mode Exit fullscreen mode

}

public class Switch
{
private LightBulb _lightBulb;

public Switch(LightBulb lightBulb)
{
    _lightBulb = lightBulb;
}

public void Operate()
{
    _lightBulb.TurnOn();
}
Enter fullscreen mode Exit fullscreen mode

}

In this example, the Switch class depends directly on the LightBulb class, creating a tight coupling between the two.

Solution:

public interface IDevice
{
void TurnOn();
void TurnOff();
}

public class LightBulb : IDevice
{
public void TurnOn()
{
// Turn on the light
}

public void TurnOff()
{
    // Turn off the light
}
Enter fullscreen mode Exit fullscreen mode

}

public class Switch
{
private IDevice _device;

public Switch(IDevice device)
{
    _device = device;
}

public void Operate()
{
    _device.TurnOn();
}
Enter fullscreen mode Exit fullscreen mode

}

By introducing an interface (IDevice), we decouple the Switch from the LightBulb, making the code more flexible and adhering to the DIP.

Read More:- Hire Dedicated React.js Developers: Unlocking the Power of Top Talent for Your Projects

Conclusion

Understanding and applying the SOLID principles in your C# projects is a crucial step towards writing high-quality, maintainable code. These principles help developers design systems that are easier to understand, extend, and maintain, ultimately leading to more robust and scalable software.

When you hire .NET developers who are well-versed in these principles, you ensure that your projects are built on a strong foundation of best practices.

By following the SOLID principles, you can improve your code's structure and make it more resilient to changes and easier to test. Remember, these principles are not just theoretical guidelines; they are practical tools that can significantly enhance your development process.

Start applying SOLID principles in your projects today, and experience the difference in your code quality and productivity!

Top comments (0)