【译】Understanding SOLID Principles - Open Closed Principle

Understanding SOLID Principles: Open Closed Principle

这是理解SOLID原则,介绍什么是开闭原则以及它为什么能够在对已有的软件系统或者模块提供新功能时,避免不必要的更改(重复劳动)。

开闭原则是什么

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

软件实体(类、模块、函数等)都应当对扩展具有开放性,但是对于修改具有封闭性。

首先,我们假设在代码中,我们已经有了若干抽象层代码,比如类、模块、高阶函数,它们都仅做一件事(还记得单一职责原则吗?),并且都做的十分出色,所以我们想让它们始终处于简洁、高内聚并且好用的状态。

但是另一方面,我们还是会面临改变,这些改变包含范围(译者注:应当是指抽象模块的职责范围)的改变,新功能的增加请求还有新的业务逻辑需求。

所以对于上面我们所拥有的抽象层代码,在长期想让它处于一成不变的状态是不现实的,你不可避免的会针对以上的需要作出改变的需求,增加更多的功能,增加更多的逻辑和交互。在上一篇文章,我们知道,改变会使系统复杂,复杂会促使模块间的耦合性上升,所以我们迫切地需要寻找一种方法能够使我们的抽象模块不仅可以扩大它的职责范围,同时还能够保持当前良好的状态(简洁、高内聚、好用)。

这便是开闭原则存在的意义,它能够帮助我们完美地实现这一切。

如何实践开闭原则

当你需要对已有代码作出一些修改时,请切记以下两点:

  • 保持函数、类、模块当前它们本身的状态,或者是近似于它们一般情况下的状态(即不可修改性)
  • 使用组合的方式(避免使用继承方式)来扩展现有的类,函数或模块,以使它们可能以不同的名称来暴露新的特性或功能

这里关于继承,我们特意增加了一个注释,在这种情况下使用继承可能会使模块之间耦合在一起,同时这种耦合是可避免的,我们通常在一些预先有着良好定义的结构上使用继承。(译者注:这里应该是指,对于我们预先设计好的功能,推荐使用继承方式,对于后续新增的变更需求,推荐使用组合方式)

举个例子(译者注:我对这里的例子做了一些修改,原文中并没有详细的说明)

interface IRunner {
  run: () => void;
}
class Runner implements IRunner {
  run(): void {
    console.log("9.78s");
  }
}

interface IJumper {
  jump: () => void;
}
class Jumper implements IJumper {
  jump(): void {
    console.log("8.95,");
  }
}

例子中,我们首先声明了一个IRunner接口,之后又声明了IJumper,并分别实现了它们,并且实现类的职能都是单一的。

假如现在我们需要提供一个既会跑又会跳的对象,如果我们使用继承的方式,可以这么写

class RunnerAndJumper extends Runner {
  jump: () => void
}

或者

class RunnerAndJumper extends Jumper {
  run: () => void
}

但是使用继承的方式会使这个RunnerAndJumperRunner(或者Jumper)耦合在一起(耦合在一起的原因是因为它的职责不再单一),我们再来用组合的方式试试看,如下:

class RunnerAndJumper {
  private runnerClass: IRunner;
  private jumperClass: IJumper;
  constructor(runner: IRunner, jumper: IJumper) {
    this.runnerClass = new runner();
    this.jumperClass = new jumper();
  }
  run() {
    this.runnerClass.run();
  }
  jump() {
    this.jumperClass.jump();
  }
}

我们在RunnerAndJumper的构造函数中声明两个依赖,一个是IRunner类型,一个是IJumper类型。

最终的代码其实和依赖倒置原则中的例子很像,而且你会发现,RunnerAndJumper类本身并没有与任何别的类耦合在一起,它的职能同样是单一的,它是对一个即会跑又会跳的实体的抽象,并且这里我们还可以使用DI(依赖注入)技术进一步的优化我们的代码,降低它的耦合度。

反思

开闭原则所带来最有用的好处就是,当我们在实现我们的抽象层代码时,我们就可以对未来可能需要作出改变的地方拥有一个比较完整的设想,这样当我们真正面临改变时,我们所对原有代码的修改,更贴近于改变本身,而不是一味的修改我们已有的抽象代码。

在这种情况下,由于我们节省了不必要的劳动和时间,我们就可以将更多的精力投入到关于更加长远的事宜计划上面,而且可以针对这些事宜需要作出的改变,提前和团队沟通,最终给予一套更加健壮、更符合系统模块本身的解决方案。

在整个软件开发周期中(比如一个敏捷开发周期),你对于整个周期中的事情了解的越透彻、越多,则越好。身为一个工程师,在一个开发冲刺中,为了在冲刺截止日期结束前,实现一个高效的、可靠的系统,你不会期望作出太多的改变,因此往往你可能会“偷工减料”。

从另一个角度来讲,我们也应当致力于在每一次面临需求变更的情况下,不需要一而再,再而三的更改我们已有的代码。所有新的功能都应当通过增加一个新的组合类或方法实现,或者通过复用已有的代码来实现。

插件与中间件

充分贯彻开闭原则的另一个例子,便是插件与中间件架构,我们可以从三个角度来简单分析这种架构是如何运作的:

  • 内核或者容器:往往是核心功能的实现的前提,一般会成为整个系统最核心的部分
  • 插件:在实现容器的基础上,往往一些核心功能都是以内置的插件实现的,并且,通过实现一套通用的网关类接口,我们可以使插件具有可插拔性,这样在需要新增特性和功能时,只需要实现新的插件并添加到容器即可,比如支持插件扩展功能的浏览器Chrome
  • 中间件:中间件我们可以通过一个例子来说明,比如我们拥有一个请求 - 响应周期,我们可以通过中间件,在周期中添加中间业务逻辑,以便为应用程序提供额外的服务或横切关注点,比如Reduxexpress还有很多框架都支持这样的功能。

总结

希望这篇文章能够帮助你学会如何应用开闭原则并且从中收益。设计一个具有可组合性的系统,同时提供具有良好定义的扩展接口,是一种非常有用的技术,这种技术最关键的地方在于,它使我们的系统能够在保持强健的同时,提供新功能、新特性,但是却不会影响它当前的状态。

译者注

开闭原则是面向对象编程中最重要的原则之一,有多重要呢?这么说吧,很多的设计原则和设计模式所希望达成的最终状态,往往符合开闭原则,因此需要原则也都作为实现开闭原则的一种手段,在原文的例子中,我们可以很明显的体会到,在实现开闭原则所提倡的理念的过程中,我们不经意地使用之前两篇文章中涉及的原则,比如:

  • 保持对象的单一性(单一职责)
  • 实现依赖于抽象(依赖倒置原则)

我之前一直是做后端相关工作的,所以对于开闭原则接触较早,这两年转行做了前端,随着nodejs的发展,框架技术日新月异,但是其中脱颖而出的优秀框架往往是充分贯彻了开闭原则,比如expresswebpack还有状态管理容器redux,它们均是开闭原则的最佳实践。

另外一方面,在这两年的工作也感受到,适当的使用函数式编程的思想,往往是贯彻开闭原则一个比较好的开始,因为函数式的编程中的核心概念之一便是compose(组合)。以函数式描述业务往往是原子级的指令,之后在需要描述更复杂的业务时,我们复用并组合之前已经存在的指令以达到目的,这恰恰符合开闭原则所提倡的可组合性。

最后在分享一些前端中,经常需要使用开闭原则的最佳业务场景,

  • UI组件的表单组件:对于表单本身以容器来实现,表单项以插件来实现,这样对于表单项如何渲染、如何加载、如何布局等功能,均会封闭与表单容器中,而对于表单项如何校验、如何取值、如何格式化等功能,则会开放与表单项容器中。
  • API服务:一般我们可能会在项目中提供自定义修改请求头部的工具方法,并在需要的时候调用。但这其实是一种比较笨的方法,如果可能的话,建议使用拦截器来完成这项任务,不仅会提供代码的可读性,同时还会使发接口的业务层代码保持封闭。
  • 事件驱动模型:对于一些复杂的事件驱动模型,比如拖拽,往往使用开闭原则会达到意想不到的效果。最近有一个比较火的拖拽库draggable,提供的拖拽体验相比其他同类型的库简直不是一个级别。我前段时间去读它的源码,发现它之所以强大,是因为在它内部,针对多种拖拽事件,封装了独立的事件发射器(其内部称作Sensor),之后根据这些发射器指定了一套独立的抽象事件驱动模型,在这个模型基础上,针对不同的业务场景提供不同的插件,比如:
    • 原生拖拽(Draggable)
    • 拖拽排序(Sortable)
    • 拖拽放置(Droppable)
    • 拖拽交换(Swappable)

还有若干提高用户体验的其他插件,这一切均是以开闭原则而实现的。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏喔家ArchiSelf

探索嵌入式应用框架(EAF)

EAF是Embedded Application Framework 的缩写,即嵌入式应用框架。嵌入式应用框架是 Application framework的一...

21530
来自专栏BestSDK

史上最全,0基础快速入门Python

首先,在学习之前一定会考虑一个问题——Python版本选择 对于编程零基础的人来说,选择Python3。 ? 1、学习基础知识 首先,Python 是一个有条理...

53340
来自专栏ThoughtWorks

为什么优秀的程序员喜欢命令行?|洞见

优秀的程序员 要给“优秀的程序员”下一个明确的定义无疑是一件非常困难的事情。擅长抽象思维、动手能力强、追求效率、喜欢自动化、愿意持续学习、对代码质量有很高的追求...

31650
来自专栏Young Dreamer

webpack3新特性简介

6月20号webpack推出了3.0版本,官方也发布了公告。根据公告介绍,webpack团队将未来版本的改动聚焦在社区提出的功能需求,同时将保持一个快速、稳定的...

30690
来自专栏進无尽的文章

聊聊程序设计思想之面向数据驱动编程

数据驱动编程的核心:数据驱动编程的核心出发点是相对于程序逻辑,人类更擅长于处理数据。数据比程序逻辑更容易驾驭,所以我们应该尽可能的将设计的复杂度从程序代码转移至...

76420
来自专栏编程

5个酷毙的Python工具

工欲善其事必先利其器,一个好的工具能让起到事半功倍的效果,Python社区提供了足够多的优秀工具来帮助开发者更方便的实现某些想法,下面这几个工具给我的工作也带来...

25680
来自专栏韩伟的专栏

帧同步游戏开发基础指南

最近一个月休了个假,体验了一下类似欧洲的田园生活。所以更新几乎荒废了,但是总结和积累是一直持续着的。根据前一阶段对于实时对战游戏的开发思考,写了这一篇入门级的文...

88970
来自专栏jouypub

数据仓库之ETL实战

ETL,Extraction-Transformation-Loading的缩写,中文名称为数据抽取、转换和加载。

80110
来自专栏Timhbw博客

《Pro Git》学习笔记连载(一.起因)

2016-11-1619:39:45 发表评论 299℃热度 《Pro Git》这本书可谓是学习进阶Git的最好材料了,之所以阅读这本书,是因为虽然用Git,...

318110
来自专栏Django中文社区

在学习django-rest-framework时收集的学习资料推荐

由于我平时开发的 django 项目都比较小,所以一直以来都是使用 django 模板引擎渲染 html 页面这种比较原始的方式在开发。最近发起了一个 Djan...

45160

扫码关注云+社区

领取腾讯云代金券