Coding Principles - SOLID

Short introduction about the topic,

  • In order to enable scalable architecture and software development, principles are best practices to adhere to. 
  • Design patterns are methods for structuring and designing your code for a solution.

Each design pattern has a unique use case and can be used in a variety of situations. On the other hand, in order to have high-quality code, you almost always need to adhere to certain standards.

Certain design patterns are required by certain principles.

  • Using the Open/Closed principle as an example, a strategy pattern is highly suggested. 
  • IoC Design Pattern and its connection to DI Principle are the foundation of .NET Core.

Following the SOLID principles when creating software has a number of benefits, including:

  • Improved maintainability: The SOLID principles encourage the production of modular, adaptable code that is less prone to errors and more resistant to changes in requirements, so you can create code that is easier to maintain and adjust over time.
  • Reduced complexity: The SOLID principles encourage the use of abstraction and encapsulation, which can make the code easier to comprehend and work with. This helps to reduce the complexity of software.
  • Increased flexibility: These guidelines support the development of adaptable code that is open to expansion but closed to alteration, which fosters adaptability without compromising current functioning.
  • Increased scalability: The SOLID principles can aid in increasing the scalability of software.

Solid Principle: It's a rule to write code.

"To create an understandable, readable, and testable code that many developers can collaboratively work on."

solid principle

Single Responsibility Principle

The single-responsibility principle (SRP) states that each class, module, or function in your program should be limited to carrying out a single duty. In other words, each class, module or function should be fully accountable for just one feature. Only variables and methods necessary for the class's functioning should be included.

Real-Time

We need to write a code to save student information while registering and send mail to him and admin to know if a student has raised the registration request successfully or failed.

Please check the below sample codes,

#SRP Not Applied Code:

public class StudentBusinessAccess
{
    public StudentBusinessAccess()
    {

    }

    /// <summary>
    /// This Method for example alone not for execute. To understand the concept.
    /// </summary>
    /// <param name="studentBO"></param>
    /// <returns></returns>
    public bool SaveStudentDetails(StudentBO studentBO)
    {
        // Collect and Map Student Informations IF EF

        StudentEntity studentEntity = new StudentEntity();
        studentEntity.Name = studentBO.Name;

        StudentDataAccesss ctx = new StudentDataAccesss();
        ctx.SaveStidentInfo(studentEntity);
             
        // Save the Details to Database
        // Wirte Insert Query to Save Data to DB. IF ADO.NET
        string query = "Insert student Details query";
        SqlCommand command = new SqlCommand(query);

        // Send Email to Admin and Student for the Register Success
        // Send Email to Admin if Register Failed
        MailMessage mailMessage = new MailMessage();
        SmtpClient smtpClient = new SmtpClient();
        smtpClient.Send(mailMessage);
        return true;
    }
}

Please check the above code “StudentBusinessAccess” was owner for save the Student Information. The SOLID Principle isn't being used, though. The code will be difficult to maintain and unable to be improved if all business requirements are contained in a single class.

Let's explore how we can implement SRP in the aforementioned code.

#SRP Applied Code:
public class StudentBusinessAccess
{
    public StudentBusinessAccess()
    {

    }

    public bool SaveStudentDetails(StudentBO studentBO)
    {
        // Save or register the Student Information to Database
        new StudentDAL().SaveStudent(studentBO);

        // Send Email to Admin and Student for the Register Success
        // Send Email to Admin if Register Failed
        EMailUtil.SendEmail(new MailMessage(), "Student Registeration was Success");

        return true;
    }
}

public class EMailUtil
{
    public static void SendEmail(MailMessage mailMessage, string subject)
    { 
            
    }
}

public class StudentDAL
{
    public StudentDAL()
    {
                
    }

    public int SaveStudent(StudentBO studentBO)
    {
        return 1;
    }
}

We have three distinct classes, including StudentDAL, StudentBusinessAccess, and EMailUtil for carrying out their own responsibilities.

  • EMailUtil class will take care the job of send emails.
  • StudentDAL class will take care the job of Database connectivity and data operations.
  • StudentBusinessAccess class will call methods of Data Access and Until Jobs for the needs.

Open - Closed Principle

Building a plugin system where new features can be added by implementing a common interface without impact the existing functionality. Let’s take the EMailUtil Class here for the process. Initial we have implemented direct Email send method. After sometime, We need to implement the SendGrid API or Outlook 365 SMTP for Send Emails.

public class EMailUtil
{
    public static void SendEmail(MailMessage mailMessage, string subject)
    { 
            
    }
    public static void ConfigEmail()
    { 
            
    }
}

Open - Closed Principle

In Future, we can able to add one more new implementation if need without touching the existing code.

public interface IEMailOperations
{
    void SetupEMail();

    void SendEmail();

    void SendBulkEmail();

    void GetStacks();
}

public class SendGridAPIOperation : IEMailOperations
{
    public void GetStacks()
    {
        throw new NotImplementedException();
    }

    public void SendBulkEmail()
    {
        throw new NotImplementedException();
    }

    public void SendEmail()
    {
        throw new NotImplementedException();
    }

    public void SetupEMail()
    {
        throw new NotImplementedException();
    }
}

public class SMTPOperation : IEMailOperations
{
    public void GetStacks()
    {
        throw new NotImplementedException();
    }

    public void SendBulkEmail()
    {
        throw new NotImplementedException();
    }

    public void SendEmail()
    {
        throw new NotImplementedException();
    }

    public void SetupEMail()
    {
        throw new NotImplementedException();
    }
}

Liskov Substitution Principle

Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program."Robert c. Martin

The Liskov substitution principle (LSP) is a specific definition of a subtyping relation created by Barbara Liskov and Jeannette Wing. The principle says that any class must be directly replaceable by any of its subclasses without error.

In other words, each subclass must maintain all behavior from the base class along with any new behaviors unique to the subclass. The child class must be able to process all the same requests and complete all the same tasks as its parent class.

In practice, programmers tend to develop classes based on behavior and grow behavioral capabilities as the class becomes more specific. The advantage of LSP is that it speeds up the development of new subclasses as all subclasses of the same type share a consistent use. You can trust that all newly created subclasses will work with the existing code. If you decide that you need a new subclass, you can create it without reworking the existing code.

public abstract class EMailOperations
{
    public virtual void GetStacks()
    {
        throw new NotImplementedException();
    }

}
public class SendGridAPIOperation : EMailOperations
{
    public override void GetStacks()
    {
        throw new NotImplementedException();
    }
}
public class SMTPOperation : EMailOperations
{
    public override void GetStacks()
    {
        throw new NotImplementedException();
    }
}
EMailOperations eMail = new SendGridAPIOperation();
eMail.GetStacks();

eMail = new SMTPOperation();
eMail.GetStacks();

Interface segregation principle

Create small, focused interfaces catering to clients’ needs. Avoid creating interfaces with methods that aren’t relevant to all implementing classes.

Classes must only be able to carry out behaviours that are necessary to achieve their intended functionality in accordance with the interface segregation principle (ISP). Thus, classes do not cover behaviours that they do not practise. This is connected to the first SOLID principle because when used combined, these two principles deprive a class of any variables, processes, or behaviours that do not directly advance their function. The entire purpose of a method must be the same as the end result.

In the below, Image, We have an interface for List Operations, Contains both number and string (word) operations. It was wrong.

Interface segregation principle

For correct the implementation, See the below image,

Interface segregation principle

Make an interface named IListOperations for list operations, then make separate interfaces for numbers and words that are unrelated to one another and use IListOperations.

Dependency Inversion Principle

The dependency inversion principle (DIP) has two parts:

  • High-level modules should not depend on low-level modules. Instead, both should depend on abstractions (interfaces)

Abstractions should not depend on details. Details (like concrete implementations) should depend on abstractions.

public interface IStudentAccess
{
    string GetName();
}

public class StudentAccess : IStudentAccess
{
    public string GetName()
    {
        return "";
    }
}

public class StudentFactory
{
    public static IStudentAccess GetStudentAccess()
    {
        return new StudentAccess();
    }
}
public class StudentBusinessLogicWay2
{
    public string GetName()
    {
        return StudentFactory.GetStudentAccess().GetName();
    }
}
public class StudentBusinessLogicWay1
{ 
    readonly IStudentAccess studentAccess;

    // Method #1
    public StudentBusinessLogicWay1(IStudentAccess studentAccess)
    {
        this.studentAccess = studentAccess; 
    }

    public string GetName()
    {
       return this.studentAccess.GetName();
    }
}
public interface IStudentAccess
{
  string GetName();
}

public class StudentAccess : IStudentAccess
{
  public string GetName()
  {
    return "";
  }
}

In the above code, StudentAccess Class will return Name, so It implement the IStudentAccess interface.

public class StudentFactory
{
    public static IStudentAccess GetStudentAccess()
    {
        return new StudentAccess();
    }
}

In the above code, we created a factory class for StudentFacotry create StudentAccess class object and return as IStudentAccess.

{
    public string GetName()
    {
        return StudentFactory.GetStudentAccess().GetName();
    }
}
public class StudentBusinessLogicWay1
{ 
    readonly IStudentAccess studentAccess;

    // Method #1
    public StudentBusinessLogicWay1(IStudentAccess studentAccess)
    {
        this.studentAccess = studentAccess; 
    }

    public string GetName()
    {
       return this.studentAccess.GetName();
    }
}

The above logic, Will access the created to access the StudentAccess.

We’ll go into more detail regarding DRY and KISS principles in the upcoming post.

Source  Link: https://github.com/Vinoar/Solid_Applied