The SOLID principles, introduced by Robert C. Martin (Uncle Bob), are a set of design principles that make software more maintainable, scalable, and robust. They are essential for developers aiming to build high-quality, clean, and sustainable software.
What Does SOLID Stand For?
- S: Single Responsibility Principle (SRP)
- O: Open/Closed Principle (OCP)
- L: Liskov Substitution Principle (LSP)
- I: Interface Segregation Principle (ISP)
- D: Dependency Inversion Principle (DIP)
Let’s explore each principle in detail with examples.
1. Single Responsibility Principle (SRP)
Definition: A class should have only one reason to change.
This means a class should focus on a single responsibility or functionality.
Example:
Bad Design:
public class Report
{
public void GenerateReport()
{
// Logic for generating a report
}
public void SaveToDatabase()
{
// Logic for saving the report to the database
}
public void SendEmail()
{
// Logic for sending the report via email
}
}
In this design, the Report
class has multiple responsibilities: generating a report, saving it, and emailing it. Any change in one feature might impact the others.
Better Design:
public class ReportGenerator
{
public void GenerateReport()
{
// Logic for generating a report
}
}
public class ReportSaver
{
public void SaveToDatabase()
{
// Logic for saving the report to the database
}
}
public class EmailSender
{
public void SendEmail()
{
// Logic for sending the report via email
}
}
2. Open/Closed Principle (OCP)
Definition: A class should be open for extension but closed for modification.
This means you should be able to add new functionality without changing existing code.
Example:
Bad Design:
public class PaymentProcessor
{
public void ProcessPayment(string paymentType)
{
if (paymentType == "CreditCard")
{
// Process credit card payment
}
else if (paymentType == "PayPal")
{
// Process PayPal payment
}
}
}
To add a new payment type, you’d need to modify the class.
Better Design:
public interface IPayment
{
void Process();
}
public class CreditCardPayment : IPayment
{
public void Process()
{
// Process credit card payment
}
}
public class PayPalPayment : IPayment
{
public void Process()
{
// Process PayPal payment
}
}
public class PaymentProcessor
{
public void ProcessPayment(IPayment payment)
{
payment.Process();
}
}
Now, adding a new payment type only requires creating a new class that implements IPayment
.
3. Liskov Substitution Principle (LSP)
Definition: Subtypes should be substitutable for their base types without altering the correctness of the program.
Example:
Bad Design:
public class Rectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
}
public class Square : Rectangle
{
public override int Width
{
set { base.Width = base.Height = value; }
}
public override int Height
{
set { base.Width = base.Height = value; }
}
}
Here, substituting Square
for Rectangle
can break the program logic.
Better Design:
public interface IShape
{
int Area();
}
public class Rectangle : IShape
{
public int Width { get; set; }
public int Height { get; set; }
public int Area()
{
return Width * Height;
}
}
public class Square : IShape
{
public int Side { get; set; }
public int Area()
{
return Side * Side;
}
}
4. Interface Segregation Principle (ISP)
Definition: A class should not be forced to implement interfaces it does not use.
Example:
Bad Design:
public interface IPrinter
{
void Print();
void Scan();
void Fax();
}
public class BasicPrinter : IPrinter
{
public void Print()
{
// Print implementation
}
public void Scan()
{
throw new NotImplementedException();
}
public void Fax()
{
throw new NotImplementedException();
}
}
Better Design:
public interface IPrinter
{
void Print();
}
public interface IScanner
{
void Scan();
}
public interface IFax
{
void Fax();
}
public class BasicPrinter : IPrinter
{
public void Print()
{
// Print implementation
}
}
5. Dependency Inversion Principle (DIP)
Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions.
Abstractions should not depend on details. Details should depend on abstractions.
Example:
Bad Design:
public class LightBulb
{
public void TurnOn() { }
public void TurnOff() { }
}
public class Switch
{
private LightBulb _lightBulb = new LightBulb();
public void Toggle(bool isOn)
{
if (isOn)
_lightBulb.TurnOn();
else
_lightBulb.TurnOff();
}
}
Better Design:
public interface IDevice
{
void TurnOn();
void TurnOff();
}
public class LightBulb : IDevice
{
public void TurnOn() { }
public void TurnOff() { }
}
public class Switch
{
private readonly IDevice _device;
public Switch(IDevice device)
{
_device = device;
}
public void Toggle(bool isOn)
{
if (isOn)
_device.TurnOn();
else
_device.TurnOff();
}
}
Conclusion
By adhering to the SOLID principles, you can write cleaner, modular, and testable code, which leads to easier maintenance and scaling of your projects. Mastering these principles is a fundamental step for becoming a proficient software developer.