SOLID principles make it easy for a developer to write easily extendable code and avoid common coding errors.
These principles were introduced by Robert C. Martin, and they have become a fundamental part of object-oriented programming.
In the context of .NET development, adhering to SOLID principles can lead to more modular, flexible, and maintainable code. In this article, we’ll delve into each SOLID principle with practical coding examples in C#.
Following are the five SOLID design principles:
1. Single Responsibility Principle (SRP)
The SRP states that a class should have only one reason to change, meaning it should have only one responsibility. This promotes modularization and makes the code easier to understand and maintain.
Key idea: A class should do only one thing, and it should do it well.
Real-time example: Think of a chef who only focuses on cooking, not managing the restaurant or delivering food.
Practical coding example in C#:
Before applying for SRP:
public class Report
{
public void GenerateReport() { }
public void SaveToFile() { }
}
In this scenario, the Report
class has two responsibilities: generating a report and saving it to a file. This violates the SRP.
After applying SRP:
public class Report
{
public void GenerateReport() { }
}
public class ReportSaver
{
public void SaveToFile() { }
}
Now, the Report
class is responsible only for generating reports, while the ReportSaver
class is responsible for saving reports. Each class has a single responsibility.
Explanation: According to SRP, one class should take one responsibility hence to overcome this problem we should write another class to save the report functionality. If you make any changes to theReport class will not affect the ReportSaver
class.
2. Open/Closed Principle (OCP)
The Open/Closed Principle suggests that a class should be open for extension but closed for modification. This means you can add new features without altering existing code.
Key idea: Once a class is written, it should be closed for modifications but open for extensions.
Real-time example: Your smartphone — you don’t open it up to add features; you just download apps to extend its capabilities.
Practical coding example in C#:
Before applying for 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;
}
}
This design may become problematic when adding new shapes. Modifying the AreaCalculator
for each new shape violates the OCP.
After applying for OCP:
public interface IShape
{
double CalculateArea();
}
public class Rectangle : IShape
{
// implementation
}
public class Circle : IShape
{
// implementation
}
By introducing an interface (IShape
), new shapes like Circle
can be added without modifying existing code, adhering to the OCP.
Explanation: According to OCP, the class should be open for extension but closed for modification. So, When you introduce a new shape, then just implement it from the interface IShape
. So IShape
is open for extension but closed for further modification.
3. Liskov Substitution Principle (LSP)
The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.
Key idea: You should be able to use any subclass where you use its parent class.
Real-time example: You have a remote control that works for all types of TVs, regardless of the brand.
Practical coding example in C#:
Before applying for LSP:
public class Bird
{
public virtual void Fly() { /* implementation */ }
}
public class Penguin : Bird
{
public override void Fly()
{
throw new NotImplementedException("Penguins can't fly!");
}
}
Here, the Penguin
class violates the LSP by throwing an exception for the Fly
method.
After applying for LSP:
public interface IFlyable
{
void Fly();
}
public class Bird : IFlyable
{
public void Fly()
{
// implementation specific to Bird
}
}
public class Penguin : IFlyable
{
public void Fly()
{
// implementation specific to Penguins
throw new NotImplementedException("Penguins can't fly!");
}
}
By introducing the IFlyable
interface, both Bird
and Penguin
adhere to the Liskov Substitution Principle.
Explanation: According to LSP, a derived class should not break the base class’s type definition and behavior which means objects of a base class shall be replaceable with objects of its derived classes without breaking the application. This needs the objects of derived classes to behave in the same way as the objects of your base class.
4. Interface Segregation Principle (ISP)
The Interface Segregation Principle states that a class should not be forced to implement interfaces it does not use. This principle encourages the creation of small, client-specific interfaces.
Key idea: A class should not be forced to implement interfaces it doesn’t use.
Real-time example: You sign up for a music streaming service and only choose the genres you like, not all available genres.
Practical coding example in C#:
Before applying for ISP:
public interface IWorker
{
void Work();
void Eat();
}
public class Manager : IWorker
{
// implementation
}
public class Robot : IWorker
{
// implementation
}
The Robot
class is forced to implement the Eat
method, violating ISP.
After applying for ISP:
public interface IWorkable
{
void Work();
}
public interface IEatable
{
void Eat();
}
public class Manager : IWorkable, IEatable
{
// implementation
}
public class Robot : IWorkable
{
// implementation
}
By splitting the IWorker
interface into smaller interfaces (IWorkable
and IEatable
), classes can implement only what they need, adhering to ISP.
Explanation: According to LSP, any client should not be forced to use an interface that is irrelevant to it. In other words, clients should not be forced to depend on methods that they do not use.
5. Dependency Inversion Principle (DIP)
The Dependency Inversion Principle suggests that high-level modules should not depend on low-level modules, but both should depend on abstractions. Additionally, abstractions should not depend on details; details should depend on abstractions.
Key idea: High-level modules should not depend on low-level modules; both should depend on abstractions.
Real-time example: Building a LEGO tower — the bricks (high and low-level modules) connect through smaller bricks (abstractions).
Practical coding example in C#:
Before applying DIP:
public class LightBulb
{
public void TurnOn() { /* implementation */ }
public void TurnOff() { /* implementation */ }
}
public class Switch
{
private LightBulb bulb;
public Switch(LightBulb bulb)
{
this.bulb = bulb;
}
public void Toggle()
{
if (bulb.IsOn)
bulb.TurnOff();
else
bulb.TurnOn();
}
}
The Switch
class directly depends on the concrete LightBulb
class, violating DIP.
After applying DIP:
public interface ISwitchable
{
void TurnOn();
void TurnOff();
}
public class LightBulb : ISwitchable
{
// implementation
}
public class Switch
{
private ISwitchable device;
public Switch(ISwitchable device)
{
this.device = device;
}
public void Toggle()
{
if (device.IsOn)
device.TurnOff();
else
device.TurnOn();
}
}
By introducing an interface (ISwitchable
), the Switch
class now depends on an abstraction, adhering to the Dependency Inversion Principle.
Explanation: According to DIP, do not write any tightly coupled code because that is a nightmare to maintain when the application is growing bigger and bigger. If a class depends on another class, then we need to change one class if something changes in that dependent class. We should always try to write loosely coupled classes.
Conclusion
Remember, by understanding and applying these SOLID principles, .NET developers can create more robust, flexible, and maintainable software. It’s important to note that these principles work together and complement each other, contributing to the overall design philosophy of object-oriented programming.