7.3 KiB
| title | description | date | tags | categories | image | slug | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| SOLID Principles | The SOLID principles are a set of design principles aimed at improving software quality, making it easier to understand, extend, and maintain. | 2024-04-03T16:10:57.476Z |
|
|
|
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.
// 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.
// 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.
// 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.
// 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:
// 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 2.