前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一周技术学习笔记(第83期)-这一条原则竟然影响了现代编程30多年!

一周技术学习笔记(第83期)-这一条原则竟然影响了现代编程30多年!

作者头像
王新栋
发布2022-12-01 15:27:39
2130
发布2022-12-01 15:27:39
举报
文章被收录于专栏:程序架道程序架道

我们一般应该怎么衡量代码的质量?容易被阅读,容易被维护,其中在维护代码的过程中,代码的扩展又是非常重要的衡量标准。另外,在我们所熟悉的23种设计模式中,大部分的设计模式都是为了解决代码的扩展性问题而被总结出来的。而所有这些跟扩展性相关的设计模式又都是以一个很重要的原则作为指导原则的:开闭原则。

架构分解中有两大难题:其一,需求的交织。不同需求混杂在一起,也就是存在所谓的全局性功能。其二,需求的易变。不同客户,不同场景下需求看起来很不一样,场景呈发散趋势。我们可能经常会听到各种架构思维的原则或模式。但就架构的本质而言,我们核心要掌握的架构设计的工具其实就只有两个,一个是组合,用小业务组装出大业务,组装出越来越复杂的系统。另外一个是开闭,如何应对变化。

开闭原则是什么时间提出来的?

开闭原则(Open Closed Principle,OCP)由勃兰特·梅耶(Bertrand Meyer)提出,他在 1988 年的著作《面向对象软件构造》(Object Oriented Software Construction)中提出:软件实体应当对扩展开放,对修改关闭(Software entities should be open for extension,but closed for modification),这就是开闭原则的经典定义。

都说开闭原则是应对变化的,那么变化可以分为几种类型?

我们可以把变化总结为两种类型。

第一种,单一模块的逻辑变化:仅仅变化了一个逻辑,并不涉及到其它的模块。比如原先的算法逻辑X*Y*Z,现在是X+Y+Z。这个时候就可以直接修改原有类里面的方法来实现新的需求。

第二种,业务逻辑的变化:这种情况下,一个业务逻辑发生变化,会对一系列的模块产生影响。特别是一个底层的业务逻辑变化了然后引起了很多的高层代码模块的变化,这个时候就需要通过扩展来实现变化。

都说开闭原则是应对变化的,那它又为什么总是提倡我们要编写“只读”的代码呢?

本质上,开闭原则的背后,是推崇模块业务的确定性。我们可以修改模块代码的缺陷(Bug),但不要去随意调整模块的业务范畴,增加功能或减少功能都并不鼓励。这意味着,它认为模块的业务变更是需要极其谨慎的,需要经得起推敲的。

我见过有的代码为了判断符合某个条件就执行某个动作,对于这样的逻辑,如果要重构成符合开闭原则的代码,我们往往都是将那些if/else逻辑拿到外面去,形成一个个类对象,这样我们的维护成本不就是更高了吗?

确实,把if/else逻辑提炼出去,形成一个个类对象,类的数量会增加,维护成本也会高一些,但是它们各自的职责更单一,更加高内聚、低耦合、扩展性也更好了。如果不是这样,那么在原有的if/else逻辑中增加代码,你的测试都要去把所有的条件回滚一遍,测试的成本一直居高不下,而且原先的代码的可读性也会越来越差,出错的概率也会越来越大。如果是符合开闭原则这样的代码结构,那么测试的成本就下降很多了。

架构设计的第一步是分析需求,那么这个过程有什么需要注意的吗?

从需求分析角度来说,关键要抓住需求的稳定点和变化点。需求的稳定点,往往是系统的核心价值点;而需求的变化点,则往往需要相应去做开放性设计。

开闭原则是一种 “面向对象编程(OOP)” 领域提出来的编程思想,这种说法为什么不对?

冯·诺依曼体系的中央处理器(CPU)的设计完美体现了 “开闭原则” 的架构思想。它表现在:

指令是稳定的,但指令序列是变化的,只有这样计算机才能够实现 “解决一切可以用 ‘计算’ 来解决的问题” 这个目标。计算是稳定的,但数据交换是多变的,只有这样才能够让计算机不必修改基础架构却可以适应不断发展变化的交互技术革命。

体会一下:我们怎么做到支持多变的指令序列的?我们由此发明了软件。我们怎么做到支持多变的输入输出设备的?我们定义了输入输出规范。

我们不必去修改 CPU,但是我们却支持了如此多姿多彩的信息世界。

多么优雅的设计。它与面向对象无关,完全是开闭原则带来的威力。

冯·诺依曼计算体系结构还记得它描述了什么吗?

数学家冯·诺依曼提出了计算机制造的三个基本原则,即采用二进制逻辑、程序存储执行以及计算机由五个部分组成(运算器、控制器、存储器、输入设备、输出设备),这套理论被称为冯·诺依曼体系结构。

开闭原则中提到的“闭”就是让我们对核心的代码不要进行修改吗?

将开闭原则上移到业务系统。业务对外只读,意味着不可变,但不变的业务生命周期是很短暂的,所以要可扩。要扩展还要不变,就倒逼着要做兼容,而兼容可能会导致现有的功能职责不单一,这又倒逼着要对现有的功能做再抽象,以适应更广的“单一职责”。

所以不改是不可能的,只是改的结果应当是让项目往更稳定去发展。然而这里面其实好难,无论是新的抽象的定义还是职责范围的扩张,这都需要有强大的分析能力和精湛的设计思维、重构手法、调优能力以及站在核心目标上的权衡来支撑。然而难亦是乐趣所在。

理解了开闭原则的定义之后,我应该如何执行呢?

系统设计的好坏,与开闭原则有最直接的关系。如果一个新的需求的实现,要大肆改动以前的代码,说明这个系统极其 “不稳定”,肯定也是不符合开闭原则的。

我们在设计实现一个需求时,首先要做的是,理解需求。要对不同的需求进行业务分组(代码分组),每组负责一个独立的业务逻辑,也就是 SRP。然后在处理这些分组之间的依赖关系。

“具体到代码就是将不同的操作(业务)划分不同的类,再将这些类分割为不同的组件”

在业务正交分解的过程中必然会遇到以前分解好的模块需要调整的情况,比如说随着新模块的加入发现和老模块的部分实现有重复。这种情况下是保持新老模块的重复部分呢还是抽取出共同的部分作为更基础的支撑模块呢?如果要抽取共同的模块必然会涉及老模块的修改,这种情况是否有违反了开闭原则呢?更进一步开闭原则和重构的关系应该如何处理?

开闭原则不是追求的完全不修改,而是不改变业务范畴,不轻易改变改变模块的使用界面,另外在额外提一点就是,修改BUG也不在开闭原则的思考范畴内,修改业务需求和修改BUG是完全不同的。

你认为程序员需要拥有哪些最基本的思维?

对于程序员而言,三种思维最为基础:

1.DRY (Don’t Repeat Yourself) 。

这是好程序员的根本追求,永久的驱动力。

2.分而治之。

这是人类解决复杂问题的普遍方式。

3.开闭原则。

这是应对变化(主动的变化如功能扩展,被动的变化如故障修复)的最佳手段。

其他各种原则/方法/模式/最佳实践,全部都是以此三者为基础,结合具体领域/场景/时代的更具操作性的推论。

为什么我们的系统往往会变得越来越复杂呢?

1. 事情变得复杂往往有两个方面的原因。1.联系耦合过多 2.变化过多

2. 架构的目的就是让事情变得,简单,清晰。

3. 应对耦合过多,可以使用单一原则。

4. 应对变化过多,可以用开闭原则。

开闭原则是一个知易行难的事情,我知道了定义,知道了原理,可是我在做设计,在进行开发需求的时候还是仍然不能应用得手,你怎么看呢?

目前来看,架构设计套路有限,设计原则也就是那些,也有限,但业务领域却是无限的。如何应用有限的套路做出无限的架构设计,难点应是在业务上。没有足够的业务背景积累和业务需求洞察,架构设计就会出现知易行难的窘境。但是想要架构师一步到位具有健全的业务领域知识也是不现实的。所以,横竖都不好,那么就落地灵活的设计,动态去演变,也就所谓的演进式架构。而代码变动是高成本的,所以要想办法降低成本。首先代码可读要高。接着,单模块内各层,以及模块间的耦合要低。最后,代码的实现要采用合适的设计模式,以便易于扩展。但这三点不取决于架构师,具体业务开发个人素养的影响更大。所以,感觉约往后,业务开发的个人素养要求会越高。至少领域设计(战略规划)和架构分层、设计模式(战术应用)的诉求怕是少不了的

写出符合开闭原则代码的关键是提前预留好扩展点,而这个前提有需要我们识别到扩展点。”如果你开发的是一个业务导向的系统,比如金融系统、电商系统、物流系统等,要想识别出尽可能多的扩展点,就要对业务有足够的了解,能够知道当下以及未来可能要支持的业务需求。但是,即便我们对业务、对系统有足够的了解,那也不可能识别出所有的扩展点,即便你能识别出所有的扩展点,为这些地方都预留扩展点,这样做的成本也是不可接受的。我们没必要为一些遥远的、不一定发生的需求去提前买单,做过度设计”。这种情况下,我们又该如何做呢?

最合理的做法是,对于一些比较确定的、短期内可能就会扩展,或者需求改动对代码结构影响比较大的情况,或者实现成本不高的扩展点,在编写代码的时候之后,我们就可以事先做些扩展性设计。但对于一些不确定未来是否要支持的需求,或者实现起来比较复杂的扩展点,我们可以等到有需求驱动的时候,再通过重构代码的方式来支持扩展的需求。

所谓代码的完美境界,亦非加无可加,而是减无可减。。。。。。

从技能这个角度来说,对于一名架构师,他需要拥有的能力可以归结为三种能力,理解需求的能力、读代码的能力和抽象系统的能力。在拥有了这三种能力之后,他可以利用这些技术技能去实现业务需求,在实现需求的过程中还需要想着如何做到让不同的业务之间耦合度更小。

架构设计就是业务正交分解的过程。我们分解之后的每个模块都有它自己的业务,这些业务对应的系统模块之间是一种正交的关系。这里面我们说的模块是一种泛指,它包括:方法、类、接口、子系统、网络服务程序等等。“架构就是业务正交分解的过程”这句话看似简单,但是它太重要了,它是一切架构动作的基础。按照正交的方向进行业务分解的最终结果就是有一个最小化的核心系统,周围有多个小的正交的周边系统。而且,这个最小化的核心系统尽可能的在以后的需求变化中是“只读”的,如果想要修改就到周边的系统上。你看,这不就是一个“开闭原则”的案例么。

参考资料:

https://time.geekbang.org/column/article/175236

https://time.geekbang.org/column/article/176075

----END----

这里记录,我每周碰到的,或想到的,引起触动,或感动的,事物的思考及笔记。不见得都对,但开始思考记录总是好的。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-09-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序架道 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档