Quick SOLID 101
1️⃣ Single Responsibility
-
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
- 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
-
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
- contravariance
-
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
-
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
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.
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 concreteFileLogger
&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
referencesILogger
.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); } } }
No Comments