一.概述
SOLID五大原则使我们能够管理解决大多数软件设计问题。由Robert C. Martin在20世纪90年代编写了这些原则。这些原则为我们提供了从紧耦合的代码和少量封装转变为适当松耦合和封装业务实际需求的结果方法。使用这些原则,我们可以构建一个具有整洁,可读且易于维护的代码应用程序。
SOLID缩写如下:
SRP 单一责任原则
OCP 开放/封闭原则
LSP 里氏替换原则
ISP 接口分离原则
DIP 依赖反转原则
1.单一责任原则SRP
一个类承担的责任在理想情况下应该是多少个呢?答案是一个。这个责任是围绕一个核心任务构建,不是简化的意思。通过暴露非常有限的责任使这个类与系统的交集更小。
(1) 演示:违反了单一责任原则,原因是:顾客类中承担了太多无关的责任。
////// 顾客类所有实现 /// 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) 解决方案,使用单一责任原则,每个类只负责自己的业务。
////// 顾客实体 /// 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 bool IsValid() { return EmailServices.IsValid(Email) && CPFServices.IsValid(CPF); } } /// /// 保存顾客信息 /// public class ClienteRepository { /// /// 保存 /// /// 要保存的顾客实体 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(); } } } /// /// CPF服务 /// public static class CPFServices { public static bool IsValid(string cpf) { return cpf.Length == 11; } } /// /// 邮件服务 /// 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); } } /// /// 客户服务,程序调用入口 /// 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方法,例如开发后期又增加了圆形形状。
////// 长方形实体 /// public class Rectangle { public double Height { get; set; } public double Width { get; set; } } /// /// 圆形 /// public class Circle { /// /// 半径 /// public double Radius { get; set; } } /// /// 面积计算 /// 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方法(封闭)
////// 形状抽象类 /// public abstract class Shape { /// /// 面积计算 /// /// public abstract double Area(); } /// /// 长方形 /// public class Rectangle : Shape { public double Height { get; set; } public double Width { get; set; } public override double Area() { return Height * Width; } } /// /// 圆形 /// public class Circle : Shape { public double Radius { get; set; } public override double Area() { return Radius * Radius * Math.PI; } } /// /// 面积计算 /// 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父类。
////// sql文件类 读取、保存 /// 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 */ } } /// /// 开发途中增加了sql文件只读类 /// 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 { /// /// 集合中存在两种类:SqlFile和ReadOnlySqlFile /// public List lstSqlFiles { get; set; } /// /// 读取 /// /// public string GetTextFromFiles() { StringBuilder objStrBuilder = new StringBuilder(); foreach (var objFile in lstSqlFiles) { objStrBuilder.Append(objFile.LoadText()); } return objStrBuilder.ToString(); } /// /// 保存 /// 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(ListaLstReadableFiles) { StringBuilder objStrBuilder = new StringBuilder(); foreach (var objFile in aLstReadableFiles) { //ReadOnlySqlFile的LoadText实现 objStrBuilder.Append(objFile.LoadText()); } return objStrBuilder.ToString(); } public void SaveTextIntoFiles(List aLstWritableFiles) { foreach (var objFile in aLstWritableFiles) { //SqlFile的SaveText实现 objFile.SaveText(); } } }
4.接口分离原则ISP
接口分离原则是解决接口臃肿的问题,建议接口保持最低限度的函数。永远不应该强迫客户端依赖于它们不用的接口。
(1) 演示:违反了接口分离原则。原因是Manager无法处理任务,同时没有人可以将任务分配给Manager,因此WorkOnTask方法不应该在Manager类中。
////// 领导接口 /// public interface ILead { //创建任务 void CreateSubTask(); //分配任务 void AssginTask(); //处理指定任务 void WorkOnTask(); } /// /// 团队领导 /// 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. } } /// /// 管理者 /// 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) 解决方案,使用接口分离原则
////// 程序员角色 /// public interface IProgrammer { void WorkOnTask(); } /// /// 领导角色 /// public interface ILead { void AssignTask(); void CreateSubTask(); } /// /// 程序员:执行任务 /// public class Programmer : IProgrammer { public void WorkOnTask() { //code to implement to work on the Task. } } /// /// 管理者:可以创建任务、分配任务 /// public class Manager : ILead { public void AssignTask() { //Code to assign a Task } public void CreateSubTask() { //Code to create a sub taks from a task. } } /// /// 团队领域:可以创建任务、分配任务、执行执行 /// 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来记录异常。
////// 数据库日志类 /// 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); } } }
(2) 解决方案,使用依赖反转原则,这里演示没有用依赖注入。
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); } } }
参考文献
SOLID原则简介