Mastering SOLID Principles: The Key to Clean and Scalable Code

linhvuquach
4 min readJul 7, 2024

--

The SOLID principles.

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.

SOLID principle via mind-map

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! 🍻🍻

References

--

--

linhvuquach
linhvuquach

Written by linhvuquach

I'm a Software Engineer. I enjoy finding solutions to issues, and elevating client pleasure, so I often learn and contribute solutions along the way.

No responses yet