Understanding SOLID Principles in .NET C#

Osama HaiDer
3 min readJul 15, 2024

--

Introduction

SOLID principles are five design principles that help developers create more understandable, flexible, and maintainable code. Robert C. Martin introduced these principles which are widely used in object-oriented programming. Let’s break down each principle with easy-to-understand explanations and examples in .NET 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:

Let’s say we have a class Invoice that handles both creating invoices and sending them via email. This violates SRP because the class has more than one responsibility.

public class Invoice
{
public void CreateInvoice()
{
// Code to create an invoice
}

public void SendEmail()
{
// Code to send email
}
}

To follow SRP, we should separate these responsibilities into different classes:

public class Invoice
{
public void CreateInvoice()
{
// Code to create an invoice
}
}

public class EmailService
{
public void SendEmail()
{
// Code to send email
}
}

2. Open/Closed Principle (OCP)

Definition: Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.

Example:

Suppose we have a class Discount that applies a discount to a price. If we want to add a new discount type, we should not modify the existing code.

public class Discount
{
public double ApplyDiscount(double price)
{
// Default discount
return price * 0.9;
}
}

Instead, we can extend the class:

public abstract class Discount
{
public abstract double ApplyDiscount(double price);
}

public class DefaultDiscount : Discount
{
public override double ApplyDiscount(double price)
{
return price * 0.9;
}
}

public class SpecialDiscount : Discount
{
public override double ApplyDiscount(double price)
{
return price * 0.8;
}
}

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.

Example:

Consider a class Bird and a subclass Ostrich. If Ostrich cannot behave as a Bird (e.g., it cannot fly), it violates LSP.

public class Bird
{
public virtual void Fly()
{
// Flying code
}
}

public class Ostrich : Bird
{
public override void Fly()
{
throw new NotSupportedException("Ostriches can't fly!");
}
}

To follow LSP, we should redesign the classes:

public abstract class Bird
{
// Bird properties and methods
}

public class FlyingBird : Bird
{
public void Fly()
{
// Flying code
}
}

public class Ostrich : Bird
{
// Ostrich-specific properties and methods
}

4. Interface Segregation Principle (ISP)

Definition: Clients should not be forced to depend on interfaces they do not use.

Example:

If we have an interface IMultiFunctionDevice with methods for printing, scanning, and faxing, a class implementing this interface might only need some methods.

public interface IMultiFunctionDevice
{
void Print();
void Scan();
void Fax();
}

public class Printer : IMultiFunctionDevice
{
public void Print() { }
public void Scan() { }
public void Fax() { }
}

To follow ISP, we should split the interface:

public interface IPrinter
{
void Print();
}

public interface IScanner
{
void Scan();
}

public interface IFax
{
void Fax();
}

public class Printer : IPrinter
{
public void Print() { }
}

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:

Suppose we have a UserService a class that depends on a concrete EmailService class.

public class EmailService
{
public void SendEmail() { }
}

public class UserService
{
private EmailService _emailService;

public UserService()
{
_emailService = new EmailService();
}

public void NotifyUser()
{
_emailService.SendEmail();
}
}

To follow DIP, we should depend on an abstraction:

public interface IEmailService
{
void SendEmail();
}

public class EmailService : IEmailService
{
public void SendEmail() { }
}

public class UserService
{
private IEmailService _emailService;

public UserService(IEmailService emailService)
{
_emailService = emailService;
}

public void NotifyUser()
{
_emailService.SendEmail();
}
}

Conclusion

By adhering to SOLID principles, you can write easier code to understand, maintain, and extend. These principles help you build robust and scalable applications in .NET C#. Happy coding!

For more updates and insights, and to connect with me, feel free to follow me on LinkedIn:

🔗 [Connect on LinkedIn]

Let’s stay connected and continue the conversation!

--

--

Osama HaiDer

SSE at TEO International | .Net | Azure | AWS | Web APIs | C#