这是理解
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
}
但是使用继承的方式会使这个RunnerAndJumper
与Runner
(或者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
。Redux
、express
还有很多框架都支持这样的功能。希望这篇文章能够帮助你学会如何应用开闭原则并且从中收益。设计一个具有可组合性的系统,同时提供具有良好定义的扩展接口,是一种非常有用的技术,这种技术最关键的地方在于,它使我们的系统能够在保持强健的同时,提供新功能、新特性,但是却不会影响它当前的状态。
开闭原则是面向对象编程中最重要的原则之一,有多重要呢?这么说吧,很多的设计原则和设计模式所希望达成的最终状态,往往符合开闭原则,因此需要原则也都作为实现开闭原则的一种手段,在原文的例子中,我们可以很明显的体会到,在实现开闭原则所提倡的理念的过程中,我们不经意地使用之前两篇文章中涉及的原则,比如:
我之前一直是做后端相关工作的,所以对于开闭原则接触较早,这两年转行做了前端,随着nodejs
的发展,框架技术日新月异,但是其中脱颖而出的优秀框架往往是充分贯彻了开闭原则,比如express
、webpack
还有状态管理容器redux
,它们均是开闭原则的最佳实践。
另外一方面,在这两年的工作也感受到,适当的使用函数式编程的思想,往往是贯彻开闭原则一个比较好的开始,因为函数式的编程中的核心概念之一便是compose(组合)
。以函数式描述业务往往是原子级的指令,之后在需要描述更复杂的业务时,我们复用并组合之前已经存在的指令以达到目的,这恰恰符合开闭原则所提倡的可组合性。
最后在分享一些前端中,经常需要使用开闭原则的最佳业务场景,
Sensor
),之后根据这些发射器指定了一套独立的抽象事件驱动模型,在这个模型基础上,针对不同的业务场景提供不同的插件,比如:还有若干提高用户体验的其他插件,这一切均是以开闭原则而实现的。