SOLID 在前端

733 阅读7分钟

solid.jpeg

Why

面向对象程序设计(OOP)在 80 年代成为了一种主导思想后,如何进行良好的 OOP 设计成为了业界一直探索的问题。SOLID 原则首先由著名的计算机科学家 Robert C·Martin (著名的 Bob 大叔)由 2000 年在他的论文中提出,稍晚由 Michael Feathers 先使用的。Bob 大叔还是畅销书《代码整洁之道》和《架构整洁之道》的作者,也是“敏捷开发”的创始人之一,他们始终追求的是 —— “创建可多人协作的、易于理解的、易读的以及可测试的代码。” SOLID 原则即是为了在 OOP 中达成这个目的。

SOLID 原则是面向对象设计的五条原则,这是设计类(class)结构时应该遵守的准则和最佳实践。当这些原则被一起应用时,它们使得一个程序员开发一个容易进行软件维护和扩展的系统变得更加可能。

What

SOLID 是 5 条原则集合在一起的缩写,分别是:

  • S 单一职责原则
  • O 开闭原则
  • L 里氏替换原则
  • I 接口隔离原则
  • D 依赖倒置原则

S —— 单一职责原则

单一职责原则的描述是一个 class 应该只做一件事,一个 class 应该只有一个变化的原因。更技术的描述该原则:应该只有一个软件定义的潜在改变(数据库逻辑、日志逻辑等)能够影响 class 的定义。

O —— 开闭原则

开闭原则要求“class 应该对扩展开放对修改关闭”。这个原则想要表达的是:我们应该能在不动 class 已经存在代码的前提下添加新的功能。这是因为当我们修改存在的代码时,我们就面临着创建潜在 bug 的风险。因此,如果可能,应该避免碰通过测试的(大部分时候)可靠的生产环境的代码。

L —— 里氏替换原则

给定 class B 是 class A 的子类,在预期传入 class A 的对象的任何方法传入 class B 的对象,方法都不应该有异常。因为继承假定子类继承了父类的一切,子类可以扩展行为但不会收窄。

I —— 接口隔离原则

隔离意味着保持独立,接口隔离原则是关于接口的独立。指明下游客户(client)不应被迫使用对其而言无用的方法或功能。接口隔离原则(ISP)拆分非常庞大臃肿的接口成为更小的和更具体的接口,这样下游客户将会只需要知道他们感兴趣的方法。

D —— 依赖倒置原则

依赖倒置原则描述的是我们的 class 应该依赖接口和抽象类而不是具体的类和函数。我们想要我们的类开放扩展,因此我们需要明确我们的依赖的是接口而不是具体的类,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。

该原则规定:

  1. 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
  2. 抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。

在前端 How(React 为例)

React 的开发团队更推崇函数式的开发方式,现今在一般的 React 开发过程中已经比较难看到面向对象的影子了,那么 SOLID 原则是否还能落地呢?下面以一个简单的 React 对话框组件为例,浅谈一下我对 SOLID 原则在 React 开发中落地的理解。

该对话框组件有常见的开启、关闭功能,在开启时需要访问 API,关闭时需要发送给 API 结果,并且在前端展示结果交互。以下都是伪代码:

// 依赖倒置,依赖 res.result 的返回结果,但对于如何返回不关心
const SInit = async (params) => {
    const res = await request(params)

    return res.result
}

const SConfirm = async (params) => {
    const res = await request(params)

    return res.result
}

// 单一职责,对话框只用来进行SConfirm 的确认
const ConfirmModal = ({ isOpen, onOpen, onOk, onCancel }) => {
  const onOpenInner = async () => { ... }

  const onOkInner = () => {
      SConfirm({ ... })
      // 开闭原则,对成功、取消的后续功能使用 props 扩展,但不实现
      onOk()
  }

  const onCancelInner = () => { ... }

  ...

  // 接口隔离,下游调用方只需要实现必要 props
  return (
    <Modal
      visible={isOpen}
      title="请确认"
      onOpen={onOpenInner}
      onOk={onOkInner}
      onCancel={onCancelInner}
      ...
    >
      { ... }
    </Modal>
  )
}

可以大体看到,由于 React 组件的开发方式和面向对象编程小有差距,更多以组合的方式进行开发,所以跟继承相关的两条原则 —— 里氏替换和接口隔离的使用并不明显。但是如果在设计 React 组件中能遵守开闭原则、单一职责、和依赖反转,依旧对增强组件的健壮性和扩展性很有帮助。例如,上面给的例子单一对应了特定业务的确认功能,并通过开闭原则和依赖反转可以在不修改组件的情况下,可以修改业务对应的 API,支持多种不同的情况。

总结

综上,尽管 SOLID 原则应用和前端开发的常见场景有一定的距离,但是其中的思想依旧可以在合适的落地之后帮助前端开发者写出更好的代码。不过落地的场景千差万别,落地出问题反而可能只有表面形式,实际适得其反。这其中有没有什么可以相对通用衡量的办法呢?之前写过的前端测试中的可测试性,这是一个不错的衡量手段,测试本身也可以有效提高代码质量。

以下部分是亚马逊(NASDAQ:AMZN)众多八股文恶习中难得有点用的东西。因为一些原因,写的时候就写了这部分,发出来的时候也保留吧。

FAQ

这些开发原则在开发过程中是否同等重要?

从上面的例子可以看出,由于 React 的组件开发并非面向对象的开发方式,JavaScript 整体上也不像 Java 强依赖于面向对象的开发形式,所以对于一些面向对象的相关原则,重要性没那么强。从我个人的实践经验来说,对继承提出要求的里氏替换和接口隔离按照定义严格落地的情况确实不多。

这些开发原则是否相对独立?

这些原则既可以互相独立使用,也可以互相配合,无所谓是否独立。在实际情况中,往往可以通过一些最佳实践,同时实现几条原则的效果,这也体现了最佳实践的价值。通常情况下,开闭原则和依赖反转总是在一起落地的,因为开闭原则需要依赖反转去实现,这也是前端开发中用的比较多的原则。

应用 SOLID 原则是否会带来额外的成本?

在应用初期会难以避免的带来一些心智负担,毕竟是实践一种新的思想,对于开发设计也提出了更高的要求。但是在团队熟练掌握以后,应用 SOLID 带来的开发成本增加微乎其微,反而对于长时间迭代项目会有明显的在可维护性和扩展性上的收益。

提高的可维护、可扩展性能否衡量?

可以,但是需要配合敏捷等开发流程管理方法,才能量化的看出团队产出的提升。如果没有这些流程,那么在项目迭代 3 个月到半年后会有体感上的感觉,开发速度相比不采用 SOLID 原则的项目会下降的更少,技术债更少,更容易维护可协作方的关系。

有没有其他的原则也可以达到类似的效果?

效果其实更多的看落地过程。单就原则本身来说,SOLID 原则比较具体,诞生应对的场景也比较明确。其他的例如 DRY 原则、KISS 原则、YAGNI 原则等使用场景更加宽泛,描述的问题也没那么细。但是,这些方法论落地的好的话,依旧可以取得非常好的效果。