Skip to content

Mastering SOLID Principles with Practical Examples in C#

00:00 / --:--

Read by your browser's built-in voice.

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:

C#

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:

C#

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:

C#

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.

C#

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.");
}		

C#

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.

C#

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:

C#

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:

C#

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:

C#

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:

C#

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:

C#

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.

Rate this article

Rate this article out of 5

No ratings yet
Be the first to rate this article.

Selecting a star will ask you to sign in.
HV
Huy Vũ
Contributor at Selectgo

Shares practical engineering notes on this blog.