SOLID Design Principles
The SOLID design principles are fundamental guidelines that help developers create robust, maintainable, and scalable software. Here’s an overview of each principle, explained with real-time examples in C#.
1. Single Responsibility Principle (SRP)
Definition: A class should have only one reason to change, meaning it should have only one job or responsibility.
Example:
Imagine you have a class that handles both user authentication and sending email notifications. This violates SRP because the class is doing two unrelated things.
Violation Example:
public class UserService
{
public void RegisterUser(string username, string password)
{
// Code to register the user
// Code to send email notification
SendEmailNotification(username);
}
private void SendEmailNotification(string username)
{
// Email sending logic
}
}
Refactored (SRP Applied):
public class UserService
{
private readonly EmailService _emailService;
public UserService(EmailService emailService)
{
_emailService = emailService;
}
public void RegisterUser(string username, string password)
{
// Code to register the user
_emailService.SendEmailNotification(username);
}
}
public class EmailService
{
public void SendEmailNotification(string username)
{
// Email sending logic
}
}
Now, UserService is only responsible for user registration, and EmailService is responsible for sending email notifications.
2. Open-Closed Principle (OCP)
Definition: Classes should be open for extension but closed for modification. You should be able to extend the behavior of a class without altering its source code.
Example:
A payment system that supports only one type of payment method.
Violation Example:
public class PaymentService
{
public void ProcessPayment(string paymentType)
{
if (paymentType == "CreditCard")
{
// Process credit card payment
}
else if (paymentType == "PayPal")
{
// Process PayPal payment
}
}
}
Adding a new payment method requires modifying the PaymentService class, which violates OCP.
Refactored (OCP Applied):
public abstract class PaymentMethod
{
public abstract void ProcessPayment();
}
public class CreditCardPayment : PaymentMethod
{
public override void ProcessPayment()
{
// Process credit card payment
}
}
public class PayPalPayment : PaymentMethod
{
public override void ProcessPayment()
{
// Process PayPal payment
}
}
public class PaymentService
{
public void ProcessPayment(PaymentMethod paymentMethod)
{
paymentMethod.ProcessPayment();
}
}
Now, adding a new payment method involves extending the PaymentMethod class without changing the PaymentService class.
3. Liskov Substitution Principle (LSP)
Definition: Objects of a base class should be replaceable with objects of a derived class without affecting the correctness of the program.
Example:
Consider a base class Bird and a derived class Penguin. If Penguin cannot substitute Bird in a meaningful way, this violates LSP.
Violation Example:
public class Bird
{
public virtual void Fly()
{
Console.WriteLine("Bird is flying");
}
}
public class Penguin : Bird
{
public override void Fly()
{
throw new NotImplementedException("Penguins can't fly");
}
}
Calling the Fly method on a Penguin object will throw an exception, violating LSP.
Refactored (LSP Applied):
public abstract class Bird
{
public abstract void Move();
}
public class Sparrow : Bird
{
public override void Move()
{
Console.WriteLine("Sparrow is flying");
}
}
public class Penguin : Bird
{
public override void Move()
{
Console.WriteLine("Penguin is swimming");
}
}
Now, both Sparrow and Penguin can substitute Bird, and each class correctly implements the Move method according to its behavior.
4. Interface Segregation Principle (ISP)
Definition: Clients should not be forced to depend on methods they do not use. Instead of one large interface, split it into smaller, specific ones.
Example:
A large interface that forces classes to implement methods they don’t need.
Violation Example:
public interface IMultiFunctionDevice
{
void Print();
void Scan();
void Fax();
}
public class BasicPrinter : IMultiFunctionDevice
{
public void Print() { /* Print logic */ }
public void Scan() { throw new NotImplementedException(); }
public void Fax() { throw new NotImplementedException(); }
}
BasicPrinter is forced to implement Scan and Fax, even though it doesn't need them.
Refactored (ISP Applied):
public interface IPrinter
{
void Print();
}
public interface IScanner
{
void Scan();
}
public interface IFax
{
void Fax();
}
public class BasicPrinter : IPrinter
{
public void Print() { /* Print logic */ }
}
Now, BasicPrinter only implements the IPrinter interface, which is what it actually needs.
5. Dependency Inversion Principle (DIP)
Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions (interfaces). Also, abstractions should not depend on details; details should depend on abstractions.
Example:
A class directly depends on another class instead of an abstraction.
Violation Example:
public class EmailService
{
public void SendEmail(string message) { /* Email logic */ }
}
public class Notification
{
private readonly EmailService _emailService = new EmailService();
public void SendNotification(string message)
{
_emailService.SendEmail(message);
}
}
Notification depends directly on EmailService, making it difficult to switch to another notification method.
Refactored (DIP Applied):
public interface INotificationService
{
void Send(string message);
}
public class EmailService : INotificationService
{
public void Send(string message) { /* Email logic */ }
}
public class Notification
{
private readonly INotificationService _notificationService;
public Notification(INotificationService notificationService)
{
_notificationService = notificationService;
}
public void SendNotification(string message)
{
_notificationService.Send(message);
}
}
Now, Notification depends on the INotificationService abstraction, making it flexible to switch between different notification services (e.g., SMS, Push).
Summary
The SOLID design principles in C# make software design more modular, maintainable, and adaptable. Here's a quick recap:
- SRP: One responsibility per class.
- OCP: Extend classes without modifying existing code.
- LSP: Subclasses should behave as their parent class.
- ISP: Avoid large interfaces; split them based on functionality.
- DIP: Depend on abstractions, not concrete implementations. These principles help developers create software that is easier to maintain, extend, and scale over time.
Top comments (0)