[译] 从不同的抽象层次理解单一职责原则

在二十世纪初的某个地方,广为人知的 Uncle Bob —— 罗伯特·马丁(Robert C. Martin)第一次提出了面向对象设计的五大原则 —— SOLID 原则。SOLID 是这五大原则单词首字母的缩略词,其中的每个字母代表着不同的原则:

  • S - Single Responsibility Principle - 单一职责原则

  • O - Open Close Principle - 开闭原则

  • L - Liskov Principle of Substitution - 里式替换原则

  • I - Interface Segregation Principle - 接口隔离原则

  • D - Dependency Inversion Principle - 依赖倒置原则

这些原则是面向对象程序设计的骨架,并且是制定高品质和可维护代码的关键。在五大原则的第一条 —— 单一职责原则中,Uncle Bob 将 《关于将系统分解为模块的标准》《关于科学思想的作用》 两篇论文的观点结合起来,得出了关于 SRP (单一职责原则) 的定义:

The Single Responsibility Principle (SRP) states that each software
module should have one and only one reason to change.
​单一职责原则(SRP)​指出,每一个软件模块都应当只做一件事(只对某一个因素敏感)。

在这段定义中,"Reason to change" 并不好理解,我更愿意把它理解成 responsibility (职责)。这意味着软件的每一个模块或者类都应当只提供一项职能。现在看来,这个观点十分简单合理,然而多年以来,业界却很难按照这一原则实践。造成这一现象的原因可能有很多:比如有大量的遗留代码,比如缺乏改变的动力,又比如缺少相关领域的知识,更有甚者认为这一原则背离了自然趋势。软件设计的关键,其实在于将相互耦合的各种职能分离,并找到一种方式让这些互相独立的模块共存于一个系统中。

SRP是一个十分抽象并且适用性很强的概念,只要你仔细观察就会发现,做为一名开发者,我们已经将其用于软件开发的各个方面。因此我想更深入的了解这一原则以及它的各种变化,并探索这一概念的各种应用。

面向对象编程

实践中,这一原则会如何应用呢?让我们看下边这个例子:

using (var sqlConnection = new sqlConnection(connectionString))
{
  sqlConnection.open();
  try
  {
    using (var readCommand = new sqlCommand("select * from Entity",sqlConnection))
    {
      var reader = readCommand.ExecuteReader();
      while (reader.Read())
      {
        currenValue = reader.GetInt32(0);
        type = reader.GetInt32(0);
      }
      reader.Close();
    }
    using (var updateCommand = new sqlCommand(String.Format("update Entity set Data = {0} where Data = {1}",newValue,currenValue),sqlConnection))
    {
      updateCommand.ExecuteNonQuery();
    }
 
    Console.WriteLine("Data successfuly modified!");
    Console.ReadLine();
  }
  catch(Exception e)
  {
    Console.WriteLine("Failed to modify data");
  }
}

上述代码的逻辑可以分为三个步骤:

  • 连接数据库

  • 从Entity表中读取数据并缓存第一个Entity的值

  • 将新值写入到第一个Entity

当我们不阅读所有的代码,我们真的能明白这段代码在做什么吗?并不能。这是一段很常见的代码,所有的职责都扔到一个函数里执行:处理sql连接,获取数据,修改数据,这些功能都是这一大段代码的一部分。

如果代码写成这样呢:

using (var sqlDataHandler = new sqlDataHandler())
{
  var entity = sqlDataHandler.ReadEntity();
  sqlDataHandler.UpdatedatafieldInEntity(entity,modificationValue);
}
 
Console.WriteLine("Data successfuly modified!");
Console.ReadLine();

现在看上去,代码更精简了,它需要完成什么样的功能一目了然。我们将大量复杂的逻辑从这个函数移到了 sqlDataHandler 类,这个类的代码如下:

public class sqlDataHandler : Idisposable
{
    private string _connectionString;
    private sqlConnection _sqlConnection;
 
    public sqlDataHandler()
    {
        _connectionString = ConfigurationManager.AppSettings["connectionString"];
        _sqlConnection = new sqlConnection(_connectionString);
        _sqlConnection.open();
    }
 
    public Entity ReadEntity()
    {
        var entity = new Entity();
 
        try
        {
            using (var readCommand = new sqlCommand("select * from Entity",_sqlConnection))
            {
                var reader = readCommand.ExecuteReader();
                while (reader.Read())
                {
                    entity.CurrentValue = reader.GetInt32(0);
                    entity.Type = (EntityType)reader.GetInt32(1);
                }
                reader.Close();
            }
        }
        catch(Exception e)
        {
            Console.WriteLine("Failed to read the data!");
        }
 
        return entity;
      }
 
   public void UpdatedatafieldInEntity(Entity entity,int newValue)
   {
        var tovalue = entity.GetNewValueBasedOnType(newValue);
 
        try
        {
            using (var updateCommand = new sqlCommand(String.Format("update Entity set Data = {0} where Data = {1}",tovalue,entity.CurrentValue),_sqlConnection))
            {
                updateCommand.ExecuteNonQuery();
            }
        }
        catch (Exception e)
        {
            Console.WriteLine("Failed to modify data");
        }
    }
 
    public void dispose()
    {
        _sqlConnection.Close();
    }
}

我们把与数据库操作相关的代码移到了这个新类中,在原始代码中只留下了一些用来驱动整个工作流的代码。我们将职责分离出来,让我们的代码变得更易读,更容易维护,也让我们的代码更加灵活。

现在我们应该能更好的理解每个类应该只维护一个功能的原则了。你也许会觉得 SRP 和 ISP (接口隔离原则)两者有一些相似性。ISP 认为不应当要求使用者依赖他不需要的方法。在实践中,这意味着类不应当实现接口中不需要的方法。这就需要我们将臃肿的接口切分的更小更合理,将相似的方法抽象到一个接口中(译者注:关于 ISP 可以通过这篇文章了解 —— http://blog.csdn.net/zhengzhb...)。这个定义是不是听起来跟 SRP 的定义很像?有的人认为,ISP 就是 SRP 应用于接口的抽象概念。

让我们想的更多一些。定义一个对象,通常要为其赋予多个数据和不同行为。如果不断的采用 SRP 原则去尽可能的分离行为,让我们看看还留下些什么 —— 函数以及该函数所依赖的数据。这不就是闭包么?(译者注:关于闭包的定义

函数式编程

闭包是函数式编程的基础。他们其实就是拥有自己的运行环境的 function 。它具有稍后执行的特性,它还能访问创建它时所在的环境。为了证明这一点,可以看看下面的C#代码

using System;
 
namespace ClosureExample
{
    class Program
    {
        static void Main(string[] args)
        {
            Action counterIncrementAction = CounterIncrementAction();
            counterIncrementAction();
            counterIncrementAction();
            Console.ReadLine();
 
        }
 
        static Action CounterIncrementAction()
        {
            int counter = 0;
            return delegate
            {
                counter++;
                Console.WriteLine("Counter value is {0}",counter);
            };
        }
    }
}

输出结果如下:

Counter value is 1 
Counter value is 2

正如我们所见 counterIncrementAction 可以一直访问局部变量 counter

CounterIncrementAction函数体看上去与对象很类似。那么对象和闭包之间真正的区别又是什么呢?为了回答这个问题,我想分享一个有趣的段子,这个段子能够在很多讲函数式编程的书和文章中找到:

大师 Qc Na 和他的学生 Anton 一同在路上走着。Anton 说:“大师,我听说 Object 是一样很棒的设计,这是真的么?”Qc
Na 怜悯的看着他的学生,并回复到:“傻学生,对象不过是能力不足的人使用的闭包罢了。”

Anton 离开大师回到他的房间,打算深入研究闭包。他仔细阅读了全部的“终极 Lambda
”等一系列的论文,以及一些相关的文章,并着手实现了一个具有封闭式对象系统的小型 Scheme
解释器。他觉得自己学到了很多东西,十分想要跟大师汇报他的进度。

在他与 Qc Na 大师的又一次步行中,Anton
试图引起大师的注意,就说:“大师,我一直在研究这件事,现在终于明白,对象真的是能力不足的人使用的类似闭包的东西。” 结果 Qc Na
用棍子敲打 Anton 并说:“你学什么啦?闭包是能力不足的使用的类似对象的东西。”这一刻,Anton 终于想明白了。

这条段子想要揭示的道理是闭包和对象其实是一件相同的事情:组合数据和行为。他们只是同一件事的两种表现形式。

我试图在这里展示一个像 SRP 一样直观的概念,为我们的软件开发开辟了一条新的道路。闭包使用起来十分方便,它也十分流行,尤其是在 web 开发中(JavaScript 天生就有闭包这个概念,C# 则通过 lambdas 和 匿名函数 实现)。

让我们再来换个角度。到目前为止,我们试图将它应用于微观。接下来我们用 SRP 观察一下宏观的东西。我们在各种服务上如何应用 SRP ?如果我们将一个大型服务切分成数个微小的服务,又会怎样呢?于是,我们得到了当前最大的趋势之一 —— 微服务。

微服务

如果你去看任何一个关于微服务的讨论,很可能会见到类似于下面这样的图像:

总的来说,所谓微服务就是将巨大而混乱的大型服务拆分成一个个更小型的服务。这些小型服务专注于解决某个专门的业务需求,并且他们都是能够自治的独立个体。微服务几乎采取了与我们之前处理面向对象中的问题一样的解决方案,通过切分服务的方式,增加了整体的灵活性。我们现在能够独立部署每一个独立的业务,或者采用不同的技术实现,使用最恰当的技术解决某一部分的问题。这样的系统当然十分灵活,容易组装。

我们再次看到了 SRP 在不同层次上的抽象,最终得到了一个全新的东西。

结论

有时候,我看起来像个外星人,总是在试图让每一个计算机科学的突破都与 SRP 发生关系。

当然不是这样的, 但是分离职责的概念确实是有一些强大的东西,它对我们工作的不同领域都有影响。 快去找找,你可能会发现这个概念出现在其他我还没有找到的地方。你一定能够找到的。

原文地址:https://rubikscode.net/2017/0...

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


迭代器模式(Iterator)迭代器模式(Iterator)[Cursor]意图:提供一种方法顺序访问一个聚合对象中的每个元素,而又不想暴露该对象的内部表示。应用:STL标准库迭代器实现、Java集合类型迭代器等模式结构:心得:迭代器模式的目的是在不获知集合对象内部细节的同时能对集合元素进行遍历操作
高性能IO模型浅析服务器端编程经常需要构造高性能的IO模型,常见的IO模型有四种:(1)同步阻塞IO(BlockingIO):即传统的IO模型。(2)同步非阻塞IO(Non-blockingIO):默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。注意这里所说的N
策略模式(Strategy)策略模式(Strategy)[Policy]意图:定义一系列算法,把他们封装起来,并且使他们可以相互替换,使算法可以独立于使用它的客户而变化。应用:排序的比较方法、封装针对类的不同的算法、消除条件判断、寄存器分配算法等。模式结构:心得:对对象(Context)的处理操作可
访问者模式(Visitor)访问者模式(Visitor)意图:表示一个作用于某对象结构中的各元素的操作,它使你在不改变各元素的类的前提下定义作用于这些元素的新操作。应用:作用于编译器语法树的语义分析算法。模式结构:心得:访问者模式是要解决对对象添加新的操作和功能时候,如何尽可能不修改对象的类的一种方
命令模式(Command)命令模式(Command)[Action/Transaction]意图:将一个请求封装为一个对象,从而可用不同的请求对客户参数化。对请求排队或记录请求日志,以及支持可撤消的操作。应用:用户操作日志、撤销恢复操作。模式结构:心得:命令对象的抽象接口(Command)提供的两个
生成器模式(Builder)生成器模式(Builder)意图:将一个对象的构建和它的表示分离,使得同样的构建过程可以创建不同的表示。 应用:编译器词法分析器指导生成抽象语法树、构造迷宫等。模式结构:心得:和工厂模式不同的是,Builder模式需要详细的指导产品的生产。指导者(Director)使用C
设计模式学习心得《设计模式:可复用面向对象软件的基础》一书以更贴近读者思维的角度描述了GOF的23个设计模式。按照书中介绍的每个设计模式的内容,结合网上搜集的资料,我将对设计模式的学习心得总结出来。网络上关于设计模式的资料和文章汗牛充栋,有些文章对设计模式介绍生动形象。但是我相信“一千个读者,一千个
工厂方法模式(Factory Method)工厂方法模式(Factory Method)[Virtual Constructor]意图:定义一个用于创建对象的接口,让子类决定实例化哪一个类,使一个类的实力化延迟到子类。应用:多文档应用管理不同类型的文档。模式结构:心得:面对同一继承体系(Produc
单例模式(Singleton)单例模式(Singleton)意图:保证一个类只有一个实例,并提供一个访问它的全局访问点。应用:Session或者控件的唯一示例等。模式结构:心得:单例模式应该是设计模式中最简单的结构了,它的目的很简单,就是保证自身的实例只有一份。实现这种目的的方式有很多,在Java中
装饰者模式(Decorator)装饰者模式(Decorator)[Wrapper]意图:动态的给一个对象添加一些额外的职责,就增加功能来说,比生成子类更为灵活。应用:给GUI组件添加功能等。模式结构:心得:装饰器(Decorator)和被装饰的对象(ConcreteComponent)拥有统一的接口
抽象工厂模式(Abstract Factory)抽象工厂模式(Abstract Factory)[Kit]意图:提供一个创建一系列相关或相互依赖对象的接口,而无须指定他们具体的类。应用:用户界面工具包。模式结构:心得:工厂方法把生产产品的方式封装起来了,但是一个工厂只能生产一类对象,当一个工厂需要生
桥接模式(Bridge)桥接模式(Bridge)[Handle/Body]意图:将抽象部分与它的实现部分分离,使他们都可以独立的变化。应用:不同系统平台的Windows界面。模式结构:心得:用户所见类体系结构(Window派生)提供了一系列用户的高层操作的接口,但是这些接口的实现是基于具体的底层实现
适配器模式(Adapter)适配器模式(Adapter)[Wrapper]意图:将类的一个接口转换成用户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的类可以一起工作。应用:将图形类接口适配到用户界面组件类中。模式结构:心得:适配器模式一般应用在具有相似接口可复用的条件下。目标接口(Targ
组合模式(Composition)组合模式(Composition)意图:将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。应用:组合图形、文件目录、GUI容器等。模式结构:心得: 用户(Client)通过抽象类(Component)提供的公用接口统一
原型模式(Prototype)原型模式(Prototype)意图:用原型实例制定创建对象的种类,并且通过拷贝这些原型创建新的对象。应用:Java/C#中的Clonable和IClonable接口等。模式结构:心得:原型模式本质上就是对象的拷贝,使用对象拷贝代替对象创建的原因有很多。比如对象的初始化构
什么是设计模式一套被反复使用、多数人知晓的、经过分类编目的、代码 设计经验 的总结;使用设计模式是为了 可重用 代码、让代码 更容易 被他人理解、保证代码 可靠性;设计模式使代码编制  真正工程化;设计模式使软件工程的 基石脉络, 如同大厦的结构一样;并不直接用来完成代码的编写,而是 描述 在各种不同情况下,要怎么解决问题的一种方案;能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免引
单一职责原则定义(Single Responsibility Principle,SRP)一个对象应该只包含 单一的职责,并且该职责被完整地封装在一个类中。Every  Object should have  a single responsibility, and that responsibility should be entirely encapsulated by t
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强烈推荐。原文截图*************************************************************************************************************************原文文本************
适配器模式将一个类的接口转换成客户期望的另一个接口,使得原本接口不兼容的类可以相互合作。
策略模式定义了一系列算法族,并封装在类中,它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。