软件开发的上古智慧

软件开发的上古智慧

一、总结

一句话总结:看得出类之间的依赖关系不合理,自然容易发现子系统之间的依赖不合理;搞得懂Unix如何巧妙定义通用的IO设备,自然容易想到对PC Web、Mobile Web、App内页面做合适抽象;认得清各线程、进程、链接库的职责,自然容易明白微服务也需要避免跨边界调用。更妙的是,掌握这种古老的视角,往往更能摆脱细节的困扰,把握问题的核心。就像老子说的那样:治大国如烹小鲜。

其实吧,就是弄懂底层最基本的原理,上层自然是迎刃而解。

1、架构设计是什么?

架构设计是一门复杂的学问,要综合考虑编码、质量、部署、发布、运维、排障、升级等等各种因素,作出权衡。

所谓架构,就是“用最小的人力成本来满足构建和维护系统的需求”的设计行为。以前的面向对象系统,和如今的分布式系统,在这一点上是完全一致的。

2、让你接手一套不稳定但要紧的在线系统,这套系统还有各种问题:变量命名非常随意,依赖逻辑错综复杂,层次结构乱七八糟,部署流程一塌糊涂,监控系统一片空白…… 你该怎么办

我提了个很“笨”的办法:把所有“共享变量”都抽到Redis中读写,消灭本地副本,然后把稳定版本程序多部署几份,就可以多启动几个实例,这些实例标记为AB两组。同时,在访问链路前部搭建代理服务用于分流请求,核心功能请求分配到A组(程序基本不更新),外围功能请求分配到B组(程序按业务需求更新)。这样看起来有点“多此一举”,AB两组都只有部分代码提供服务,而且要通过Redis共享状态,但是无论B组的程序如何更新,都不会影响到A组所承载的核心服务。

这办法有个专门的名字叫“蓝绿部署”。

大家说了很多办法:开发态度更认真一些,把单元测试都补全,重构代码拆分核心功能和非核心功能,跟业务方谈暂停需求…… 这些办法都很对,但是,都需要时间才能见效,而我们最缺的就是时间。

3、SOC(关注点分离)、SRP(单一责任原则)、OCP(开放-闭合原则)是什么?

关注点分离是日常生活和生产中广泛使用的解决复杂问题的一种系统思维方法。大体思路是,先将复杂问题做合理的分解,再分别仔细研究问题的不同侧面(关注点)最后综合各方面的结果合成整体的解决方案。在概念上分割整体以使实体个体化的观点可以追溯到柏拉图。柏拉图把探究自然比作在关节处切割自然,窍门在于要找到关节,不要像生疏的屠夫那样把关节切得粉碎。庄子在庖丁解牛寓言中也阐释了类似的真知灼见。 作为最重要的计算思维原则之一,关注点分离是计算科学和软件工程在长期实践中确立的一项方法论原则。此原则在业界更多的时候以分而治之的面目出现,即将整体看成为部分的组合体并对各部分分别加以处理。模块化是其中最有代表性的具体设计原则之一

SRP:单一职责原则

1.定义:一个类应该只有一个发生变化的原因

2.分析:如果一个累承担了多于一个的职责,那么引起它变化的原因就会有多个,就等于把这些职责耦合在了一起。一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭到意想不到的破坏。

4、编程的三大基本范式是什么?

结构化编程

一般理解,结构化编程是由if-else, switch-case之类的语句组织程序代码的方式,而杜绝了goto导致的混乱。但是从更深的层次上看,它也是一种设计范式,避免随意使用goto,使用if-else, switch-case之类控制语句和函数、子函数组织起来的程序代码,可以保证程序的结构是清楚的,自顶向下层层细化,消灭了杂错,杜绝了混淆。

面向对象编程

一般理解,面向对象编程是由封装、继承、多态三种特性支持的,包含类、接口等等若干概念的编程方式。但是从更深的层次上看,它也是一种设计范式。多态大概算其中最“神奇”的特性了,程序员在确定接口时做好抽象,代码就可以很灵活,遇到新情况,新写一个实现就可以无缝对接。

函数式编程

一般理解,函数式编程是以函数为基本单元,没有变量(更准确地说是不能重复赋值)也没有副作用的编程方式。但是从更深的层次上看,它彻底隔离了可变性,变量或者状态默认就是不可变的,如果要变化,则必须经过合理设计的专门机制。所以,它也避免了死锁、状态冲突等众多麻烦。

5、如何又要保证操作原子性又要能精确还原各时刻的状态

只提供CR操作,而不提供完整的CRUD操作(就像MySQL的binlog那样)。平时只要追加操作记录即可,各时刻的状态永远通过重放之前的操作记录得出,这样就彻底避免了状态的错乱。

二、内容在总结中

让你接手一套不稳定但要紧的在线系统,这套系统还有各种问题:变量命名非常随意,依赖逻辑错综复杂,层次结构乱七八糟,部署流程一塌糊涂,监控系统一片空白…… 你该怎么办?

前几年我就遇到了这种问题,我冒着频发的故障仔细观察,发现了最关键的问题:如果放着不动,这套系统的核心功能还是相对稳定的,但经常会有一些外围需求要开发,原有的依赖逻辑和层次结构不够清楚,就会“牵一发而动全身”,加上测试不完善,所以几乎每次外围功能上线更新,核心功能都会受影响,然后又要重复好几次“调试、改正、上线”的流程。 

怎么办?大家说了很多办法:开发态度更认真一些,把单元测试都补全,重构代码拆分核心功能和非核心功能,跟业务方谈暂停需求…… 这些办法都很对,但是,都需要时间才能见效,而我们最缺的就是时间。 

我提了个很“笨”的办法:把所有“共享变量”都抽到Redis中读写,消灭本地副本,然后把稳定版本程序多部署几份,就可以多启动几个实例,这些实例标记为AB两组。同时,在访问链路前部搭建代理服务用于分流请求,核心功能请求分配到A组(程序基本不更新),外围功能请求分配到B组(程序按业务需求更新)。这样看起来有点“多此一举”,AB两组都只有部分代码提供服务,而且要通过Redis共享状态,但是无论B组的程序如何更新,都不会影响到A组所承载的核心服务。 

虽然当时不少人疑惑“怎么能这样玩呢?”,但它确实有效。当天部署当天生效,在线服务迅速就稳定了,即便新开发的外围功能有问题,核心服务也不受任何影响。这样,业务人员满意了,开发人员也可以安心对系统做改造。 

后来有不少人问我:你怎么会想到这个办法呢?答案是:因为我是个老程序员,成长在面向对象的年代,SOC(关注点分离)、SRP(单一责任原则)、OCP(开放-闭合原则)这些东西对我来说,就好像刷牙洗脸一样是本能。具体到这个例子,无非就是识别关注点,隔离责任,保持核心关注点的封闭而已。 

后来我才知道,这办法有个专门的名字叫“蓝绿部署”。当然,我是个老程序员,不懂这些新鲜概念,我不太在乎。不过,如今不少程序员确实已经不认识SOC、SRP、OCP、LSP等等“古老”的玩意儿了,大家熟悉的是各种语言、类库、框架、代码托管网站。互联网开发场景千变万化,技术一日千里,而“面向对象”在不少人的脑海里,早就是弃之不用的老古董了。只有“老一辈”的老程序员,还记得那些古老的教诲,守着那些古拙的技巧。但是这些东西,总有一天会被时代淘汰吗?

实际上,这也是我初读Clean Architecture的疑惑。虽然Uncle Bob的名字对我们这些“老程序员”如雷贯耳,之前针对一般性软件开发的Clean Code和Clean Coder也确实很受欢迎,但如今写架构,还从结构化编程、面向对象编程、函数式编程写起,还花时间去解释SRP、OCP、LSP等等原则,实在难掩“古老”的感觉——拜托,它们和如今的“架构”有什么关系吗? 

不过如果你耐心读下去就会发现,还真有关系。

按照Uncle Bob的说法,所谓架构,就是“用最小的人力成本来满足构建和维护系统的需求”的设计行为。以前的面向对象系统,和如今的分布式系统,在这一点上是完全一致的。听取久远的教诲,尊重古老的智慧,如今的架构师也会从中受益的。

不信?我们就拿经典的三种编程范式来举例,看看这些“老掉牙”的玩意儿和如今的架构设计有什么关联。

结构化编程

一般理解,结构化编程是由if-else, switch-case之类的语句组织程序代码的方式,而杜绝了goto导致的混乱。但是从更深的层次上看,它也是一种设计范式,避免随意使用goto,使用if-else, switch-case之类控制语句和函数、子函数组织起来的程序代码,可以保证程序的结构是清楚的,自顶向下层层细化,消灭了杂错,杜绝了混淆。 

联系到如今的分布式系统,我们在设计的时候,真的能够做到自顶向下层层细化、结构清晰吗?有多少次,我看到的系统设计图里,根本没有“层次”的概念,各个模块没有一致的层次划分,“子系统交互的不是子系统,而是一盘散沙式的接口”,甚至接口之间随意互调、关系乱成一团麻的情况也时常出现,结果就是维护和调试的噩梦。

吹散历史的迷雾,不正是古老的goto陷阱的再现吗? 

面向对象编程

一般理解,面向对象编程是由封装、继承、多态三种特性支持的包含类、接口等等若干概念的编程方式。但是从更深的层次上看,它也是一种设计范式。多态大概算其中最“神奇”的特性了,程序员在确定接口时做好抽象,代码就可以很灵活,遇到新情况,新写一个实现就可以无缝对接。 

联系到如今的分布式系统,我们在设计的时候,真的能够做到接口足够抽象,新模块能无缝对接吗?有多少次,我看到接口的设计非常随意,接口不是基于行为而是基于特定场景的实现,没有做适当的抽象,也没有为未来预留空间,直接导致契约僵硬死板。新增一种终端呈现形式,整个内容生产流程就要大动干戈,这样的例子并不罕见。

许多人都熟悉那个经典的例子:三角形、圆形、正方形都是形状,都必须提供draw方法,只是各自实现不同。这个道理很容易讲通,但它对如今的开发真的有什么用吗?要我说,还真有用。比如某在线教学系统,有各种类型的学习计划,也使用分布式架构。但是不管这些学习计划分布在哪里,用什么语言实现的,都必须提供“完成百分比计算”的方法,虽然具体的计算逻辑各异。照这样做了,教学情况统计就简单异常,不照这样做,就会麻烦异常。

抹去历史的尘埃,这类设计背后的逻辑,不正是“多态”的思想吗? 

函数式编程

一般理解,函数式编程是以函数为基本单元没有变量(更准确地说是不能重复赋值)也没有副作用的编程方式。但是从更深的层次上看,它彻底隔离了可变性,变量或者状态默认就是不可变的,如果要变化,则必须经过合理设计的专门机制。所以,它也避免了死锁、状态冲突等众多麻烦。 

联系到如今的分布式系统,我们在设计的时候,真的能够彻底隔离可变性,避免状态冲突吗?有多少次,我看到状态或变量的修改接口大方暴露,被不经意(或者恶意)修改,导致奇怪的故障。Uncle Bob举了个相当有趣的例子,如果又要保证操作原子性又要能精确还原各时刻的状态,有个办法是这样的:只提供CR操作,而不提供完整的CRUD操作(就像MySQL的binlog那样)。平时只要追加操作记录即可,各时刻的状态永远通过重放之前的操作记录得出,这样就彻底避免了状态的错乱。这个办法看起来古怪,但我真的在之前的开发中用过(当然是在程序生命周期有限的场景下),而且真的从没出过错。 

坦白说,看完Uncle Bob的书,我心里好过点儿了。因为我发现,我们这些这些老程序员的知识其实没有过时,如今不少光鲜的架构,其实骨子里还是那些古老的问题。只是多亏了Uncle Bob的妙手点拨,我才能穿越时空,享受到“重新发现智慧”的愉悦。 

当然,架构设计是一门复杂的学问,要综合考虑编码、质量、部署、发布、运维、排障、升级等等各种因素,作出权衡。好消息是,Uncle Bob的这本书覆盖广泛,各方面都有涉及,认真读完全书一定会有不小的收获。唯一的问题是,你要适应这个老程序员的口味和节奏:他当然也会拿如今流行的打车系统做例子,但他更熟悉的,还是链接器、C语言、UML图等等。这些玩意儿对如今的程序员来说,确实透出一股“古老”的感觉。

不过我觉得,这都不是大问题。看得出类之间的依赖关系不合理,自然容易发现子系统之间的依赖不合理;搞得懂Unix如何巧妙定义通用的IO设备,自然容易想到对PC Web、Mobile Web、App内页面做合适抽象;认得清各线程、进程、链接库的职责,自然容易明白微服务也需要避免跨边界调用。更妙的是,掌握这种古老的视角,往往更能摆脱细节的困扰,把握问题的核心。就像老子说的那样:治大国如烹小鲜。 

噢,对了,“治大国如烹小鲜”,这也是久远的教诲,也包含着古老的智慧。

参考:余晟以为
https://mp.weixin.qq.com/s?__biz=MzA3MDMwOTcwMg==&mid=2650005790&idx=1&sn=ca7fda1a8d135f9ae72bc21bad486cc5&chksm=87398337b04e0a213a0a135f5e054dcff62489f54482313d08d3782c1138be9e2530d0a2588e&mpshare=1&scene=2&srcid=0926kF5emlsGHp7mcTiAeTfw&from=timeline&ascene=2&devicetype=android-24&version=2607028a&nettype=WIFI&abtest_cookie=BAABAAoACwASABMABAAjlx4AVpkeAGiZHgBsmR4AAAA%3D&lang=zh_CN&pass_ticket=SeYJO7A9kINb8J4hCWz73oxXinbpnRc%2BHpvbLaHPRIMCSlwvbyko%2BCBMqpFeeeVe&wx_header=1

 
原文地址:https://www.cnblogs.com/Renyi-Fan/p/9712424.html