Mastering SOLID Principles: The Key to Clean and Scalable Code
The SOLID principles are five principles of Object-Oriented class design. They’re a set of rules and best practices to follow while designing a class structure.
You can have a look at the following content to capture the important idea of the SOLID principles.
The single responsibility principle (SRP)
A class should have only one reason to change, meaning it should have only one job or responsibility.
Let’s see the following example to know how it violates and fixes.
// Violates SRP
public class User
{
public void SaveUser(User user)
{
// Save user to database
}
public void SendEmail(User user)
{
// Send email to user
}
}
// Follow SRP
public class UserRepository
{
public void SaveUser(User user)
{
// Save user to database
}
}
public class EmailService
{
public void SendEmail(User user)
{
// Send email to user
}
}
The open-closed principle (OCP)
Software entities should be open for extension but closed for modification. This means you should be able to extend a class’s behavior without modifying it.
Let’s see the following example to know how it violates and fixes.
// Violates OCP
public class Rectangle
{
public double Width { get; set; }
public double Height { get; set; }
}
public class AreaCalulator
{
public double CalculateArea(object[] shapes)
{
double area = 0;
foreach (var shape in shapes)
{
if (shape is Rectangle)
{
Rectangle rectangle = (Rectangle)shape;
area += rectangle.Width * rectangle.Height;
}
// Adding new shapes requires modifying this class
}
return area;
}
}
// Follow OCP
public abstract class Shape()
{
public abstract double CalculateArea();
}
public Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double CalculateArea()
{
return Width * Height;
}
}
public Square : Shape
{
public double Side { get; set; }
public override double CalculateArea()
{
return Side * Side;
}
}
public Circle : Shape
{
public double Radius { get; set; }
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
}
public class AreaCalculator
{
public double CalculateArea(Shape[] shapes)
{
return shapes.Sum(shape => shape.CalculateArea());
}
}
The Liskov substitution principle (LSP)
Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.
Let’s see the following example to know how it violates and fixes.
// Violates LSP
public class Rectangle
{
public virtual double Width { get; set; }
public virtual double Height { get; set; }
public virtual int CalculateArea()
{
return Width * Height;
}
}
public class Square : Rectangle
{
public override double Width
{
set { base.Width = base.Height = value; }
}
public override double Height
{
set { base.Height = base.Width = value; }
}
}
// Follow LSP
public abstract class Shape
{
public abstract double CalculateArea();
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double CalculateArea()
{
return Width * Height;
}
}
public class Square : Shape
{
public double Side { get; set; }
public override double CalculateArea()
{
return Side * Side;
}
}
The interface segregation principle (ISP)
Many client-specific interfaces are better than one general-purpose interface. This principle suggests breaking larger interfaces into smaller, more specific ones.
Let’s see the following example to know how it violates and fixes.
// Violates ISP
public interface IWoker
{
void Work();
void Eat();
void Sleep();
}
// Follow ISP
public interface IWorkable
{
void Work();
}
public interface IEatable
{
void Eat();
}
public interface ISleepable
{
void Sleep();
}
public class Human : IWorkable, IEatable, ISleepable
{
public void Work()
{
// Human work
}
public void Eat()
{
// Human eat
}
public void Sleep()
{
// Human sleep
}
}
public class Robot : IWorkable
{
public void Work()
{
// Robot work
}
}
The dependency inversion principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstraction. Abstractions should not depend on details, details should depend on abstraction.
Let’s see the following example to know how it violates and fixes.
// Violates DIP
public class EmailNotifier
{
public void SendEmail(string message)
{
Console.WriteLine($"Email sent: {message}");
}
}
public class NotificationService
{
private EmailNotifier _emailNotifier = new EmailNotifier();
public void Notify(string message)
{
_emailNotifier.SendEmail(message);
}
}
// Follow DIP
public interface INotifier
{
void SendNotification(string message);
}
public class EmailNotifier : INotifier
{
public void SendNotification(string message)
{
Console.WriteLine($"Email sent: {message}");
}
}
public class SmsNotifier : INotifier
{
public void SendNotification(string message)
{
Console.WriteLine($"SMS sent: {message}");
}
}
public class NotificationService
{
public INotifier _notifier;
public NotificationService(INotifier notifier)
{
_notifier = notifier;
}
public void Notify(string message)
{
_notifier.SendNotification(message);
}
}
When you find this post informative, don’t forget to share it with your team and colleagues, thanks.
You can reach me on Twitter @linhvuquach
to get my new blog every week with a bunch of categories like software engineer, problem-solving, and how to make your product …
Cheers! 🍻🍻