编码最佳实践——单一职责原则

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

微信公众号:

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

原文地址:https://www.cnblogs.com/songwenjie/p/9555440.html

时间: 2024-10-12 23:37:02

编码最佳实践——单一职责原则的相关文章

编码最佳实践——依赖注入原则

我们在这个系列的前四篇文章中分别介绍了SOLID原则中的前四个原则,今天来介绍最后一个原则--依赖注入原则.依赖注入(DI)是一个很简单的概念,实现起来也很简单.但是简单却掩盖不了它的重要性,如果没有依赖注入,前面的介绍的SOLID技术原则都不可能实际应用. 控制反转(IoC) 人们在谈论依赖注入的时候,经常也会谈到另一个概念--控制反转(IoC).按照大内老A的解释:"IoC主要体现了这样一种设计思想:通过将一组通用流程的控制权从应用转移到框架中以实现对流程的复用,并按照"好莱坞法则

敏捷开发:原则,模式与实践——第8章 单一职责原则SRP

鲍勃大叔说: 单一职责原则(SRP):就一个类而言,应该仅有一个引起它变化的原因. 我最开始理解成只能有一个原因去改变,跟我以前的认知有问题,从我开始学OOP以来,我觉得一个类就是一个事物的抽象,比如书,BOOK类,如果按照我理解的意思,book类就有很多可以改变它的原因,例如翻书或者买书,我感觉SRP说的是函数的职责,不是类的职责,于是我就去找了一个同学讨论,然后我们讨论了一会,得出的结论是这样的(以保龄球为例): SRP指的是只改变保龄球相关的内容算一件事,如果我用一个比赛类去代替保龄球的话

【设计模式之禅】第1章 单一职责原则

1.1 我是"牛"类,我可以担任多职吗 SRP 单一职责原则的英文名称是Single Responsibility Principle,简称是SRP. RBAC模型(Role-Based Access Control)基于角色的访问控制        通过分配和取消角色来完成用户权限的授予和取消,使动作主体(用户)与资源的行为(权限)分离 单一职责原则的定义是:应该有且仅有一个原因引起类的变更. 1.2 绝杀技,打破你的传统思维 SRP的原话解释是:        There shou

敏捷软件开发 – SRP 单一职责原则

SRP:单一职责原则  一个类应该只有一个发生变化的原因. 为何把两个职责分离到单独的类中很重要呢?因为每一个职责都有变化的一个轴线.当需求变化时,该变化会反映为类的职责的变化.如果一个类承担了多于一个的职责,那么引起它变化的原因就会有多个. 如果一个类承担的职责过多,就等于把这些职责耦合在了一起.一个职责发生变化可能会削弱或抑制这个类完成其他职责的能力.这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到意想不到的破坏. 有两个不同的应用程序使用Rectangle类.一个应用程序是有关计算几何

单一职责原则

什么是单一职责原则 什么是单一职责原则? 单一职责原则的英文名称是Single Responsibility Principle,简称SRP.SRP的原话解释是:There should never be more than one reason for a class to change. 也就是说一个类,只有一个引起它变化的原因.应该只有一个职责.每一个职责都是变化的一个轴线,如果一个类有一个以上的职责,这些职责就耦合在了一起. 这会导致脆弱的设计.当一个职责发生变化时,可能会影响其它的职责

设计模式六大原则(1):单一职责原则

单一职责原则 定义: 不要存在多于一个导致类变更的原因.通俗的说,即一个类只负责一项职责. 问题由来:类T负责两个不同的职责:职责P1,职责P2.当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障. 解决方案: 遵循单一职责原则.分别建立两个类T1.T2,使T1完成职责P1功能,T2完成职责P2功能.这样,当修改类T1时,不会使职责P2发生故障风险:同理,当修改T2时,也不会使职责P1发生故障风险. 单一职责原则是最简单的面向对象设计原则,它用于控制类的粒

[OOD] 为什么单一职责原则(SRP)是最难运用的

单一职责原则(SRP)已经几乎是每一个程序员都知道的设计原则.最早由Robert C. Martin在<<敏捷软件开发 - 原则.模式与实践>>中正式提出.书中作者在结论中提到:  SRP是所有设计原则最简单的,但也是最难运用的.(中文翻译有之一,略去了) 现实工作中,关于一个类是否符合SRP,或者是否有必要符合SRP的讨论是经常发生的.争论的关键在于职责的定义,但我理解SRP真正的核心是关注于变化.这并不是我的新见解,全是来自Martin大叔的解释: 首先职责的定义是: 引起变化

10个精妙的Java编码最佳实践

这是一个比Josh Bloch的Effective Java规则更精妙的10条Java编码实践的列表.和Josh Bloch的列表容易学习并且关注日常情况相比,这个列表将包含涉及API/SPI设计中不常见的情况,可能有很大影响. 我在编写和维护jOOQ(Java中内部DSL建模的SQL)时遇到过这些.作为一个内部DSL,jOOQ最大限度的挑战了Java的编译器和泛型,把泛型,可变参数和重载结合在一起,Josh Bloch可能不会推荐的这种太宽泛的API. 让我与你分享10个微妙的Java编码最佳

php设计模式的六大原则(一):单一职责原则

<?php //单一职责原则 class Modem{ public function dial(){ return "dialing...."; } public function hangup(){ return "hangup!!!"; } public function send(){ return "send info!!!"; } public function receive(){ return "received!