asp.net core系列 65 正反案例介紹SOLID原則
一.概述
SOLID五大原則使我們能夠管理解決大多數軟體設計問題。由Robert C. Martin在20世紀90年代編寫了這些原則。這些原則為我們提供了從緊耦合的程式碼和少量封裝轉變為適當鬆耦合和封裝業務實際需求的結果方法。使用這些原則,我們可以構建一個具有整潔,可讀且易於維護的程式碼應用程式。
SOLID縮寫如下:
SRP 單一責任原則
OCP 開放/封閉原則
LSP 里氏替換原則
ISP 介面分離原則
DIP 依賴反轉原則
1.單一責任原則SRP
一個類承擔的責任在理想情況下應該是多少個呢?答案是一個。這個責任是圍繞一個核心任務構建,不是簡化的意思。通過暴露非常有限的責任使這個類與系統的交集更小。
(1) 演示:違反了單一責任原則,原因是:顧客類中承擔了太多無關的責任。
/// <summary> /// 顧客類所有實現 /// </summary> public class Cliente { public int ClienteId { get; set; } public string Nome { get; set; } public string Email { get; set; } public string CPF { get; set; } public DateTime DataCadastro { get; set; } public string AdicionarCliente() { //顧客資訊驗證 if (!Email.Contains("@")) return "Cliente com e-mail inválido"; if (CPF.Length != 11) return "Cliente com CPF inválido"; //儲存顧客資訊 using (var cn = new SqlConnection()) { var cmd = new SqlCommand(); cn.ConnectionString = "MinhaConnectionString"; cmd.Connection = cn; cmd.CommandType = CommandType.Text; cmd.CommandText = "INSERT INTO CLIENTE (NOME, EMAIL CPF, DATACADASTRO) VALUES (@nome, @email, @cpf, @dataCad))"; cmd.Parameters.AddWithValue("nome", Nome); cmd.Parameters.AddWithValue("email", Email); cmd.Parameters.AddWithValue("cpf", CPF); cmd.Parameters.AddWithValue("dataCad", DataCadastro); cn.Open(); cmd.ExecuteNonQuery(); } //釋出郵件 var mail = new MailMessage("[email protected]", Email); var client = new SmtpClient { Port = 25, DeliveryMethod = SmtpDeliveryMethod.Network, UseDefaultCredentials = false, Host = "smtp.google.com" }; mail.Subject = "Bem Vindo."; mail.Body = "Parabéns! Você está cadastrado."; client.Send(mail); return "Cliente cadastrado com sucesso!"; } }
(2) 解決方案,使用單一責任原則,每個類只負責自己的業務。
/// <summary> /// 顧客實體 /// </summary> public class Cliente { public int ClienteId { get; set; } public string Nome { get; set; } public string Email { get; set; } public string CPF { get; set; } public DateTime DataCadastro { get; set; } /// <summary> /// 顧客資訊驗證 /// </summary> /// <returns></returns> public bool IsValid() { return EmailServices.IsValid(Email) && CPFServices.IsValid(CPF); } } /// <summary> /// 儲存顧客資訊 /// </summary> public class ClienteRepository { /// <summary> /// 儲存 /// </summary> /// <param name="cliente">要儲存的顧客實體</param> public void AdicionarCliente(Cliente cliente) { using (var cn = new SqlConnection()) { var cmd = new SqlCommand(); cn.ConnectionString = "MinhaConnectionString"; cmd.Connection = cn; cmd.CommandType = CommandType.Text; cmd.CommandText = "INSERT INTO CLIENTE (NOME, EMAIL CPF, DATACADASTRO) VALUES (@nome, @email, @cpf, @dataCad))"; cmd.Parameters.AddWithValue("nome", cliente.Nome); cmd.Parameters.AddWithValue("email", cliente.Email); cmd.Parameters.AddWithValue("cpf", cliente.CPF); cmd.Parameters.AddWithValue("dataCad", cliente.DataCadastro); cn.Open(); cmd.ExecuteNonQuery(); } } } /// <summary> /// CPF服務 /// </summary> public static class CPFServices { public static bool IsValid(string cpf) { return cpf.Length == 11; } } /// <summary> /// 郵件服務 /// </summary> public static class EmailServices { public static bool IsValid(string email) { return email.Contains("@"); } public static void Enviar(string de, string para, string assunto, string mensagem) { var mail = new MailMessage(de, para); var client = new SmtpClient { Port = 25, DeliveryMethod = SmtpDeliveryMethod.Network, UseDefaultCredentials = false, Host = "smtp.google.com" }; mail.Subject = assunto; mail.Body = mensagem; client.Send(mail); } } /// <summary> /// 客戶服務,程式呼叫入口 /// </summary> public class ClienteService { public string AdicionarCliente(Cliente cliente) { //先驗證 if (!cliente.IsValid()) return "Dados inválidos"; //儲存顧客 var repo = new ClienteRepository(); repo.AdicionarCliente(cliente); //郵件傳送 EmailServices.Enviar("[email protected]", cliente.Email, "Bem Vindo", "Parabéns está Cadastrado"); return "Cliente cadastrado com sucesso"; } }
2. 開放/封閉原則OCP
類應該是可以可擴充套件的,可以用作構建其他相關新功能,這叫開放。但在實現相關功能時,不應該修改現有程式碼(因為已經過單元測試執行正常)這叫封閉。
(1) 演示:違反了開放/封閉原則,原因是每次增加新形狀時,需要改變AreaCalculator 類的TotalArea方法,例如開發後期又增加了圓形形狀。
/// <summary> /// 長方形實體 /// </summary> public class Rectangle { public double Height { get; set; } public double Width { get; set; } } /// <summary> /// 圓形 /// </summary> public class Circle { /// <summary> /// 半徑 /// </summary> public double Radius { get; set; } } /// <summary> /// 面積計算 /// </summary> public class AreaCalculator { public double TotalArea(object[] arrObjects) { double area = 0; Rectangle objRectangle; Circle objCircle; foreach (var obj in arrObjects) { if (obj is Rectangle) { objRectangle = (Rectangle)obj; area += objRectangle.Height * objRectangle.Width; } else { objCircle = (Circle)obj; area += objCircle.Radius * objCircle.Radius * Math.PI; } } return area; } }
(2) 解決方案,使用開放/封閉原則,每次增加新形狀時(開放),不需要修改TotalArea方法(封閉)
/// <summary> /// 形狀抽象類 /// </summary> public abstract class Shape { /// <summary> /// 面積計算 /// </summary> /// <returns></returns> public abstract double Area(); } /// <summary> /// 長方形 /// </summary> public class Rectangle : Shape { public double Height { get; set; } public double Width { get; set; } public override double Area() { return Height * Width; } } /// <summary> /// 圓形 /// </summary> public class Circle : Shape { public double Radius { get; set; } public override double Area() { return Radius * Radius * Math.PI; } } /// <summary> /// 面積計算 /// </summary> public class AreaCalculator { public double TotalArea(Shape[] arrShapes) { double area = 0; foreach (var objShape in arrShapes) { area += objShape.Area(); } return area; } }
3.里氏替換原則LSP
這裡也涉及到了類的繼承,也適用於介面。子類可以替換它們的父類。里氏替換原則常見的程式碼問題是使用虛方法,在父類定義虛方法時,要確保該方法裡沒有任何私有成員。
(1) 演示:違反了里氏替換原則, 原因是不能使用ReadOnlySqlFile子類替代SqlFile父類。
/// <summary> /// sql檔案類 讀取、儲存 /// </summary> public class SqlFile { public string FilePath { get; set; } public string FileText { get; set; } public virtual string LoadText() { /* Code to read text from sql file */ return ".."; } public virtual void SaveText() { /* Code to save text into sql file */ } } /// <summary> /// 開發途中增加了sql檔案只讀類 /// </summary> public class ReadOnlySqlFile : SqlFile { public override string LoadText() { /* Code to read text from sql file */ return ".."; } public override void SaveText() { /* Throw an exception when app flow tries to do save. */ throw new IOException("Can't Save"); } } public class SqlFileManager { /// <summary> /// 集合中存在兩種類:SqlFile和ReadOnlySqlFile /// </summary> public List<SqlFile> lstSqlFiles { get; set; } /// <summary> /// 讀取 /// </summary> /// <returns></returns> public string GetTextFromFiles() { StringBuilder objStrBuilder = new StringBuilder(); foreach (var objFile in lstSqlFiles) { objStrBuilder.Append(objFile.LoadText()); } return objStrBuilder.ToString(); } /// <summary> /// 儲存 /// </summary> public void SaveTextIntoFiles() { foreach (var objFile in lstSqlFiles) { //檢查當前物件是ReadOnlySqlFile類,跳過呼叫SaveText()方法 if (!(objFile is ReadOnlySqlFile)) { objFile.SaveText(); } } } }
(2) 解決方案,使用里氏替換原則,子類可以完全代替父類
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 */ return ""; } } 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 */ return ""; } 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) { //ReadOnlySqlFile的LoadText實現 objStrBuilder.Append(objFile.LoadText()); } return objStrBuilder.ToString(); } public void SaveTextIntoFiles(List<IWritableSqlFile> aLstWritableFiles) { foreach (var objFile in aLstWritableFiles) { //SqlFile的SaveText實現 objFile.SaveText(); } } }
4.介面分離原則ISP
介面分離原則是解決介面臃腫的問題,建議介面保持最低限度的函式。永遠不應該強迫客戶端依賴於它們不用的介面。
(1) 演示:違反了介面分離原則。原因是Manager無法處理任務,同時沒有人可以將任務分配給Manager,因此WorkOnTask方法不應該在Manager類中。
/// <summary> /// 領導介面 /// </summary> public interface ILead { //建立任務 void CreateSubTask(); //分配任務 void AssginTask(); //處理指定任務 void WorkOnTask(); } /// <summary> /// 團隊領導 /// </summary> public class TeamLead : ILead { public void AssginTask() { //Code to assign a task. } public void CreateSubTask() { //Code to create a sub task } public void WorkOnTask() { //Code to implement perform assigned task. } } /// <summary> /// 管理者 /// </summary> public class Manager : ILead { public void AssginTask() { //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"); } }
(2) 解決方案,使用介面分離原則
/// <summary> /// 程式設計師角色 /// </summary> public interface IProgrammer { void WorkOnTask(); } /// <summary> /// 領導角色 /// </summary> public interface ILead { void AssignTask(); void CreateSubTask(); } /// <summary> /// 程式設計師:執行任務 /// </summary> public class Programmer : IProgrammer { public void WorkOnTask() { //code to implement to work on the Task. } } /// <summary> /// 管理者:可以建立任務、分配任務 /// </summary> public class Manager : ILead { public void AssignTask() { //Code to assign a Task } public void CreateSubTask() { //Code to create a sub taks from a task. } } /// <summary> /// 團隊領域:可以建立任務、分配任務、執行執行 /// </summary> 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. } }
5. 依賴反轉原則DIP
依賴反轉原則是對程式的解耦。高階模組/類不應依賴於低階模組/類,兩者都應該依賴於抽象。意思是:當某個類被外部依賴時,就需要把該類抽象成一個介面。介面如何變成可呼叫的例項呢?實踐中多用依賴注入模式。這個依賴反轉原則在DDD中得到了很好的運用實踐(參考前三篇)。
(1) 演示:違反了依賴反轉原則。原因是:每當客戶想要引入新的Logger記錄形式時,我們需要通過新增新方法來改變ExceptionLogger類。這裡錯誤的體現了:高階類 ExceptionLogger直接引用低階類FileLogger和DbLogger來記錄異常。
/// <summary> /// 資料庫日誌類 /// </summary> public class DbLogger { //寫入日誌 public void LogMessage(string aMessage) { //Code to write message in database. } } /// <summary> /// 檔案日誌類 /// </summary> 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); } } }
(2) 解決方案,使用依賴反轉原則,這裡演示沒有用依賴注入。
public interface ILogger { void LogMessage(string aString); } /// <summary> /// 資料庫日誌類 /// </summary> public class DbLogger : ILogger { //寫入日誌 public void LogMessage(string aMessage) { //Code to write message in database. } } /// <summary> /// 檔案日誌類 /// </summary> 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); } } }
參考文獻
SOLID原則簡介
&n