Unlocking Code Quality: Mastering SOLID Principles in Software Engineering (with C# Examples).
Mastering SOLID principles in Software Engineering

Unlocking Code Quality: Mastering SOLID Principles in Software Engineering (with C# Examples).


In our previous article, we explored the fundamentals of Object-Oriented Programming (OOP). Now, we're diving into the SOLID principles, a set of essential guidelines that transform your software design. These principles help create maintainable and scalable systems by reducing dependencies, enhancing readability, and simplifying maintenance and testing. In this article, we'll delve into each SOLID principle with practical C# examples to make these concepts more accessible.

1. Single Responsibility Principle (SRP)

Definition: A class should have only one reason to change, meaning it should have only one job or responsibility.

Explanation: When a class has multiple responsibilities, it becomes coupled. A change to one responsibility results in modifications to the class, potentially affecting the other responsibilities. This makes the class fragile and harder to maintain.

Example:

Consider a class Employee that handles both employee data management and salary calculations.

public class Employee
{
    public string Name { get; set; }
    public decimal Salary { get; set; }

    public void CalculateSalary()
    {
        // Salary calculation logic
    }

    public void SaveToDatabase()
    {
        // Database save logic
    }
}        

To adhere to SRP, split this class into two separate classes:

public class Employee
{
    public string Name { get; set; }
    public decimal Salary { get; set; }
}

public class SalaryCalculator
{
    public void CalculateSalary(Employee employee)
    {
        // Salary calculation logic
    }
}

public class EmployeeRepository
{
    public void SaveToDatabase(Employee employee)
    {
        // Database save logic
    }
}        

Recap the principle.

Violation: `Employee` Class with multiple responsibilities, SRP is violated.

Adhering to SRP:

The `Employee` class focuses on one task. It works with the SalaryCalculator and `EmployeeRepository` classes, each handling a specific job.This setup makes testing easier, improve maintainability and makes the code easier to understand.

A single class with multiple responsibilities being split into three separate classes, each with a single responsibility.


2. Open/Closed Principle (OCP)

Definition: Software entities should be open for extension but closed for modification.

Explanation: The idea is to allow the behavior of a module to be extended without modifying its source code. This can be achieved through abstraction and polymorphism.

Example:

Suppose we have a class Invoice that calculates the discount based on the type of customer:

public class Invoice
{
    public double GetDiscount(string customerType)
    {
        if (customerType == "Regular")
        {
            return 0.1; // 10% discount
        }
        else if (customerType == "Premium")
        {
            return 0.2; // 20% discount
        }
        return 0.0;
    }
}        

To adhere to OCP, we can introduce an interface for discount calculation:

public interface IDiscount
{
    double GetDiscount();
}

public class RegularCustomerDiscount : IDiscount
{
    public double GetDiscount()
    {
        return 0.1; // 10% discount
    }
}

public class PremiumCustomerDiscount : IDiscount
{
    public double GetDiscount()
    {
        return 0.2; // 20% discount
    }
}

public class Invoice
{
    public double GetDiscount(IDiscount discount)
    {
        return discount.GetDiscount();
    }
}        

Recap the principle.

Violation: `Invoice` class with a method `GetDiscount` that contains hardcoded conditions for different customer types, OCP is violated.

Adhering to OCP:

`RegularCustomerDiscount` and `PremiumCustomerDiscount` classes implement the IDiscount interface.

`Invoice` class and its relationship with the IDiscount interface.

This design adheres to the Open/Closed Principle, allowing for new discount types to be added without modifying the `Invoice` class.

A class with hardcoded conditions being extended using interfaces to add new behaviours

3. Liskov Substitution Principle (LSP)

Definition: Subtypes must be substitutable for their base types without altering the correctness of the program.

Explanation: This principle ensures that a derived class can be used wherever a base class is expected, without the need for special treatment.

Example:

Consider a class `Bird` and its subclass `Penguin`:

public class Bird
{
    public virtual void Fly()
    {
        // Fly logic
    }
}

public class Penguin : Bird
{
    public override void Fly()
    {
        throw new NotSupportedException("Penguins cannot fly");
    }
}        

To adhere to LSP, we need to refactor the design so that the `Penguin` class does not need to override the `Fly` method in a way that violates the principle:

public class Bird
{
    public virtual void Move()
    {
        // Move logic
    }
}

public class FlyingBird : Bird
{
    public override void Move()
    {
        Fly();
    }

    private void Fly()
    {
        // Fly logic
    }
}

public class Penguin : Bird
{
    public override void Move()
    {
        Swim();
    }

    private void Swim()
    {
        // Swim logic
    }
}        

Recap the principle.

Violation: `Penguin` class inherits from `Bird` and overrides the `Fly` method in a way that violates the LSP by throwing a NotSupportedException.

Adhering to LSP:

`FlyingBird` class inherits from `Bird` and overrides the `Move` method to include `flying` logic.

`Penguin` class inherits from `Bird` and overrides the `Move` method to include swimming logic.

Both methods `Fly` and `Swim` are private within their respective subclasses, encapsulating specific behaviours without violating the Liskov Substitution Principle.

A hierarchy where subclasses properly inherit and extend the base class without violating the principle.

4. Interface Segregation Principle (ISP)

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

Explanation: This principle suggests creating smaller, more specific interfaces instead of large, general-purpose ones.

Example:

Consider an interface IWorker with multiple responsibilities:

public interface IWorker
{
    void Work();
    void Eat();
}

public class Worker : IWorker
{
    public void Work()
    {
        // Work logic
    }

    public void Eat()
    {
        // Eat logic
    }
}

public class Robot : IWorker
{
    public void Work()
    {
        // Work logic
    }

    public void Eat()
    {
        throw new NotSupportedException();
    }
}        

To adhere to ISP, split the interface into smaller ones:

public interface IWorkable
{
    void Work();
}

public interface IFeedable
{
    void Eat();
}

public class Worker : IWorkable, IFeedable
{
    public void Work()
    {
        // Work logic
    }

    public void Eat()
    {
        // Eat logic
    }
}

public class Robot : IWorkable
{
    public void Work()
    {
        // Work logic
    }
}        

Recap the principle.

Violation: The `Robot` class implements both methods but throws a `NotSupportedException` for the Eat method, highlighting a violation of the ISP.

Adhering to ISP: The `Robot` class implements only the IWorkable interface, so it has only the Work method. whereas, The `Worker` class implements both `IWorkable` and `IFeedable` interfaces, so it has both Work and Eat methods.

This design demonstrates the principle of breaking a large interface into smaller, more focused interfaces, allowing classes to implement only what they need.

A large interface being split into smaller, more specific interfaces, with classes implementing only the interfaces they need


5. Dependency Inversion Principle (DIP)

Definition: 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.

Explanation: This principle aims to reduce the coupling between high-level and low-level modules by introducing abstractions.

Example:

Consider a class `Employee` that depends on a specific database class:

public class SqlDatabase
{
    public void Save()
    {
        // Save to SQL Database
    }
}

public class Employee
{
    private SqlDatabase _database;

    public Employee()
    {
        _database = new SqlDatabase();
    }

    public void Save()
    {
        _database.Save();
    }
}        

To adhere to DIP, introduce an abstraction for the database:

public interface IDatabase
{
    void Save();
}

public class SqlDatabase : IDatabase
{
    public void Save()
    {
        // Save to SQL Database
    }
}

public class Employee
{
    private IDatabase _database;

    public Employee(IDatabase database)
    {
        _database = database;
    }

    public void Save()
    {
        _database.Save();
    }
}        

Recap the principle.

Violation: Employee class directly depending on SqlDatabase class and not on abstract class, DIP is violated.

Adhering to DIP: Employee class takes instance of `IDatabase` in its constructor, allowing it to work with any class that Implements `IDatabase` and this design promoting flexibility.

A high-level class directly depending on a low-level class, then refactored to depend on an abstraction instead

Conclusion

Wrapping up, Mastering SOLID principles is crucial for developers across projects of all sizes. These principles, widely endorsed by developers worldwide, address common challenges and are instrumental in honing programming skills. By mastering SOLID, you equip yourself with a powerful toolkit that significantly boosts code quality, maintainability, scalability, and overall effectiveness in developing resilient software solutions.


As we progress, expect insights on design patterns, system architecture, and optimisation strategies, all aimed at refining our craft. Let's continue to grow as better programmers together!

#SoftwareEngineering #SOLIDPrinciples #CSharp #SoftwareDesign #CodeQuality #DeveloperTips #Scalability #SystemDesign #SoftwareArchitecture #CodingExamples #SoftwareArchitecture

要查看或添加评论,请登录

社区洞察

其他会员也浏览了