Skip to main content

Quick SOLID 101

1️⃣ Single Responsibility

Quick%20SOLID%20101%20fd3c41414fa1478584dd10a759e1b1c1/Untitled.png

  • Each class contains one responsibility

  • Example:

    • UserService class is just for Register. No Send Email process and Validate Email.
    public class UserService
    {
       EmailService _emailService;
       DbContext _dbContext;
       public UserService(EmailService aEmailService, DbContext aDbContext)
       {
          _emailService = aEmailService;
          _dbContext = aDbContext;
       }
       public void Register(string email, string password)
       {
          if (!_emailService.ValidateEmail(email))
             throw new ValidationException("Email is not an email");
             var user = new User(email, password);
             _dbContext.Save(user);
             emailService.SendEmail(new MailMessage("myname@mydomain.com", email) {Subject="Hi. How are you!"});
    
          }
       }
       public class EmailService
       {
           SmtpClient _smtpClient;
    		   public EmailService(SmtpClient aSmtpClient)
    		   {
    		      _smtpClient = aSmtpClient;
    		   }
    		   public bool virtual ValidateEmail(string email)
    		   {
    		      return email.Contains("@");
    		   }
    		   public bool SendEmail(MailMessage message)
    		   {
    		      _smtpClient.Send(message);
    		   }
    }
    
    

👐 Open closed principle

Quick%20SOLID%20101%20fd3c41414fa1478584dd10a759e1b1c1/Untitled%201.png

  • Open for extension closed for modification
  • Class is designed with future use in mind
  • Example:
    • Instead of:

      public class Rectangle
      {
          public double Width { get; set; }
          public double Height { get; set; }
      }
      
      public double Area(object[] shapes)
      {
          double area = 0;
          foreach (var shape in shapes)
          {
              if (shape is Rectangle)
              {
                  Rectangle rectangle = (Rectangle) shape;
                  area += rectangle.Width*rectangle.Height;
              }
              else
              {
                  Circle circle = (Circle)shape;
                  area += circle.Radius * circle.Radius * Math.PI;
              }
          }
      
          return area;
      }
      
      
    • We Use

      public abstract class Shape
      {
          public abstract double Area();
      }
      
      public class Rectangle : Shape
      {
          public double Width { get; set; }
          public double Height { get; set; }
          public override double Area()
          {
              return Width*Height;
          }
      }
      
      public class Circle : Shape
      {
          public double Radius { get; set; }
          public override double Area()
          {
              return Radius*Radius*Math.PI;
          }
      }
      
      

👋 Liskov substitution principle

Quick%20SOLID%20101%20fd3c41414fa1478584dd10a759e1b1c1/Untitled%202.png

  • You should be able to use any derived class instead of a parent class and have it behave in the same manner without modification

  • Behavioural subtyping

  • Focus on contravariance & covariance

    • contravariance
      • Method parameter should be open as much as possible, using more base parent type
      • Parameter type can be any specific subtype but treated as the base type in method process
    • covariance
      • Method return type should be open as much as possible, using more base parent type
      • Return type is base parent type in method process, if possible, even when the inside process of the method use specific typing of said type
  • Example:

    public interface IReadableSqlFile
    {
       string LoadText();
    }
    public interface IWritableSqlFile
    {
       void SaveText();
    }
    
    public class ReadOnlySqlFile: IReadableSqlFile
    {
       public string FilePath {get;set;}
       public string FileText {get;set;}
       public string LoadText()
       {
          /* Code to read text from sql file */
       }
    }
    
    public class SqlFile: IWritableSqlFile,IReadableSqlFile
    {
       public string FilePath {get;set;}
       public string FileText {get;set;}
       public string LoadText()
       {
          /* Code to read text from sql file */
       }
       public void SaveText()
       {
          /* Code to save text into sql file */
       }
    }
    
    public class SqlFileManager
    {
       public string GetTextFromFiles(List<IReadableSqlFile> aLstReadableFiles)
       {
          StringBuilder objStrBuilder = new StringBuilder();
          foreach(var objFile in aLstReadableFiles)
          {
             objStrBuilder.Append(objFile.LoadText());
          }
          return objStrBuilder.ToString();
       }
       public void SaveTextIntoFiles(List<IWritableSqlFile> aLstWritableFiles)
       {
       foreach(var objFile in aLstWritableFiles)
       {
          objFile.SaveText();
       }
       }
    }
    
    

🔌 Interface segregation principle

Quick%20SOLID%20101%20fd3c41414fa1478584dd10a759e1b1c1/Untitled%203.png

  • Splits interfaces that are very large into smaller and more specific ones

  • These conform to Single responsibility, as even each interface must declare each of their responsibility, and we must not have a responsibility that's too big.

    • When responsibility is too big, we must split it into multiple different responsibility (decoupling)
    • By splitting responsibility, each changes made is contained within those responsibility, making development can progress faster and more robust
  • Example:

    public Interface ILead
    {
       void CreateSubTask();
       void AssginTask();
       void WorkOnTask();
    }
    public class TeamLead : ILead
    {
       public void AssignTask()
       {
          //Code to assign a task.
       }
       public void CreateSubTask()
       {
          //Code to create a sub task
       }
       public void WorkOnTask()
       {
          //Code to implement perform assigned task.
       }
    
    

    The design looks fine until there are role like Manager that do not WorkOnTask.

    public class Manager: ILead
    {
       public void AssignTask()
       {
          //Code to assign a task.
       }
       public void CreateSubTask()
       {
          //Code to create a sub task.
       }
       public void WorkOnTask()
       {
          throw new Exception("Manager can't work on Task");
       }
    }
    
    

    Because of that, we should design as follow:

    public interface ILead
    {
       void AssignTask();
       void CreateSubTask();
    }
    
    public interface IProgrammer
    {
       void WorkOnTask();
    }
    
    public class TeamLead: IProgrammer, ILead
    {
       public void AssignTask()
       {
          //Code to assign a Task
       }
       public void CreateSubTask()
       {
          //Code to create a sub task from a task.
       }
       public void WorkOnTask()
       {
          //code to implement to work on the Task.
       }
    }
    
    

🔀 Dependency Inversion principle

Quick%20SOLID%20101%20fd3c41414fa1478584dd10a759e1b1c1/Untitled%204.png

Dependency inversion principle - Wikipedia

  • Dependency modules are reversed, thus rendering high-level modules independent of the low-level module implementation details.

    • High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces).
    • Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

    Quick%20SOLID%20101%20fd3c41414fa1478584dd10a759e1b1c1/Untitled%205.png

    Invert the package dependency from Package A needs Package B to Package B needs Package A, by declaring Interface A in Package A. Interface A contains the definition of the responsibility Object B needed to fulfill.

    Before:

    ExceptionLogger need concrete FileLogger & DbLogger

    public class DbLogger
    {
       public void LogMessage(string aMessage)
       {
          //Code to write message in database.
       }
    }
    public class FileLogger
    {
       public void LogMessage(string aStackTrace)
       {
          //code to log stack trace into a file.
       }
    }
    public class ExceptionLogger
    {
       public void LogIntoFile(Exception aException)
       {
          FileLogger objFileLogger = new FileLogger();
          objFileLogger.LogMessage(GetUserReadableMessage(aException));
       }
       public void LogIntoDataBase(Exception aException)
       {
          DbLogger objDbLogger = new DbLogger();
          objDbLogger.LogMessage(GetUserReadableMessage(aException));
       }
       private string GetUserReadableMessage(Exception ex)
       {
          string strMessage = string.Empty;
          //code to convert Exception's stack trace and message to user readable format.
          //....
          //....
          return strMessage;
       }
    }
    public class DataExporter
    {
       public void ExportDataFromFile()
       {
          try {
             //code to export data from files to database.
          }
          catch(IOException ex)
          {
             new ExceptionLogger().LogIntoDataBase(ex);
          }
          catch(Exception ex)
          {
             new ExceptionLogger().LogIntoFile(ex);
          }
       }
    }
    
    

    After:

    ExceptionLogger references ILogger.

    public interface ILogger
    {
       void LogMessage(string aString);
    }
    
    public class DbLogger: ILogger
    {
       public void LogMessage(string aMessage)
       {
          //Code to write message in database.
       }
    }
    public class FileLogger: ILogger
    {
       public void LogMessage(string aStackTrace)
       {
          //code to log stack trace into a file.
       }
    }
    
    public class ExceptionLogger
    {
       private ILogger _logger;
       public ExceptionLogger(ILogger aLogger)
       {
          this._logger = aLogger;
       }
       public void LogException(Exception aException)
       {
          string strMessage = GetUserReadableMessage(aException);
          this._logger.LogMessage(strMessage);
       }
       private string GetUserReadableMessage(Exception aException)
       {
          string strMessage = string.Empty;
          //code to convert Exception's stack trace and message to user readable format.
          //....
          //....
          return strMessage;
       }
    }
    
    public class DataExporter
    {
       public void ExportDataFromFile()
       {
          ExceptionLogger _exceptionLogger;
          try {
             //code to export data from files to database.
          }
          catch(IOException ex)
          {
             _exceptionLogger = new ExceptionLogger(new DbLogger());
             _exceptionLogger.LogException(ex);
          }
          catch(Exception ex)
          {
             _exceptionLogger = new ExceptionLogger(new FileLogger());
             _exceptionLogger.LogException(ex);
          }
       }
    }