asp.net core系列 65 正反案例介绍SOLID原则

Posted lonelyxmas

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了asp.net core系列 65 正反案例介绍SOLID原则相关的知识,希望对你有一定的参考价值。

原文: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原则简介

 

以上是关于asp.net core系列 65 正反案例介绍SOLID原则的主要内容,如果未能解决你的问题,请参考以下文章

2021-06-24 .NET高级班 65-ASP.NET Core EFCore数据库(EFCore调优技巧)

ASP.NET Core Web 应用程序系列- 在ASP.NET Core中使用AutoMapper进行实体映射

asp.net core系列 48 Identity 身份模型自定义

ASP.NET Core Identity 系列之四

Asp.net core 简单介绍

ASP.NET Core Identity 系列之五