Skip to main content

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:

  1. Single Responsibility Principle (SRP)
  2. Open/Closed Principle (OCP)
  3. Liskov Substitution Principle (LSP)
  4. Interface Segregation Principle (ISP)
  5. 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.

Every Bit of Support Helps!

If you have enjoyed this post, please consider buying me a coffee ☕ to help me keep writing!