读懂 SOLID 的「单一职责」原则

这是理解 SOLID原则中,关于 单一职责原则如何帮助咱们编写低耦合和高内聚的第二篇文章。

单一职责原则是什么

以前的第一篇文章阐述了依赖倒置原则(DIP)可以使咱们编写的代码变得低耦合,同时具备很好的可测试性,接下来咱们来简单了解下单一职责原则的基本概念:前端

Every module or class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class.

每个模块或者类所对应的职责,应对应系统若干功能中的某个单一部分,同时关于该职责的封装都应当经过这个类来完成。react

往简单来说:程序员

A class or module should have one, and only one, reason to be changed.

一个类或者模块应当用于单一的,而且惟一的原因被更改。编程

若是仅仅经过这两句话去理解, 一个类或者模块若是若是越简单(具备单一职责),那么这个类或者模块就越容易被更改是有一些困难的。为了便于咱们理解整个概念,咱们将分别从三个不一样的角度来分析这句话,这三个角度是:redux

  • Single: 单一
  • Responsibility: 职责
  • Change: 改变

什么是单一

Only one; not one of several.

惟一的,而不是多个中的某个。segmentfault

Synonyms: one, one only, sole, lone, solitary, isolated, by itself.设计模式

同义词:一,仅有的一个,惟一,独个,独自存在的,孤立的,仅本身。api

单一意味着某些工做是独立的。好比,在类中,类方法仅完成某家独立的事情,而不是两件,以下:架构

class UserComponent { 
  // 这是第一件事情,获取用户详情数据
  getUserInfo(id) {
    this.api.getUserInfo(id).then(saveToState)
  }

  // 这是第二件事情,渲染视图的逻辑
  render() {
    const { userInfo } = this.state;
    return <div>
      <ul>
        <li>Name: { userInfo.name }</li>
        <li>Surname: { userInfo.surname }</li>
        <li>Email: { userInfo.email }</li>
      </ul>
    </div>
  }
}

看了上面的代码,你可能很快就会联想到,这些代码基本存在于全部的React组件中。ide

确实,对于一些小型的项目或者演示型项目,这样编写代码不会产生太大的问题。可是若是在大型或者复杂度很高的项目中,仍然按照这样的风格,则是一件比较糟糕的事情,由于一个组件每每作了它本不该当作的事情(承担了过多的职责)。

这样会带来什么坏处呢?好比对于以上的api服务,在未来的某天你作出了一些修改,增长了一些额外的逻辑,那么为了使代码可以正常工做,你至少须要修改项目中的两个地方以适应这个修改,一处修改是在API服务中,而另外一处则在你的组件中。若是进一步思考的,咱们会发现,修改次数与在项目直接使用API服务的次数成正比,若是项目足够复杂,足够大,一处简单的逻辑修改,就须要作出一次贯穿整个系统的适配工做。

那么咱们若是避免这种状况的发生呢?很简单,咱们仅仅须要将关于用户详情数据的逻辑提高到调用层,在上面的例子中,咱们应当使用React.component.prop来接受用户详情数据。这样,UserComponent组件的工做再也不与如何获取用户详情数据的逻辑耦合,从而变得单一

对于鉴别什么是单一,什么不是单一,有不少不一样的方式。通常来讲,只须要牢记,让你的代码尽量的少的去了解它已经作的工做。(译者注:我理解意思应当是,应当尽量的让已有的类或者方法变得简单、轻量,不须要全部事情都亲自为之)

总之,不要让你的对象成为上帝对象

A God Object aka an Object that knows everything and does everything.

上帝对象,一个知道一切事情,完成一切事情的对象。

In object-oriented programming, a God object is an object that knows too much or does too much. The God object is an example of an anti-pattern.

在面向对象编程中,上帝对象指一个了解太情或者作太多事情的对象。上帝对象是反模式的一个典型。

什么是职责

职责指软件系统中,每个指派给特定方法、类、包和模块所完成的工做或者动做。

Too much responsibility leads to coupling.

太多的职责致使耦合。

耦合性表明一个系统中某个部分对系统中另外一个部分的了解程度。举个例子,若是一段客户端代码在调用class A的过程当中,必需要先了解有关class B的细节,那么咱们说AB耦合在了一块儿。一般来讲,这是一件糟糕的事情。由于它会使针对系统自己的变动复杂化,同时会在长期愈来愈糟。

为了使一个系统到达适当的耦合度,咱们须要在如下三个方面作出调整

  • 组件的内聚性
  • 如何测量每一个组件的预期任务
  • 组件如何专一于任务自己

低内聚性的组件在完成任务时,和它们自己的职责关联并不紧密。好比,咱们如今有一个User类,这个类中咱们保存了一些基本信息:

class User {
  public age;  
  public name;
  public slug;
  public email;
}

对于属性自己,若是对于每一个属性声明一些getter或者setter方法是没什么问题的。可是若是咱们加一些别的方法,好比:

class User {
  public age;  
  public name;
  public slug;
  public email;
  // 咱们为何要有如下这些方法?
  checkAge();
  validateEmail();
  slugifyName();
}

对于checkAgevalidateEmailslugifyName的职责,与Userclass自己关系并不紧密,所以就会这些方法就会使User的内聚性变低。

仔细思考的话,这些方法的职责和校验和格式化用户信息的关系更紧密,所以,它们应当从User中被抽离出来,放入到另外一个独立的UserFieldValidation类中,好比:

class User {
  public age;  
  public name;
  public slug;
  public email;
}

class UserFieldValidation {
  checkAge();
  validateEmail();
  slugifyName();
}

什么是变动

变动指对于已存在代码的修改或者改变。

那么问题来了,什么缘由迫使咱们须要对源码进行变动?从众多过时的软件系统的历史数据的研究来看,大致有三方面缘由促使咱们须要做出变动:

  • 增长新功能
  • 修复缺陷或者bug
  • 重构代码以适配未来做出的变动

作为一个程序员,咱们每天不都在作这三件事情吗?让咱们来用一个例子完整的看一下什么是变动,比方说咱们完成了一个组件,如今这个组件性能很是好,并且可读性也很是好,也许是你整个职业生涯中写的最好的一个组件了,因此咱们给它一个炫酷的名字叫做SuperDuper(译者注:这个名字的意思是超级大骗子

class SuperDuper {
  makeThingsFastAndEasy() {
    // Super readable and efficient code
  }
}

以后过了一段时间,在某一天,你的经理要求你增长一个新功能,好比说去调用别的class中的每一个函数,从而可使当前这个组件完成更多的工做。你决定将这个类以参数的形式传入构造方法,并在你的方法调用它。

这个需求很简单,只须要增长一行调用的代码便可,而后你作了如下变动(增长新功能)

class SuperDuper {
  constructor(notDuper: NotSoDuper) {
    this.notDuper = notDuper
  }
  makeThingsFastAndEasy() {
     // Super readable and efficient code
    this.notDuper.invokeSomeMethod()
  }
}

好了,以后你针对你作的变动代码运行了单元测试,而后你忽然发现这条简单的代码使100多条的测试用例失败了。具体缘由是由于在调用notDuper方法以前,你须要针对一些额外的业务逻辑增长条件判断来决定是否调用它。

因而你针对这个问题又进行了一次变动(修复缺陷或者bug),或许还会针对一些别的边界条件进行一些额外的修复和改动:

class SuperDuper {
  constructor(notDuper: NotSoDuper) {
    this.notDuper = notDuper
  }
  makeThingsFastAndEasy() {
     // Super readable and efficient code
    
    if (someCondition) {
      this.notDuper.invokeSomeMethod()
    } else {
      this.callInternalMethod()
    }
  }
}

又过了一段时间,由于这个SuperDuper毕竟是你职业生涯完成的最棒的类,可是当前调用noDuper的方法实在是有点不够逼格,因而你决定引入事件驱动的理念来达到不在SuperDuper内部直接调用noDuper方法的目的。

此次实际是对已经代码的一次重构工做,你引入了事件驱动模型,并对已有的代码作出了变动(重构代码以适配未来做出的变动):

class SuperDuper {
 
  makeThingsFastAndEasy() {
     // Super readable and efficient code
     ...
     dispatcher.send(actionForTheNotDuper(payload)) // Send a signal
  }
}

如今再来看咱们的SuperDuper类,已经和最原始的样子彻底不同了,由于你必须针对新的需求、存在的缺陷和bug或者适配新的软件架构而作出变动。

所以为了便于咱们作出变动,在代码的组织方式上,咱们须要用心,这样才会使咱们在作出变动时更加容易。

如何才能使代码贴近这些原则

很简单,只须要牢记,使代码保持足够简单。

Gather together the things that change for the same reasons. Separate those things that change for different reasons.

将因为相同缘由而作出改变的东西汇集在一块儿,将因为不一样缘由而作出改变的东西彼此分离。

孤立变化

对于所编写的作出变动的代码,你须要仔细的检查它们,不管是从总体检查,仍是有逻辑的分而治之,均可以达到孤立变化的目的。你须要更多的了解你所编写的代码,好比,为何这样写,代码到底作了什么等等,而且,对于一些特别长的方法和类要格外关注。

Big is bad, small is good…

大便是坏,小便是好。

追踪依赖

对于一个类,检查它的构造方法是否包含了太多的参数,由于每个参数都做为这个类的依赖存在,同时这些参数也拥有自身的依赖。若是可能的话,使用DI机制来动态的注入它们。

Use Dependency Injection

使用依赖注入

追踪方法参数

对于一个方法,检查它是否包含了太多参数,通常来说,一个方法的参数个数每每表明了其内部所实现的职能。

同时,在方法命名上也投入一精力,尽量地使方法名保持简单,它将帮助你在重构代码时,更好的达到单一职责。长的函数名称每每意味着其内部有糟糕的味道。

Name things descriptively

描述性命名。

尽早重构

尽量早的重构代码,当你看到一些代码能够以更简明的方式进行时,重构它。这将帮助你在项目进行的整个周期不断的整理代码以便于更好的重构。

Refactor to Design Patterns

按设计模式重构代码

善于作出改变

最后,在须要作出改变时,果断地去作。固然这些改变会使系统的耦合性更低,内聚性更高,而不是往相反的方向,这样你的代码会一直创建在这些原则之上。

Introduce change where it matters. Keep things simple but not simpler.

在重要的地方介绍改变。保持事情的简单性,但不是一味追求简单。

译者注

单一职责原则其实在咱们平常工做中常常会接触到,比方说

  • 咱们常常会听到DIY(dont repeat yourself)原则,其自己就是单一职责的一个缩影,为了达到DIY,对于代码中的一些通用方法,咱们常常会抽离到独立的utils目录甚至编写为独立的工具函数库, 好比lodashramda等等
  • OAOO, 指Once And Only Once, 原则自己的含义能够自行搜索,实际工做中咱们对于相同只能模块的代码应当尽量去在抽象层合并它们,提供抽象类,以后经过继承的方式来知足不一样的需求
  • 咱们都会很熟悉单例模式这个模式,但在使用时必定要当心,由于本质上单例模式与单一职责原则相悖,在实践中必定要具体状况具体分析。同时也不要过分优化,就如同文章中最后一部分说起的,咱们要保证一件事情的简单性,但不是一味地为了简单而简单。
  • 前端的技术栈中,redux对于数据流层的架构思想,便充分体现了单一职责原则的重要性,action做为对具体行为的抽象, store用来描述应用的状态,reducer做为针对不一样行为如何对store做出修改的抽象。
  • react中常常说起的木偶组件(dump component)其实和文章中第一部分的例子一模一样
  • 工厂模式命令模式也必定程度体现了单一职责原则,前者对于做为生产者存在并不须要关心消费者如何消费对象实例,后者以命令的方式封装功能自己就是单一职责原则的体现。

我可以想到的就这么多,写的比较乱,抛砖引玉,若有错误,还望指正。

关注公众号 全栈101,只谈技术,不谈人生

clipboard.png

相关文章
相关标签/搜索