Search  
Always will be ready notify the world about expectations as easy as possible: job change page
Nov 11

Understanding SOLID principles in .NET (C#): A practical guide with code examples

Understanding SOLID principles in .NET (C#): A practical guide with code examples
Author:
Source:
Views:
243

SOLID principles make it easy for a developer to write easily extendable code and avoid common coding errors.

These principles were introduced by Robert C. Martin, and they have become a fundamental part of object-oriented programming.

In the context of .NET development, adhering to SOLID principles can lead to more modular, flexible, and maintainable code. In this article, we’ll delve into each SOLID principle with practical coding examples in C#.

Following are the five SOLID design principles:

SOLID principles

1. Single Responsibility Principle (SRP)

The SRP states that a class should have only one reason to change, meaning it should have only one responsibility. This promotes modularization and makes the code easier to understand and maintain.

Key idea: A class should do only one thing, and it should do it well.

Real-time example: Think of a chef who only focuses on cooking, not managing the restaurant or delivering food.

Practical coding example in C#:

Before applying for SRP:

public class Report
{
    public void GenerateReport() { }
    public void SaveToFile() { }
}

In this scenario, the Report class has two responsibilities: generating a report and saving it to a file. This violates the SRP.

After applying SRP:

public class Report
{
    public void GenerateReport() { }
}

public class ReportSaver
{
    public void SaveToFile() { }
}

Now, the Report class is responsible only for generating reports, while the ReportSaver class is responsible for saving reports. Each class has a single responsibility.

Explanation: According to SRP, one class should take one responsibility hence to overcome this problem we should write another class to save the report functionality. If you make any changes to theReport class will not affect the ReportSaver class.

2. Open/Closed Principle (OCP)

The Open/Closed Principle suggests that a class should be open for extension but closed for modification. This means you can add new features without altering existing code.

Key idea: Once a class is written, it should be closed for modifications but open for extensions.

Real-time example: Your smartphone — you don’t open it up to add features; you just download apps to extend its capabilities.

Practical coding example in C#:

Before applying for OCP:

public class Rectangle
{
    public double Width { get; set; }
    public double Height { get; set; }
}

public class AreaCalculator
{
    public double CalculateArea(Rectangle rectangle)
    {
        return rectangle.Width * rectangle.Height;
    }
}

This design may become problematic when adding new shapes. Modifying the AreaCalculator for each new shape violates the OCP.

After applying for OCP:

public interface IShape
{
    double CalculateArea();
}

public class Rectangle : IShape
{
    // implementation
}

public class Circle : IShape
{
    // implementation
}

By introducing an interface (IShape), new shapes like Circle can be added without modifying existing code, adhering to the OCP.

Explanation: According to OCP, the class should be open for extension but closed for modification. So, When you introduce a new shape, then just implement it from the interface IShape. So IShape is open for extension but closed for further modification.

3. Liskov Substitution Principle (LSP)

The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.

Key idea: You should be able to use any subclass where you use its parent class.

Real-time example: You have a remote control that works for all types of TVs, regardless of the brand.

Practical coding example in C#:

Before applying for LSP:

public class Bird
{
    public virtual void Fly() { /* implementation */ }
}

public class Penguin : Bird
{
    public override void Fly()
    {
        throw new NotImplementedException("Penguins can't fly!");
    }
}

Here, the Penguin class violates the LSP by throwing an exception for the Fly method.

After applying for LSP:

public interface IFlyable
{
    void Fly();
}

public class Bird : IFlyable
{
    public void Fly()
    {
        // implementation specific to Bird
    }
}

public class Penguin : IFlyable
{
    public void Fly()
    {
        // implementation specific to Penguins
        throw new NotImplementedException("Penguins can't fly!");
    }
}

By introducing the IFlyable interface, both Bird and Penguin adhere to the Liskov Substitution Principle.

Explanation: According to LSP, a derived class should not break the base class’s type definition and behavior which means objects of a base class shall be replaceable with objects of its derived classes without breaking the application. This needs the objects of derived classes to behave in the same way as the objects of your base class.

4. Interface Segregation Principle (ISP)

The Interface Segregation Principle states that a class should not be forced to implement interfaces it does not use. This principle encourages the creation of small, client-specific interfaces.

Key idea: A class should not be forced to implement interfaces it doesn’t use.

Real-time example: You sign up for a music streaming service and only choose the genres you like, not all available genres.

Practical coding example in C#:

Before applying for ISP:

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

public class Manager : IWorker
{
    // implementation
}

public class Robot : IWorker
{
    // implementation
}

The Robot class is forced to implement the Eat method, violating ISP.

After applying for ISP:

public interface IWorkable
{
    void Work();
}

public interface IEatable
{
    void Eat();
}

public class Manager : IWorkable, IEatable
{
    // implementation
}

public class Robot : IWorkable
{
    // implementation
}

By splitting the IWorker interface into smaller interfaces (IWorkable and IEatable), classes can implement only what they need, adhering to ISP.

Explanation: According to LSP, any client should not be forced to use an interface that is irrelevant to it. In other words, clients should not be forced to depend on methods that they do not use.

5. Dependency Inversion Principle (DIP)

The Dependency Inversion Principle suggests that high-level modules should not depend on low-level modules, but both should depend on abstractions. Additionally, abstractions should not depend on details; details should depend on abstractions.

Key idea: High-level modules should not depend on low-level modules; both should depend on abstractions.

Real-time example: Building a LEGO tower — the bricks (high and low-level modules) connect through smaller bricks (abstractions).

Practical coding example in C#:

Before applying DIP:

public class LightBulb
{
    public void TurnOn() { /* implementation */ }
    public void TurnOff() { /* implementation */ }
}

public class Switch
{
    private LightBulb bulb;

    public Switch(LightBulb bulb)
    {
        this.bulb = bulb;
    }

    public void Toggle()
    {
        if (bulb.IsOn)
            bulb.TurnOff();
        else
            bulb.TurnOn();
    }
}

The Switch class directly depends on the concrete LightBulb class, violating DIP.

After applying DIP:

public interface ISwitchable
{
    void TurnOn();
    void TurnOff();
}

public class LightBulb : ISwitchable
{
    // implementation
}

public class Switch
{
    private ISwitchable device;

    public Switch(ISwitchable device)
    {
        this.device = device;
    }

    public void Toggle()
    {
        if (device.IsOn)
            device.TurnOff();
        else
            device.TurnOn();
    }
}

By introducing an interface (ISwitchable), the Switch class now depends on an abstraction, adhering to the Dependency Inversion Principle.

Explanation: According to DIP, do not write any tightly coupled code because that is a nightmare to maintain when the application is growing bigger and bigger. If a class depends on another class, then we need to change one class if something changes in that dependent class. We should always try to write loosely coupled classes.

Conclusion

Remember, by understanding and applying these SOLID principles, .NET developers can create more robust, flexible, and maintainable software. It’s important to note that these principles work together and complement each other, contributing to the overall design philosophy of object-oriented programming.

Similar
May 29, 2023
Maximizing performance in asynchronous programming Task and Task<TResult> The Task and Task<TResult> types were introduced in .NET 4.0 as part of the Task Parallel Library (TPL) in 2010, which provided a new model for writing multithreaded and asynchronous code. For...
May 8, 2023
Author: Waqas Ahmed
Dapper is a lightweight ORM (Object-Relational Mapping) framework for .NET Core and is commonly used to query databases in .NET Core applications. Here are some of the advanced features of Dapper in .NET Core: Multi-Mapping: Dapper allows you to map...
Jun 27
Author: Dayanand Thombare
Introduction Caching is a technique used to store frequently accessed data in a fast-access storage layer to improve application performance and reduce the load on backend systems. By serving data from the cache, we can avoid expensive database queries or...
May 24
Author: Cody Slingerland
Discover the best CI/CD tools for your stack — including tools to help manage and optimize your continuous integration and delivery pipelines. The modern software development lifecycle comprises two key phases: continuous integration (CI) and continuous delivery or deployment (CD)....
Send message
Type
Email
Your name
*Message