SOLID五大原則使咱們可以管理解決大多數軟件設計問題。由Robert C. Martin在20世紀90年代編寫了這些原則。這些原則爲咱們提供了從緊耦合的代碼和少許封裝轉變爲適當鬆耦合和封裝業務實際需求的結果方法。使用這些原則,咱們能夠構建一個具備整潔,可讀且易於維護的代碼應用程序。程序員
SOLID縮寫以下: sql
SRP 單一責任原則數據庫
OCP 開放/封閉原則app
LSP 里氏替換原則ide
DIP 依賴反轉原則單元測試
一個類承擔的責任在理想狀況下應該是多少個呢?答案是一個。這個責任是圍繞一個核心任務構建,不是簡化的意思。經過暴露很是有限的責任使這個類與系統的交集更小。測試
(1) 演示:違反了單一責任原則,緣由是:顧客類中承擔了太多無關的責任。 ui
/// <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("empresa@empresa.com", 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) 解決方案,使用單一責任原則,每一個類只負責本身的業務。this
/// <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("empresa@empresa.com", cliente.Email, "Bem Vindo", "Parabéns está Cadastrado"); return "Cliente cadastrado com sucesso"; } }
類應該是能夠可擴展的,能夠用做構建其餘相關新功能,這叫開放。但在實現相關功能時,不該該修改現有代碼(由於已通過單元測試運行正常)這叫封閉。
(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; } }
這裏也涉及到了類的繼承,也適用於接口。子類能夠替換它們的父類。里氏替換原則常見的代碼問題是使用虛方法,在父類定義虛方法時,要確保該方法裏沒有任何私有成員。
(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(); } } }
接口分離原則是解決接口臃腫的問題,建議接口保持最低限度的函數。永遠不該該強迫客戶端依賴於它們不用的接口。
(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. } }
依賴反轉原則是對程序的解耦。高級模塊/類不該依賴於低級模塊/類,二者都應該依賴於抽象。意思是:當某個類被外部依賴時,就須要把該類抽象成一個接口。接口如何變成可調用的實例呢?實踐中多用依賴注入模式。這個依賴反轉原則在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); } } }
參考文獻