SOLID Principles

Explained in simple words(C#)

SOLID Principles

Solid principles provide a foundation for writing high-quality code. By adhering to these principles, developers can create code that is efficient, maintainable, clean and scalable. Rather than delving into theory, let's examine how these principles can be applied in practice.

S - Single Responsibility Principle (SRP)

The Single Responsibility Principle states that a class or module should have only one reason to change. In simple words, a class or library should be responsible for only one functionality or task.

Let's understand it with code, here we have a class it has implementation as shown in code.

public class Users
{
    public string UserId { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
    public int ProductId { get; set; }
    public string ProductName { get; set; }
    public string ProductDescription { get; set; }

    public void AuthenticateUser(string _username, string _password)
    {
        //Code implementation to authenticate user
    }

    public void AddProduct(int _productId, string _productName, string         _productDescription)
    {
        //Code implementation to add product
    }
}

Here the class User contains functionality as well as properties of User as well as of Product, which is not a scalable and maintainable approach.

As per SRP, we divide the functionality of User and Product so they do not interfere with each other's functionality, and also the class with have only reason to change when there is an addition or any modification related to it.

public class Users
{
    public string UserId { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }

    public void AuthenticateUser(string _username, string _password)
    {
        //Code implementation to authenticate user
    }
}
public class Products
{
    public int ProductId { get; set; }
    public string ProductName { get; set; }
    public string ProductDescription { get; set; }

    public void AddProduct(int _productId, string _productName, string _productDescription)
    {
        //Code implementation to add product
    }
}

O - Open/Closed Principle (OCP)

The Open/Closed Principle states that classes or modules should be open for extension but closed for modification. In other words, once a class or module is created, it should not be modified directly. Instead, new functionality should be added through new code that extends the existing code.

Let us check an example of how OCP works.

public static class ExtensionClass
{
    public static bool Rename(this FileInfo fileInfo, string newName)
    {
        try
        {
            fileInfo.MoveTo(newName);
        }
        catch (Exception)
        {
            return false;
        }

        return true;
    }
}

We have added an extension method to FileInfo class which is available in the System.IO namespace, and it does not contain any method named Rename, so we have extended the FileInfo class.

FileInfo file = new FileInfo("test.txt");
file.Rename("newname.txt");

Now, Rename method can be accessed by creating an object of FileInfo.

L - Liskov Substitution Principle (LSP)

Liskov's Substitution Principle states that objects of a superclass should be able to be replaced with objects of a subclass without any problem.

The idea behind the LSP is to make code more flexible and easier to maintain. By ensuring that subclasses can be used interchangeably with their parent classes, we can write code that is more modular and easier to extend.

Let us understand it with a simple example

public class SuperHero
{
    //Implementation of SuperHero
}

public class Batman : SuperHero
{
    //Implementation of Batman
}

Here we have a SuperHero and Batman, where SuperHero is the base class and Batman is inheriting properties of SuperHero. As per Liskov Substitution Principle, we can initialize SuperHero with an instance of Batman.

SuperHero super = new Batman();

I - Interface Segregation Principle (ISP)

The Interface Segregation Principle states that no client should be forced to depend on methods it does not use. Segregation in simple words means separation.

In simpler words, this means that when creating interfaces, we should make sure that they are focused on specific functionality and only include methods that are relevant to that functionality. Classes that implement the interface should not be required to implement methods that they don't need or use.

public interface IDemoInterface
{
    void AddUser();
    void DeleteUser();
    string GetUser(int UserId);
    void AddProduct();
    void DeleteProduct();
    string GetProduct(int ProductId);
    double GetPrice(int ProductId);
}

Here in the above code if we want to implement IDemoInterface into our class that has user management functionality, we only require some implementation, rather than other unnecessary methods related to product management. So we can separate/segregate the interface, so we implement relevant functionality.

public interface IUser
{
    void AddUser();
    void DeleteUser();
    string GetUser(int UserId);
}
public interface IProduct
{
    void AddProduct();
    void DeleteProduct();
    string GetProduct(int ProductId);
    double GetPrice(int ProductId);
}

D - Dependency Inversion Principle (DIP)

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

In simpler terms, DIP is about designing software in a way that allows for easy changes in the future. By abstracting away the implementation details of low-level components, changes to those components will not affect the high-level components that depend on them. This makes it easier to make changes to the system without causing unintended consequences.

Let's understand it using a simple example.

interface IDirectory
{
    List<string> GetDirectories();
}

public class FTPDirectory : IDirectory
{
    public List<string> GetDirectories()
    {
        //Implementation to get FTP Directories
    }
}

public class LocalDriveDirectory : IDirectory
{
    public List<string> GetDirectories()
    {
        //Implementation to get Local drive Directories
    }
}

Here we have an interface, which is being implemented by two classes FTPDirectory and LocalDriveDirectory. In the below code, we are injecting dependency using a constructor and accessing methods of Interface IDirectory, which is an abstraction for low-level classes FTPDirectory and LocalDriveDirectory.

public class DirectoryExplorer
{
    private readonly idirectory _directory;

    public directoryexplorer(idirectory directory)
    {
        _directory = directory;
    }

    public list<string> listdirectories()
    {
        return _directory.getdirectories();
    }
}

By using Dependency Inversion Principle, our high-level class is not directly dependent on low-level classes but depends on abstraction. This ensures decoupled, flexible and modular code. We can create instances of Directory Explorer in the following ways, based on our requirements.

DirectoryExplorer directoryExplorer = new DirectoryExplorer(new FTPDirectory());
DirectoryExplorer directoryExplorer = new DirectoryExplorer(new LocalDriveDirectory());

Thank you for taking the time to read our first blog post on writing clean code.

If you have any thoughts or suggestions on how we can improve our blog posts, please don't hesitate to let us know. We're always looking for ways to enhance our content and make it more engaging for our readers. If there's a topic you'd like us to cover in a future blog post, we'd love to hear your ideas.

You can leave a comment below or contact us directly through our website to share your thoughts. We look forward to hearing from you and continuing to provide valuable content for our readers.

Did you find this article valuable?

Support NullByte Tech Journals by becoming a sponsor. Any amount is appreciated!