Understanding and applying the SOLID principles is essential for any developer aiming to create robust, scalable, and maintainable software. Let’s dive into each principle with practical examples using C# and .NET Core.
1. Single Responsibility Principle (SRP)
Definition: A class should have only one reason to change, meaning it should only have one job or responsibility.
Benefit: By ensuring that a class has only one responsibility, it becomes easier to maintain and understand. It also helps in reducing the risk of unintended consequences when changes are made.
Example: Let’s create a simple example which violates SRP:
public class Report
{
public string Generate(Params...);
public void SaveToFile(Params...);
public void SendByEmail(Params...);
}
To adhere to SRP, we should split it into several files:
public interface IReportGenerator
{
string Generate(string data);
}
public interface IFileManager
{
string Save(string content, string path);
}
public interface IEmailService
{
bool Send(string recipient, string subject, string body);
}
2. Open/Closed Principle (OCP)
Definition: Software entities should be open for extension but closed for modification.
Benefit: This principle allows developers to add new functionality without changing existing code, thereby reducing the risk of introducing bugs into the system.
Example: Let’s create a simple example which violates OCP:
public class PaymentProcessor
{
public void Process(string method)
{
if (method = PaymentType.CreditCard) {}
else if (method = PaymentType.PayPal) {}
}
}
Now let’s create a simple payment system where we want to extend the types of payments without modifying existing code.
public interface IPaymentMethod { void Process(); }
public class CreditCardPayment : IPaymentMethod
{
public void Process() => Console.WriteLine("CreditCard Payment.");
}
public class PayPalPayment : IPaymentMethod
{
public void Process() => Console.WriteLine("PayPal Payment.");
}
public class PaymentProcessor
{
public void Process(IPaymentMethod payment) => payment.Process();
}
We can now add new payment methods by extending the Payment class without modifying existing code.
3. Liskov Substitution Principle (LSP)
Definition: Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.
Benefit: Ensures that derived classes extend the base class without changing its behavior, promoting code reusability and robustness.
Example: Let’s consider a bird hierarchy where a subclass violates LSP by not supporting the same behavior as the base class.
public class Bird
{
public virtual void Fly() => Console.WriteLine("Fly...");
}
public class Sparrow : Bird { }
public class Penguin : Bird
{
public override void Fly() => throw new InvalidOperationExpection("Penguins don't fly!");
}
To adhere to LSP, we can introduce an interface that better represents the capabilities of different birds:
public interface IFlyable
{
void Fly();
}
public class Sparrow : IFlyable
{
public void Fly() => Console.WriteLine("I'm flying!");
}
public class Penguin
{
public void Swin() => Console.WriteLine("I'm swimming!");
}
4. Interface Segregation Principle (ISP)
Definition: A client should not be forced to depend on interfaces it does not use.
Benefit: By splitting large interfaces into smaller, more specific ones, you can reduce the complexity of the code and make it more understandable and easier to manage.
Example: Consider an overly large interface that violates ISP:
public interface IWorker
{
void Work();
void Eat();
}
public class Robot : IWorker
{
public void Work() => Console.WriteLine("Robot working.");
public void Eat() => throw new NotImplementedExcepetion("Robots don't eat!");
}
A developer implementing this interface for a robot would be forced to implement methods it doesn’t need.
To adhere to ISP, we can split the interface into more specific ones:
public interface IWorkable
{
void Work();
}
public interface IEatable
{
void Eat();
}
public class Human : IWorkable, IEatable
{
public void Work() => Console.WriteLine("Human working.");
public void Eat() => Console.WriteLine ("Human eating.");
}
public class Robot : IWorkable
{
public void Work() => Console.WriteLine("Robot working.");
}
5. Dependency Inversion Principle (DIP)
Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions.
Benefit: Promotes decoupling of software components, making the system more flexible and easier to modify or extend.
Example: Consider a high-level class directly depending on a low-level class:
public class EmailService
[
public void Send(string message) => Console.WriteLine(message);
}
public class OrderService
{
private readonly EmailService _emailService = new EmailService();
public void PlaceOrder()
{
_emailService.Send("Order placed!");
}
}
To adhere to DIP, we introduce an abstraction:
public interface IMessageService
{
void Send(string message); // Defining abstraction
}
public class OrderService
{
private readonly IMessageService _messageService;
public OrderService(IMessageService messageService)
{
_messageService = messageService;
}
public void PlaceOrder()
{
_messageService.Send("Order placed!");
}
}
Conclusion
By applying these principles, we can create a more modular, scalable, and maintainable codebase. These examples demonstrate how to adhere to each of the SOLID principles using C# and .NET Core, enabling us to write better software.



