SOLID principles
The SOLID principles are a set of five design guidelines in object-oriented programming that aim to make software designs more understandable, flexible, and maintainable. The acronym stands for:
- Single Responsibility Principle (SRP)
- Open/Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
Below is an explanation of each principle with corresponding examples in C#.
1. Single Responsibility Principle (SRP)
A class should have only one reason to change, meaning it should have only one job or responsibility.
Example Without SRP:
Consider a Report
class that handles both report generation and file saving.
public class Report
{
public string GenerateReport()
{
// Generate report data
return "Report Data";
}
public void SaveToFile(string reportData)
{
// Save report data to a file
}
}
This class violates SRP because it has two responsibilities: generating reports and saving them.
Refactored Example Applying SRP:
Split the responsibilities into separate classes.
public class ReportGenerator
{
public string GenerateReport()
{
// Generate report data
return "Report Data";
}
}
public class FileSaver
{
public void SaveToFile(string data)
{
// Save data to a file
}
}
Now, each class has a single responsibility.
2. Open/Closed Principle (OCP)
Software entities should be open for extension but closed for modification.
Example Without OCP:
A PaymentProcessor
class handles different payment methods using conditional statements.
public class PaymentProcessor
{
public void ProcessPayment(string paymentMethod)
{
if (paymentMethod == "CreditCard")
{
// Process credit card payment
}
else if (paymentMethod == "PayPal")
{
// Process PayPal payment
}
}
}
Adding a new payment method requires modifying the existing class, violating OCP.
Refactored Example Applying OCP:
Use abstraction to allow extension without modifying existing code.
public interface IPaymentMethod
{
void Pay();
}
public class CreditCardPayment : IPaymentMethod
{
public void Pay()
{
// Process credit card payment
}
}
public class PayPalPayment : IPaymentMethod
{
public void Pay()
{
// Process PayPal payment
}
}
public class PaymentProcessor
{
public void ProcessPayment(IPaymentMethod paymentMethod)
{
paymentMethod.Pay();
}
}
New payment methods can be added by implementing IPaymentMethod
without changing PaymentProcessor
.
3. Liskov Substitution Principle (LSP)
Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.
Example Violating LSP:
A Rectangle
class and a Square
class where Square
inherits from Rectangle
.
public class Rectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public int Area => Width * Height;
}
public class Square : Rectangle
{
public override int Width
{
set { base.Width = base.Height = value; }
}
public override int Height
{
set { base.Width = base.Height = value; }
}
}
Using a Square
object where a Rectangle
is expected can lead to unexpected behavior.
Refactored Example Applying LSP:
Introduce a base interface or class that both shapes can inherit without violating LSP.
public interface IShape
{
int Area { get; }
}
public class Rectangle : IShape
{
public int Width { get; set; }
public int Height { get; set; }
public int Area => Width * Height;
}
public class Square : IShape
{
public int SideLength { get; set; }
public int Area => SideLength * SideLength;
}
Now, both Rectangle
and Square
adhere to LSP by implementing IShape
.
4. Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they do not use.
Example Without ISP:
An IMultiFunctionDevice
interface that combines multiple functionalities.
public interface IMultiFunctionDevice
{
void Print();
void Scan();
void Fax();
}
public class Printer : IMultiFunctionDevice
{
public void Print()
{
// Print implementation
}
public void Scan()
{
throw new NotImplementedException();
}
public void Fax()
{
throw new NotImplementedException();
}
}
The Printer
class is forced to implement methods it doesn't need.
Refactored Example Applying ISP:
Split the large interface into smaller, more specific interfaces.
public interface IPrinter
{
void Print();
}
public interface IScanner
{
void Scan();
}
public interface IFax
{
void Fax();
}
public class Printer : IPrinter
{
public void Print()
{
// Print implementation
}
}
Now, Printer
only depends on the IPrinter
interface.
5. Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules; both should depend on abstractions.
Example Without DIP:
A UserService
class directly depends on a concrete EmailSender
class.
public class EmailSender
{
public void SendEmail(string message)
{
// Send email logic
}
}
public class UserService
{
private EmailSender _emailSender = new EmailSender();
public void RegisterUser(string username)
{
// Registration logic
_emailSender.SendEmail("Welcome!");
}
}
This creates tight coupling between UserService
and EmailSender
.
Refactored Example Applying DIP:
Depend on abstractions by introducing an interface.
public interface IMessageSender
{
void SendMessage(string message);
}
public class EmailSender : IMessageSender
{
public void SendMessage(string message)
{
// Send email logic
}
}
public class SmsSender : IMessageSender
{
public void SendMessage(string message)
{
// Send SMS logic
}
}
public class UserService
{
private readonly IMessageSender _messageSender;
public UserService(IMessageSender messageSender)
{
_messageSender = messageSender;
}
public void RegisterUser(string username)
{
// Registration logic
_messageSender.SendMessage("Welcome!");
}
}
Now, UserService
is decoupled from specific implementations and depends on the abstraction IMessageSender
.
Conclusion
By following the SOLID principles, you create code that is easier to maintain, extend, and understand. Each principle addresses common pitfalls in software design, helping you to:
- Reduce tight coupling
- Enhance code readability
- Facilitate testing and debugging
- Encourage reusable components
Implementing these principles leads to robust and flexible software architecture.
If you have enjoyed this post, please consider buying me a coffee ☕ to help me keep writing!