编码最佳实践——单一职责原则(代码片段)

songwenjie songwenjie     2022-12-28     229

关键词:

SOLID是一组最佳编码实践的首字母缩写

  • S 单一职责原则
  • O 开放与封闭原则
  • L Liskov(里式)替换原则
  • I 接口分离原则
  • D 依赖注入原则

同时应用这些最佳实践,可以提升代码适应变更的能力。但是凡事要有度,过度使用虽然可以让代码有很高的自适应能力,但是会导致层次粒度过小而难以理解或使用,还会影响代码的可读性。

技术分享图片

单一职责原则

单一职责原则(Single Responsibility principle)要求开发人员编写的代码有且只有一个变更理由。如果一个类有多个变更理由,那么它就具有多个职责。这个时候就要进行重构,将多职责类拆解为多个单职责类。通过委托和抽象,包含多个变更理由的类应该把一个或多个职责委托给其他的单职责类

之前看过一篇文章,讲为什么面向对象比面向过程更能适应业务变化?从其中也可以看出单一职责原则带来的好处,职责明确,只需要修改局部,不会对外部造成影响,影响可以控制在足以掌控的范围内。

对象将需求用类一个个隔开,就像用储物箱把东西一个个封装起来一样,需求变了,分几种情况,最严重的是大变,那么每个储物箱都要打开改,这种方法就不见得有好处;但是这种情况发生概率比较小,大部分需求变化都是局限在一两个储物箱中,那么我们只要打开这两个储物箱修改就可以,不会影响其他储物柜了。

而面向过程是把所有东西都放在一个大储物箱中,修改某个部分以后,会引起其他部分不稳定,一个BUG修复,引发新的无数BUG,最后程序员陷入焦头烂额。

我们一段代码为例,通过重构的过程,体会一下单一职责原则的好处。

面向过程编码

public class TradeRecord

    public int TradeAmount  get; set; 

    public decimal TradePrice  get; set; 
public class TradeProcessor

    public void ProcessTrades(Stream stream)
    
        var lines = new List<string>();

        using (var reader = new StreamReader(stream))
        
            string line;
            while((line =reader.ReadLine()) != null)
            
                lines.Add(line);
            
        

        var trades = new List<TradeRecord>();
        var lineCount = 1;
        foreach (var line in lines)
            
                var fields = line.Split(new char[]  ',' );

                if(fields.Length != 3 )
                
                    Console.WriteLine("WARN: Line 0 malformed. Only 1 fields found",lineCount, fields.Length);
                

                int tradeAmount;
                if (!int.TryParse(fields[0], out tradeAmount))
                
                    Console.WriteLine("WARN: Trade amount on line 0 not a valid integer :1",lineCount, fields[0]);
                

                decimal tradePrice;
                if (!decimal.TryParse(fields[1], out tradePrice))
                
                    Console.WriteLine("WARN: Trade Price on line 0 not a valid decimal :1", lineCount, fields[1]);
                

                var tradeRecord = new TradeRecord
                
                    TradeAmount = tradeAmount,
                    TradePrice = tradePrice
                ;
                trades.Add(tradeRecord);
                lineCount++;
            
        
        using (var connection = new SqlConnection("DataSource=(local);Initial Catalog=TradeDataBase;Integrated Security = True;"))
                
                    connection.Open();
                    using (var transaction = connection.BeginTransaction())
                    
                        foreach (var trade in trades)
                        
                            var command = connection.CreateCommand();
                            command.Transaction = transaction;
                            command.CommandType = System.Data.CommandType.StoredProcedure;
                            command.CommandText = "insert_trade";

                            command.Parameters.AddWithValue("@tradeamount", trade.TradeAmount);
                            command.Parameters.AddWithValue("@tradeprice", trade.TradePrice);
                        
                        transaction.Commit();
                    
                    connection.Close();
                

        Console.WriteLine("INFO: 0 trades processed",trades.Count);
    

上面的代码不仅仅是一个类拥有太多的职责,也是一个单一方法拥有太多的职责。仔细分析一下代码,原始的ProcessTrades方法代码可以分为三个部分:从流中读取交易数据、将字符串数据转换为TradeRecord实例、将交易数据持久化到永久存储。

单一职责原则可以表现在类和方法层面上。从方法的层面上,一个方法只能做一件事情;从类的层面上,一个类只能有一个职责。否则,就要对类和方法进行拆分重构。对于方法的拆分重构,目标是清晰度,能提升代码的可读性,但是不能提升代码的自适应能力。要提升代码的自适应能力,就要做抽象,将每个职责划分到不同的类中。

重构清晰度

上面我们分析过ProcessTrades方法代码可以分为三个部分,我们可以将每个部分提取为一个方法,将工作委托给这些方法,这样ProcessTrades方法就变成了:

public void ProcessTrade(Stream stream)

    var lines = ReadTradeData(stream);
    var trades = ParseTrades(lines);
    StoreTrades(trades);

提取的方法实现分别为:

/// <summary>
/// 从流中读取交易数据
/// </summary>
/// <param name="stream"></param>
/// <returns></returns>
private IEnumerable<string> ReadTradeData(Stream stream)

    var tradeData = new List<string>();
    using (var reader = new StreamReader(stream))
    
        string line;
        while ((line = reader.ReadLine()) != null)
        
            tradeData.Add(line);
        
    
    return tradeData;
/// <summary>
/// 将字符串数据装换位TradeRecord实例
/// </summary>
/// <param name="tradeData"></param>
/// <returns></returns>
private IEnumerable<TradeRecord> ParseTrades(IEnumerable<string> tradeData)

    var trades = new List<TradeRecord>();
    var lineCount = 1;
    foreach (var line in tradeData)
    
        var fields = line.Split(new char[]  ',' );

        if(!ValidateTradeData(fields,lineCount))
        
            continue;
        

        var tradeRecord = MapTradeDataToTradeRecord(fields);
        trades.Add(tradeRecord);

        lineCount++;
    
    return trades;
/// <summary>
/// 交易数据持久化
/// </summary>
/// <param name="trades"></param>
private void StoreTrades(IEnumerable<TradeRecord> trades)

    using (var connection = new SqlConnection("DataSource=(local);Initial Catalog=TradeDataBase;Integrated Security = True;"))
    
        connection.Open();
        using (var transaction = connection.BeginTransaction())
        
            foreach (var trade in trades)
            
                var command = connection.CreateCommand();
                command.Transaction = transaction;
                command.CommandType = System.Data.CommandType.StoredProcedure;
                command.CommandText = "insert_trade";

                command.Parameters.AddWithValue("@tradeamount", trade.TradeAmount);
                command.Parameters.AddWithValue("@tradeprice", trade.TradePrice);
            
            transaction.Commit();
        
        connection.Close();
    

    Console.WriteLine("INFO: 0 trades processed", trades.Count());

其中ParseTrades方法的实现比较特殊,负责的是将字符串数据转换为TradeRecord实例,包含数据的验证和实例的创建。同理,将这些工作委托给了ValidateTradeData方法和MapTradeDataToTradeRecord方法。ValidateTradeData方法负责数据的验证,只有合法的数据格式才能继续组装为TradeRecord实例,不合法的数据将会被记录在日志中。ValidateTradeData方法将记录日志的工作也委托给了LogMessage方法,具体实现如下:

/// <summary>
/// 验证交易数据
/// </summary>
/// <param name="fields"></param>
/// <param name="currentLine"></param>
/// <returns></returns>
private bool ValidateTradeData(string[] fields,int currentLine)

    if (fields.Length != 3)
    
        LogMessage("WARN: Line 0 malformed. Only 1 fields found", currentLine, fields.Length);
        return false;
    

    int tradeAmount;
    if (!int.TryParse(fields[0], out tradeAmount))
    
        LogMessage("WARN: Trade amount on line 0 not a valid integer :1", currentLine, fields[0]);
        return false;
    

    decimal tradePrice;
    if (!decimal.TryParse(fields[1], out tradePrice))
    
        LogMessage("WARN: Trade Price on line 0 not a valid decimal :1", currentLine, fields[1]);
        return false;
    
    return true;
/// <summary>
/// 组装TradeRecord实例
/// </summary>
/// <param name="fields"></param>
/// <returns></returns>
private TradeRecord MapTradeDataToTradeRecord(string[] fields)

    int tradeAmount = int.Parse(fields[0]);
    decimal tradePrice = decimal.Parse(fields[1]);
    var tradeRecord = new TradeRecord
    
        TradeAmount = tradeAmount,
        TradePrice = tradePrice
    ;
    return tradeRecord;
/// <summary>
/// 记录日志
/// </summary>
/// <param name="message"></param>
/// <param name="args"></param>
private void LogMessage(string message,params object[] args)

    Console.WriteLine(message,args);

重构清晰度之后,代码的可读性提高了,但是自适应能力并没有提升多少。方法做到了只做一件事情,但是类的职责并不单一。还所以,要继续重构抽象。

重构抽象

重构TradeProcessor抽象的第一步就是设计一个或一组接口来执行三个最高级别的任务:读取数据、处理数据和存储数据。

技术分享图片

public class TradeProcessor

    private readonly ITradeDataProvider tradeDataProvider;
    private readonly ITradeParser tradeParser;
    private readonly ITradeStorage tradeStorage;

    public TradeProcessor(ITradeDataProvider tradeDataProvider,
        ITradeParser tradeParser,
        ITradeStorage tradeStorage)
    
        this.tradeDataProvider = tradeDataProvider;
        this.tradeParser = tradeParser;
        this.tradeStorage = tradeStorage;
    

    public void ProcessTrades()
    
        var tradeData = tradeDataProvider.GetTradeData();
        var trades = tradeParser.Parse(tradeData);
        tradeStorage.Persist(trades);
    

作为客户端的TradeProcessor类现在不清楚,当然也不应该清楚StreamTradeDataProvider类的实现细节,只能通过ITradeDataProvider接口的GetTradeData方法来获取数据。TradeProcesso将不再包含任何交易流程处理的细节实现,取而代之的是整个流程的蓝图

对于ITradeparser接口的实现Simpleradeparser类,还可以继续提取更多的抽象,重构之后的UML图如下。ITradeMapper负责数据格式的映射转换,ITradeValidator负责数据的验证。

技术分享图片

public class TradeParser : ITradeParser

    private readonly ITradeValidator tradeValidator;
    private readonly ITradeMapper tradeMapper;
    public TradeParser(ITradeValidator tradeValidator, ITradeMapper tradeMapper)
    
        this.tradeValidator = tradeValidator;
        this.tradeMapper = tradeMapper;
    

    public IEnumerable<TradeRecord> Parse(IEnumerable<string> tradeData)
    
        var trades = new List<TradeRecord>();
        var lineCount = 1;
        foreach (var line in tradeData)
        
            var fields = line.Split(new char[]  ',' );

            if (!tradeValidator.Validate(fields, lineCount))
            
                continue;
            

            var tradeRecord = tradeMapper.MapTradeDataToTradeRecord(fields);
            trades.Add(tradeRecord);

            lineCount++;
        
        return trades;
    

类似于上面将职责抽象为接口(及其实现)的过程是递归的。在检视每个类时,你需要判断它是否具备多重职责。如果是,提取抽象直到该类只具备单个职责。

重构抽象完成后的整个UML图如下:

技术分享图片

需要注意的是,记录日志等一般需要依赖第三方程序集。对于第三方引用,应该通过包装的方式转换为第一方引用。这样对于第三方的依赖可以被有效控制,在可预见的将来,替换第三方引用将会变得十分容易(只需要替换一处),否则项目中可能到处是对第三方引用的直接依赖。包装一般是通过适配器模式,此处使用的是对象适配器模式。

技术分享图片

注意,示例中的代码实现对于依赖的抽象(接口),都是通过构造函数传入的,也就是说对象依赖的具体实现在对象创建时就已经确定了。有两种选择,一是客户端传入手动创建的依赖对象(穷人版的依赖注入),二是使用IOC容器(依赖注入)。

需求变更

重构抽象后的新版本能在无需改变任何现有类的情况下实现以下的需求增强功能。我们可以模拟需求变更来体验以下代码的自适应能力。

  • 当输入数据的验证规则变化时

    修改ITradeValidator接口的实现以反映最新的规则。

  • 当更改日志记录方式时,由窗口打印方式改为文件记录方式

    创建一个文件记录的FileLogger类实现文件记录日志的功能,替换ILogger的具体实现。

  • 当数据库发生了变化,例如使用文档数据库替换关系型数据库

    创建MongoTradeStorage类使用MongoDB存储交易数据,替换ITradeStorage的具体实现。

最后

我们发现,符合单一职责原则的代码会由更多的小规模但目标更明确的类组成,然后通过接口抽象以及在运行时将无关功能的责任委托给相应的接口来达成目标的。更多的小规模但目标更明确的类通过自由组合的形式配合完成任务,每个类都可以看做是一个小零件,而接口就是生产这些零件的模具。当这个零件不再适合完成此任务时,就可以考虑替换掉这个零件,前提是替换前后的零件都是通过同一个模具生产出来的。

聪明的人从来不会把鸡蛋放到同一个篮子里,但是更聪明的人会考虑把这些篮子放到不同的车上。我们应该做更聪明的人,而不是每次系统出现问题时,在意大利面条式的代码里一遍又一遍的DeBug。

参考

《C#敏捷开发实践》

作者:CoderFocus

微信公众号:
技术分享图片

声明:本文为博主学习感悟总结,水平有限,如果不当,欢迎指正。如果您认为还不错,不妨点击一下下方的推荐按钮,谢谢支持。转载与引用请注明作者及出处。

设计模式——软件api设计最佳实践指南小结(代码片段)

文章大纲引言一、面向对象的设计原则1、开闭原则(★★★★★)2、依赖倒转原则(★★★★★)3、里氏替换原则(★★★★)4、合成复用原则(★★★★)5、单一职责原则(★★★★)... 查看详情

设计模式软件设计七大原则(单一职责原则|代码示例)(代码片段)

文章目录一、单一职责原则简介二、单一职责原则代码示例(反面示例)1、不遵循单一职责原则的类2、测试类三、单一职责原则代码示例(正面示例|类的单一职责)1、用翅膀飞的鸟2、用脚走的鸟3、测试类四、单一职责原则代码示... 查看详情

设计模式六大原则:单一职责原则(代码片段)

单一职责原则定义是:不要存在多于一个导致类变更的原因。通俗地说,即一个类只负责一项职责。单一职责原则针对的问题有一个类T负责两个不同的职责:职责P1和职责P2。当因为职责P1的需求发生改变而需要修改类T的时候,... 查看详情

面向对象编程的六大原则--单一职责原则(代码片段)

什么是单一职责  单一职责原则(Singleresponsibilityprinciple,简称SRP),顾名思义,是指一个类或者一个模块应该有且只有一个职责,它是面向对象编程六大原则之一。单一职责的粒度  单一职责的粒度,可以是某个方法、某... 查看详情

设计原则之单一职责原则(代码片段)

文章目录单一职责原则(SRP)如何理解单一职责原则(SRP)?如何判断类的职责是否足够单一?类的职责是否设计得越单一越好?本文是极客时间里王争专栏《设计模式之美》的学习笔记,你可以... 查看详情

设计原则之单一职责原则(代码片段)

文章目录单一职责原则(SRP)如何理解单一职责原则(SRP)?如何判断类的职责是否足够单一?类的职责是否设计得越单一越好?本文是极客时间里王争专栏《设计模式之美》的学习笔记,你可以... 查看详情

java设计模式1,单一职责原则(代码片段)

目录一、单一职责原则定义二、模拟场景三、违背原则方案1、配置类2、逻辑代码3、测试类四、单一职责原则改善代码1、定义接口2、实现类,普通用户3、实现类,专属用户4、VIP用户一、单一职责原则定义单一职责原则&... 查看详情

软件架构设计原则-单一职责原则(代码片段)

前言单一职责是只尽量是一个类或一个方法只是负责一个职责,从而实现降低耦合的目的,接下来我们就用实际案例来解释类的单一职责首先我们有一个课程类如下publicclassCoursepublicvoidstudy(StringcourseName)if("直播课".e... 查看详情

「设计模式」六大原则之一:单一职责小结(代码片段)

文章目录1.单一职责原则定义2.如何理解单一职责原则(SRP)?3.如何判断类的职责是否足够单一?4.类的职责是否设计得越单一越好?5.应用体现6.应用示例18应用示例2(结合组合模式)9.小结「设计模... 查看详情

「设计模式」六大原则之一:单一职责小结(代码片段)

文章目录1.单一职责原则定义2.如何理解单一职责原则(SRP)?3.如何判断类的职责是否足够单一?4.类的职责是否设计得越单一越好?5.应用体现6.应用示例18应用示例2(结合组合模式)9.小结「设计模... 查看详情

编码最佳实践——依赖注入原则(代码片段)

我们在这个系列的前四篇文章中分别介绍了SOLID原则中的前四个原则,今天来介绍最后一个原则——依赖注入原则。依赖注入(DI)是一个很简单的概念,实现起来也很简单。但是简单却掩盖不了它的重要性,如果没有依赖注入... 查看详情

设计模式六大原则:单一职责原则(代码片段)

...致原本运行正常的职责P2功能发生故障。解决方案:遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职 查看详情

单一职责原则(代码片段)

...视/违背;也是前辈总结,也是为了站在前辈的肩膀上。单一职责原则:一个类只做一件事儿,一个方法 查看详情

单一职责原则(代码片段)

单一职责原则定义单一职责原则(SRP),就一个类而言,应该仅有一个引起它变化的原因。简单来说,就是让一个类只干一部分事情,这样可以降低耦合性,提高复用性,提高可读性,降低由变... 查看详情

2单一职责原则srp(代码片段)

一、什么是单一职责原则  单一职责原则(SingleResponsibilityPrinciple):就一个类而言,应该仅有一个引起它变化的原因。 二、多功能的山寨手机  山寨手机的功能:拍照、摄像、手机游戏、网络摄像头、GPS、炒股等等。 ... 查看详情

23种设计模式-单一职责原则(代码片段)

...模式为什么这样设计的依据设计模式常用的七大原则有:单一职责原则接口隔离原则依赖倒转(倒置)原则里氏替换原则开闭原则迪米特法则合成复用原则2.3单一职责原则对类来说的,即一个类应该只负责一项职责。如类A负责两... 查看详情

设计模式之美--单一职责原则(代码片段)

什么是单一职责原则?单一职责原则的英文是SingleResponsibilityPrinciple,简称SRP。其原始英文描述是:Aclassormoduleshouldhaveasingleresponsibility.一个类或者模块应当只负责完成一个功能(或职责)。举个栗子:在社交产品中,需设计一个... 查看详情

设计原则之单一职责原则(代码片段)

文章目录单一职责原则(SRP)如何理解单一职责原则(SRP)?如何判断类的职责是否足够单一?类的职责是否设计得越单一越好?本文是极客时间里王争专栏《设计模式之美》的学习笔记,你可以... 查看详情