SOLID

Pasted image 20251220175310.png
SOLID design principles are a set of guidelines that help developers create better-structured, more manageable code.

Pasted image 20251220174941.png

  1. Single Responsibility Principle (SRP): Understanding how to ensure that each class has only one responsibility and how this helps in keeping the code maintainable.

  2. Open/Closed Principle (OCP): Learning to design classes that are open for extension but closed for modification, allowing for easier addition of new features.

  3. Liskov Substitution Principle (LSP): Exploring how to design class hierarchies where derived classes can be used interchangeably with their base classes without breaking the system.

  4. Interface Segregation Principle (ISP): Understanding how to keep interfaces small and focused, avoiding the problem of "fat interfaces" that force classes to implement unnecessary methods.

  5. Dependency Inversion Principle (DIP): Learning to decouple high-level modules from low-level details by introducing abstractions, making the system flexible and easy to maintain.


The SOLID Principles

S - Single Responsibility Principle (SRP)

The Single Responsibility Principle states that a class should have only one reason to change. In other words, a class should only do one thing or be responsible for one task. This makes the system simpler, as each class has a specific responsibility. If something needs to change, it will only affect that class.

O - Open/Closed Principle (OCP)

The Open/Closed Principle means that software entities (like classes or methods) should be open for extension but closed for modification. This allows developers to extend the functionality of existing classes without changing the class itself. The goal is to avoid making unnecessary changes to existing code, which can introduce new bugs.

L - Liskov Substitution Principle (LSP)

The Liskov Substitution Principle ensures that objects of a subclass should be able to replace objects of the superclass without affecting the correctness of the program. This means subclasses should not break the functionality defined in the parent class, ensuring consistent behavior across the system.

I - Interface Segregation Principle (ISP)

The Interface Segregation Principle advises that clients should not be forced to depend on interfaces they do not use. Instead of having one large interface, break it down into smaller, more specific ones. This way, each class implements only the methods it needs, making the code cleaner and easier to work with.

D - Dependency Inversion Principle (DIP)

The Dependency Inversion Principle states that high-level modules should not depend on low-level modules. Both should depend on abstractions. This principle reduces tight coupling between modules, making the system more flexible and easier to modify. It helps create loosely coupled code by relying on abstractions rather than concrete implementations.


SRP - Cohesion and Coupling

What is Cohesion - High cohesion → Single Responsibility Principle

Cohesion refers to how closely related and focused the responsibilities of a class, module, or method are.

A highly cohesive class will have methods that work together to achieve a single purpose. On the other hand, low cohesion occurs when a class has unrelated responsibilities bundled together, which makes it harder to maintain and understand.

To follow the Single Responsibility Principle, your classes need high cohesion. Why? Because high cohesion means that a class focuses on a single responsibility or task, which is exactly what SRP demands.

By organizing your code to have high cohesion, you naturally follow the SRP, as each class or component will focus on a single, clearly defined responsibility.

What is Coupling? Low coupling → Dependency Inversion Principle

Coupling refers to how closely connected different modules, classes, or methods are to each other.

In software design, we aim for low coupling, meaning that classes and components should be as independent from each other as possible. High coupling, on the other hand, means that classes are tightly dependent on one another, making it harder to change or reuse parts of the system.

Reducing coupling makes it easier to follow the Single Responsibility Principle. Here’s why:

By splitting the responsibilities (like student management and database operations) into different classes, we maintain low coupling and follow SRP. This ensures that each part of the system is focused and independent, making future changes much easier.


Explaining Open/Closed Principle (OCP)

Let’s now break down these two important terms:


Each SOLID principle is related to each other.

The Open/Closed Principle is closely connected to the Single Responsibility Principle. Here's how:


The Liskov Substitution Principle (LSP) is an important concept in object-oriented design. It states:

"Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program."

This principle means that a subclass should behave in a way that it can substitute for its parent class without causing issues in the system. If a subclass breaks the behavior expected from the parent class, it violates LSP.

When to Break the Hierarchy

Breaking the hierarchy is useful when you notice these patterns, helping you build more flexible and maintainable systems.


Using Composition to Follow Liskov Substitution Principle (LSP)

In the previous lesson, we explored how using inheritance can sometimes violate the Liskov Substitution Principle. Another way to follow LSP without falling into the pitfalls of inheritance is by using composition over inheritance. This method allows you to design systems that are more flexible and avoid situations where subclasses cannot properly fulfill the parent class's contract.

Example: Bird and Flying Behavior

Let’s use the example of birds. If we use inheritance, we might create a Bird class that defines general bird behaviors, like flying. However, some birds—like penguins—cannot fly.

Inheritance forces subclasses to implement or override behaviors they don’t need, which can lead to LSP violations.

Solution: Using Composition to Follow LSP

To avoid this issue, we can use composition. Instead of forcing birds to inherit flying behavior, we can create a separate Flyable interface and only assign flying abilities to birds that can actually fly.

Using composition over inheritance allows us to avoid violating the Liskov Substitution Principle. By separating specific behaviors like flying into an interface, we avoid forcing all bird subclasses to implement or override methods that don’t apply to them. This creates a more flexible, maintainable design that follows LSP and allows each class to behave according to its own characteristics.

Why is LSP Important?

LSP is vital because it upholds the integrity of your class hierarchy, ensuring that extending functionality or creating new subclasses won’t disrupt the program’s existing behavior. It keeps your code clean, modular, and safe to evolve over time.


Interface Segregation Principle

The Interface Segregation Principle (ISP) is the fourth principle in the SOLID design principles. It states:

"Clients should not be forced to depend on interfaces they do not use."

This means that a class should only implement the methods it actually needs. Large, general-purpose interfaces can become problematic when they force implementing classes to include methods they don't require. Instead, it's better to create smaller, more focused interfaces.

By restructuring the code:

Techniques to Identify ISP Violations

1. Low Cohesion

Low cohesion happens when a class tries to handle multiple unrelated tasks, making the class less focused and harder to maintain. Low cohesion often leads to ISP violations because the class implements more methods than necessary.

2. Fat Interface

fat interface is one that contains too many methods, usually covering multiple unrelated functionalities. Fat interfaces force classes to depend on methods they don’t need, leading to bloated code and ISP violations.

3. Empty or Unsupported Methods

When classes implement an interface and include empty or unsupported methods, it’s a clear sign of an ISP violation. Classes should not be forced to implement methods they won’t use.

4. Classes with Too Many Responsibilities

When a class has too many responsibilities, it often implements multiple methods from a large interface. This indicates that the interface may need to be broken into smaller ones.

How ISP Relates to Other SOLID Principles


Dependency Inversion Principle

The Dependency Inversion Principle (DIP) is the final principle in the SOLID design principles. It states:

"High-level modules should not depend on low-level modules. Both should depend on abstractions."

"Abstractions should not depend on details; details should depend on abstractions."

These statements emphasize that software design should prioritize flexibility and decoupling by ensuring that high-level policies are not directly tied to low-level implementation details. Both high-level and low-level modules should rely on abstractions (e.g., interfaces or abstract classes) to establish a flexible relationship. This approach keeps the system modular and adaptable.

Key Concepts of DIP

Pasted image 20251220210939.png### Why This Violates DIP

Pasted image 20251220211033.png


using System;

// Abstraction for sending notifications
public interface NotificationSender
{
    void send(string message);
}

// EmailService that implements NotificationSender
public class EmailService : NotificationSender
{
    public void send(string message)
    {
        Console.WriteLine("Sending email: " + message);
    }
}

// SMSService that implements NotificationSender
public class SMSService : NotificationSender
{
    public void send(string message)
    {
        Console.WriteLine("Sending SMS: " + message);
    }
}

// High-level module that depends on the NotificationSender interface
public class NotificationService
{
    private NotificationSender notificationSender;

    // Constructor takes a NotificationSender, allowing for flexibility
    public NotificationService(NotificationSender notificationSender)
    {
        this.notificationSender = notificationSender;
    }

    public void send(string message)
    {
        notificationSender.send(message);
    }
}

// Main class to test the refactored design
public class Solution
{
    public static void Main(string[] args)
    {
        // Using EmailService as the NotificationSender
        NotificationSender emailService = new EmailService();
        NotificationService notificationService = new NotificationService(emailService);
        notificationService.send("Hello via Email");

        // Switching to SMSService without changing NotificationService
        NotificationSender smsService = new SMSService();
        notificationService = new NotificationService(smsService);
        notificationService.send("Hello via SMS");
    }
}



Dependency Injection

Dependency Injection (DI) is one of the most popular ways to apply DIP. It involves providing the dependencies (i.e., low-level modules) to a class from the outside rather than having the class create its own dependencies. This allows the high-level module to depend on abstractions, making the system more flexible.

Inversion of Control (IoC) Frameworks

IoC frameworks such as Spring in Java, or .NET Core in C#, use dependency injection to apply DIP at scale. These frameworks manage the lifecycle and dependencies of objects for you, allowing for loosely coupled architecture.


Applying the Dependency Inversion Principle (DIP) in real-world scenarios promotes flexibility, modularity, and maintainability in software design. Techniques such as dependency injection, service locators, IoC frameworks, and plugin architectures enable systems to follow DIP effectively, ensuring that high-level policies remain decoupled from low-level implementations.

Importance of DIP

The Dependency Inversion Principle (DIP) plays a crucial role in creating flexible, maintainable, and decoupled software. By ensuring that high-level modules do not depend directly on low-level implementations, DIP allows for:

How DIP Relates to Other SOLID Principles