一周技术学习笔记(第85期)-两篇文章13个问题重入OO设计思想

2022-12-01 15:29:07 浏览数 (1)

学习了两篇文章,转换成13个问题,我们来模拟一个问答场景,带你一起走进正交的世界。

开场

关于面向对象,设计原则,你最想跟大家分享什么?

那我想应该是,开闭原则?!就这一条原则,影响了开发30多年。以及围绕着开闭原则的“正交设计”。实际上,我们今天写的开闭原则的代码,就是正交设计的很好实践。

开闭原则、正交设计、PaaS化,这些是不是都跟面向对象有关?

谈到面向对象,我们就会有OOA、OOD、OOP三个方面的知识需要谈。那么我们今天的内容大部分属于OOD这个里面的。软件设计、设计模式、SOLID原则、正交设计、高内聚低耦合,可以说是整个OOD所要包含的内容。这里把高内聚低耦合放到最下面,可能大多数同学都认为面向对象的目的就是高内聚低耦合,实际上不是。

痛点

问题1:系统变得越来越复杂

刚才我们谈到了面向对象,似乎面向对象是个救命稻草。

然而我们大伙有个困惑,我们都是用Java写的,是面向对象的。但是我们的困惑是系统像滚雪球一样,越滚越大,越来越复杂,改一行代码如履薄冰。

您觉得导致我们系统越来越复杂的原因是什么?

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

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

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

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

问题2:如何衡量代码质量?=> 单一和开闭的精髓

我理解下您刚才说的哈。我们面临一个矛盾:左手呢是需求的变化过多,右手呢是系统变得很复杂,不可维护。但是呢,需求变化又不可避免,因为我们都听业务说了“唯一不变的是变化本身”。

化解这对矛盾的金钥匙呢,就两个原则。单一职责原则和开闭原则。而我们好多小伙伴跟我抱怨,要提升代码水平,要学23种设计模式。您这就两个!哇,一下子就简单了。您能不能给我们讲讲,这其中的精髓是什么?

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

其实这里,我们可以继续延伸一下,来看看除了OCP之外,SOLID这五种原则,它们之间的关系。我们都知道,开闭原则OCP是所有设计的终极目标,在SOLID这五大原则里面,它们之间是独立存在的吗?

其实,SOLID这五个原则里面单一原则SRP是所有设计原则的基础。而且它还经历过一个演进的过程,比如我要是问下面这个问题,大家会选择哪一个答案呢?

过程就是下面这样:

SRP它强调的是每个模块只负责一个业务,而不是同时干多个业务。而开闭原则强调的是把模块业务的变化点抽离出来,包给其他的模块。它们谈的本质上是同一个问题的两个面。

第一篇-开闭原则

----------------学习开闭思想

问题3:开闭原则的缘起?

到这里,我们知道了要用组合实现复杂的业务系统,在这个过程中也需要时刻想着怎么用开闭来应对变化。开闭,这么精髓、这么影响深远的东西,是谁想出来的?亦或是它其实也并不是一个人想出来的,而是集体智慧的结晶?

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

问题4:开闭的思想早在“冯诺依曼体系”结构中就体现了

也就是说,是勃兰特(Bertrand)先生在面向对象里想出来的。厉害。

其实严格来说,也不能完全这么说,因为早在计算机诞生的时候(新增的过渡词),冯·诺依曼体系的中央处理器(CPU)的设计完美就已经体现了 “开闭原则” 的架构思想。它表现在:

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

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

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

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

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

问题5:开闭原则实践反面1-“大修改”

我们学习到“系统设计的好坏,与开闭原则有最直接的关系”。对照这个开闭原则,我们来反思我们的一些实践。

实践中,我们常常发现,来了个新需求,我们【要大面积修改以前的代码】。

我理解这样肯定不符合开闭原则。那我们在思考,是不是做需求的时候,我们要做些分析。比如对不同的需求进行业务分组(代码分组),每组负责一个独立的业务逻辑,也就是您刚才提到的“单一职责”。紧接着处理这些分组之间的依赖关系。“具体到代码就是将不同的操作(业务)划分不同的类,再将这些类分割为不同的组件” 。对吗?

是的,架构设计的第一步是分析需求,在需求分析的过程中需要注意的就是,从需求分析角度来说,关键要抓住需求的稳定点和变化点。需求的稳定点,往往是系统的核心价值点;而需求的变化点,则往往需要相应去做开放性设计。

这里谈到变化,我们可以把变化总结为两种类型。

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

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

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

这也是有时候,会听到,开闭原则是应对变化,但同时呢,它又提倡我们编写只读的代码。

问题6:开闭原则实践反面2-“不修改”

那既然刚才提到,不能【大面积修改以前的代码】,那我们会不会走向另一个极端,比如我们的核心代码压根就不能修改!开闭原则中的“闭”,结合实践,具体要怎么拿捏?

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

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

问题7:开闭原则实践反面3-“小修改”

我们刚才聊到,遵循开闭原则的具体实践要求,不能大改、也不是说“不能改”。也就是说要适度?

我们在实践中,也发现,即便是适度改,也会畏手畏脚。比如很多老一点的系统,一堆的IF/ELSE,代码非常臃肿。一个新需求来了,我们其实也就改其中的一点点。有什么好的办法? 有人说,把IF/ELSE的逻辑单独出来,就可以简化,我在想,本来就很臃肿了,再多出一个个类岂不是更臃肿?

确实,把if/else逻辑提炼出去,形成一个个类对象,类的数量会增加,维护成本也会高一些,但是它们各自的职责更单一,更加高内聚、低耦合、扩展性也更好了。

如果不是这样,那么在原有的if/else逻辑中增加代码,你的测试都要去把所有的条件回滚一遍,测试的成本一直居高不下,而且原先的代码的可读性也会越来越差,出错的概率也会越来越大。

如果是符合开闭原则这样的代码结构,那么测试的成本就下降很多了。

问题8:开闭原则实践最佳实践-预留拓展

聊了这么多,我们发现印了那句名言,叫什么?不幸的代码,各有各的不幸,刚才讲的“大修改的”、它的反面“不让修改的”、中间的尽管修改一点点,但IF/ELSE却过于臃肿的。

那幸福的代码,是不是都很相似?!按开闭原则的思路,我们在需求分析的时候,预知“变”与“不变”,并在可能“变化”的地方,把它单拎出来,预留拓展,做到“对扩展开放、对修改关闭”。具体我们如何去实施?

这里面充分体现了扩展意识、抽象意识、封装意识

通常我们会更愿意一直等到确实需要那些抽象时再去处理。

可以首先让需求击中我们一次,当变化确实发生时,我们就来创建抽象来隔离以后发生的同类变化。

如果,提前过多的进行抽象,如果用不到,也常要去关注和维护它们,这样就造就了不必要的复杂性的臭味。

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

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

----------------升华开闭实践:不仅懂技术,还要懂业务

问题9:开闭原则实践破除“知易行难”的关键(懂业务 进化论)

有没有一种可能,我们懂的了很多道理,却依然过不好这一生。比方说,我们原理都懂的了,但是开发需求的时候还是仍然不能应用得手,你怎么看呢?

目前来看,架构设计套路有限,设计原则也就是那些,也有限,但业务领域却是无限的。如何应用有限的套路做出无限的架构设计,难点应是在业务上。没有足够的业务背景积累和业务需求洞察,架构设计就会出现知易行难的窘境。

但是想要架构师一步到位具有健全的业务领域知识也是不现实的。所以,横竖都不好,那么就可以尝试落地灵活的设计,动态去演变,也就所谓的演进式架构

而代码变动是高成本的,所以要想办法降低成本。首先代码可读要高。接着,单模块内各层,以及模块间的耦合要低。最后,代码的实现要采用合适的设计模式,以便易于扩展。

但这三点不取决于架构师,具体业务开发个人素养的影响更大。

所以,感觉约往后,业务开发的个人素养要求会越高。至少领域设计(战略规划)和架构分层、设计模式(战术应用)的诉求怕是少不了的

问题10:开闭原则实践应对需求的变化之拓展点的“程度拿捏”

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

最合理的做法是,对于一些比较确定的、短期内可能就会扩展,或者需求改动对代码结构影响比较大的情况,或者实现成本不高的扩展点,在编写代码的时候,我们就可以事先做些扩展性设计。

但对于一些不确定未来是否要支持的需求,或者实现起来比较复杂的扩展点,我们可以等到有需求驱动的时候,再通过重构代码的方式来支持扩展的需求。

正交设计

问题11:正交设计概念,需要怎么理解?

我们今天的主题里面包含了打造正交系统,那么什么是正交设计?

“正交性”是从几何学中借用来的术语。若两条直线相交后构成直角,它们就是正交的。例如,图表中的坐标轴就是正交的。对于向量而言,这两条线相互独立。图示中的向量1指向北,完全不影响东西朝向。向量2指向东,完全不影响南北朝向。

在计算科学中,这个术语象征着独立性或解耦性。对于两个或多个事物,其中一个的改变不影响其他任何一个,则这些事物是正交的。在良好设计的系统中,比如数据库相关代码应该和用户界面保持正交:你可以变更界面但不应影响数据库,切换数据库而不必更换界面。

问题12:正交设计=>OCP化 ?

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

正交设计的核心机理某种层度上是将开闭原则上移到业务系统。业务对外只读,意味着不可变,但不变的业务生命周期是很短暂的,所以要可扩。要扩展还要不变,就倒逼着要做兼容,而兼容可能会导致现有的功能职责不单一,这又倒逼着要对现有的功能做再抽象,以适应更广的“单一职责”。这也是为什么,我们的CORE工程有时是个“壳”,负责串流程并留好位置,垂直业务是“插件”的原因。

所以呢不改是不可能的,只是改的结果应当是让系统往更稳定去发展。然而这里面其实好难,无论是新的抽象的定义还是职责范围的扩张,这都需要有强大的分析能力和精湛的设计思维,这也正是我们正在做的业务建模中要定义和梳理的,之后还需要重构手法、调优能力以及站在核心目标上的权衡来支撑我们去落地到正交化的代码工程上,这也是领域架构师的职责。不过难亦是乐趣所在。

到了这里,如何实现一个正交分解的业务系统,我们是不是已经有答案了?

是的,应该也能看出来如何实现正交分解的系统了,只要符合开闭原则的设计,就是正交分解的。正交分解的系统是开闭原则指导下的实践案例。其实结合上面我们介绍的内容,只要按照OCP化的设计原则为指导方法去实践,我们的系统就是一个正交分解的系统。

问题13:正交分解架构设计就是业务正交分解的过程

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

架构设计就是业务正交分解的过程

我们分解之后的每个模块都有它自己的业务,这些业务对应的系统模块之间是一种正交的关系。这里面我们说的模块是一种泛指,它包括:方法、类、接口、子系统、网络服务程序等等。

“架构就是业务正交分解的过程”这句话看似简单,但是它太重要了,它是一切架构动作的基础。按照正交的方向进行业务分解的最终结果就是有一个最小化的核心系统,周围有多个小的正交的周边系统。而且,这个最小化的核心系统尽可能的在以后的需求变化中是“只读”的,如果想要修改就到周边的系统上。

你看,这不就是一个“开闭原则”的案例么。

寄语

问题14:程序员的基本思维【最后,寄语】

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

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

1.DRY (Don’t Repeat Yourself) 。

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

2.分而治之。

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

3.开闭原则。

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

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

架构设计的执行路径(top-down):

- 业务,业务,还是业务。更好的抽象、分解、管理业务的复杂度才是架构发展的原动力。需求分析,领域理解才是根,架构是为魂。

- 拆解业务场景为「通用的设计场景组合」,并不断完善通用场景下的架构范式。

- 任何架构范式都可以拆解为:最小化的核心系统 多个彼此正交的周边系统。

- 生命不止,正交不断,这是架构师的信仰。

参考资料:

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

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

《程序员修炼之道》

----END----

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

0 人点赞