--- title: SOLID Principles description: Solid Principles date: 2024-04-03T16:10:57.476Z tags: - code quality - oops categories: - Programming - Principles image: path: /assets/img/posts/solid-principles.jpg slug: solid-principles --- The SOLID principles are a set of **five design principles** that are intended to guide software development to create more **understandable**, **maintainable**, **extendable** and **scalable** code. These principles were introduced by **Robert C. Martin** (also known as Uncle Bob) in the early 2000s and have since become fundamental concepts in object-oriented design and programming. Here's a brief overview of each principle: ## 1. Single Responsibility Principle (SRP): This principle states that a class should have **only one reason to change**. In other words, a class should have **only one responsibility or job**. By adhering to SRP, you ensure that classes are focused and have clear, understandable purposes, which makes them **easier to maintain and test**. **Example**: *Think of a chef in a restaurant. Instead of having a chef who both cooks meals and serves customers, you'd want separate roles. The chef should focus on cooking delicious dishes, while a waiter takes care of serving customers.* ```c# // Before public class Chef { public void CookMeals() { /*...*/ } public void ServeMeals() { /*...*/ } } // After public class Chef { public void CookMeals() { /*...*/ } } public class Waiter { public void ServeMeals() { /*...*/ } } ``` ## 2. Open/Closed Principle (OCP): This Principle suggests that software entities `(classes, modules, functions, etc.)` should be **open for extension** but **closed for modification**. This means that you should be able to **extend the behavior of a module without modifying its source code**. This is typically achieved through the use of **inheritance**, **polymorphism** and **parameters**. **Example**: *Consider a shape drawing application. Instead of modifying the existing shape classes every time you need to add a new shape, you'd create a abstact class called **Shape** and implement it in different shape classes like **Circle**, **Square**, etc. Then, when you want to add a new shape, you create a new class that implements the Shape without modifying the existing code.* ```c# // Before public class Shape { public double CircleArea(double radius) { /*...*/ } public double SquareArea(double sideLength) { /*...*/ } } // After public abstract class Shape { public abstract double Area(); } public class Circle : Shape { public override double Area() { /*...*/ } } public class Square : Shape { public override double Area() { /*...*/ } } ``` ## 3. Liskov Substitution Principle (LSP): This Principle states that objects of a superclass should be substitutable with objects of its subclasses without affecting the correctness of the program. In simpler terms, **a subclass should behave in such a way that it does not break the functionality that the superclass expects**. **Example**: *Consider a program that expects objects of type Bird. According to LSP, if you have a class Swan that inherits from Bird, you should be able to substitute an instance of Swan wherever you expect an Bird without breaking the program's functionality.* ```c# // Before public abstract class Bird { public abstract void Fly() { /* I can fly */} } public abstract class Penguin : Bird { // Violating LSP principle (Penguin class breaks Fly functionality) public override void Fly() { throw new NotImplementedException("Penguins can't fly!"); } } // After public abstract class Bird { public abstract void Fly() { /* I can fly */} } public abstract class Swan : Bird { public override void Fly() { /* I can fly */ } } ``` ## 4. Interface Segregation Principle (ISP) This Principle suggests that clients should not be forced to depend on interfaces they do not use. In other words, **interfaces should be fine-grained and specific to the client's needs**. This involves breaking large interfaces into smaller, more focused interfaces. **Example**: *Lets consider **IPerson** interface which has methods to work and eat. **Robot** class cannot implement IPerson interface as it cannot eat. IPerson should be splitted in to smaller interfaces like **IEater** and **IWorker** so Robot can implement IWorker.* ```c# // Before public interface IPerson { void Work(); void Eat(); } public class Robot : IPerson { public void Work() { /*...*/ } public void Eat() { /*...*/ } // Doesn't make sense for a robot } // After public interface IWorker { void Work(); } public interface IEater { void Eat(); } public class Robot : IWorker { public void Work() { /*...*/ } } public class Human : IEater, IWorker { public void Work() { /*...*/ } public void Eat() { /*...*/ } } ``` ## 5. Dependency Inversion Principle (DIP) This Principle states that high-level modules should not depend on low-level modules. Instead, **both should depend on abstractions**. This principle encourages the use of interfaces or abstract classes to decouple classes from their concrete implementations. Abstractions should not depend on details. Details should depend on abstractions. **Example**: *If UserService directly depends on the concrete implementation of MySQLDatabase. This violates DIP since the high-level class UserService is directly dependent on a low-level class. **If we want to switch to a different database system (e.g., PostgreSQL), we need to modify the UserService class**. Instead of depending on concrete implementations, the high-level class UserService should depend on abstractions. Let's create a Database interface as an abstraction:* ```c# // Before /* Low-level module */ class MySQLDatabase { getUserData(id: number): string { // Logic to fetch user data from MySQL database } } /* High-level module */ class UserService { private database: MySQLDatabase; constructor() { this.database = new MySQLDatabase(); } getUser(id: number): string { return this.database.getUserData(id); } } // After /* Abstract interface (abstraction) for the low-level module */ interface Database { getUserData(id: number): string; } /* low-level module implementing the Database interface */ class MySQLDatabase implements Database { getUserData(id: number): string {} } /* low-level module implementing the Database interface */ class PostgreSQLDatabase implements Database { getUserData(id: number): string {} } /* High-level module */ class UserService { private database: Database; constructor(database: Database) { this.database = database; } getUser(id: number): string { return this.database.getUserData(id); } } ``` This way, the UserService class depends on the Database abstraction, not on concrete implementations, fulfilling the Dependency Inversion Principle. **Note:** I'm excited to share this post that's like a treasure chest filled with nuggets of wisdom from different articles I've come across. Some of the examples taken from these articles [1](https://dev.to/galwaycoder/the-solid-principles-in-software-design-explained-53n) [2](https://dev.to/lukeskw/solid-principles-theyre-rock-solid-for-good-reason-31hn).