计算机编程领域的三十六种基本思想概览

运用之妙,存乎一心。

引子

计算机编程领域的基本思想,是大量实践与经验的提炼总结,是近乎于“道”的指南。有了思想的指引,就如同有高人指路,行不迷惑,遇事有法,运用之妙,存乎一心。

本文盘点计算机编程领域的三十六种基本思想。


  • 编程
  • 结构化
  • 关注点分离
  • 抽象
  • 封装
  • 复用
  • 分治
  • 组合
  • 缓存
  • 解耦
  • 编码
  • 协议
  • 容错
  • 自动化
  • 预处理
  • 时空权衡
  • 统筹规划
  • 递归
  • 索引
  • 迭代
  • 遍历
  • 中断
  • 回滚
  • 模板
  • 模式
  • 代理
  • 管道
  • 并发
  • 并行
  • 批量
  • 异步
  • 回调
  • 延迟
  • 定时
  • 通知
  • 阻塞

编程思想

第一梯队思想

第一梯队思想,是计算机编程领域最为核心而根本的思想。因其威力强大,且无处不在,简直是天神般的存在。


编程

编程没什么神秘的。本质上,编程与写作、绘画、雕塑一样,也是一种手艺,一种探索和求解问题的方式。只不过,编程高度依赖于算法步骤的正确性和可行性,需要高度的理性。

理性有三种类型:逻辑理性、沟通理性和思想理性。编程属于逻辑理性。逻辑理性要求逻辑层次是缜密无矛盾的,不能有一丝纰漏,否则可能会导致全盘溃败。逻辑理性是一层层堆叠起来的。上层的缜密依赖于下层的缜密。

构建逻辑理性的缜密性:

  • 原则与思想:从根本上构建行为一致性。
  • 约定与惯例:减少多变环境下的管理。
  • 推导与演绎:即给定若干条件及事实,能够正确推导出深入而广泛的结论。推导演绎能力是逻辑理性的生动体现和重要特征。

可阅: “编程漫谈(十一): 编程概要”

结构化

结构化是编程领域的第一性原理。

软件,本质上是一种可动态而弹性变化的逻辑装置。结构化,即是将逻辑进行抽象、提炼、分离、组合,构建成缜密、动态、弹性的结构流。

可阅:“软件设计的第一性原理:结构化抽象”

关注点分离

关注点分离,是有序组织大规模逻辑结构的根本性思想。

软件中蕴藏着不计其数的大大小小的关注点。软件开发和设计的本质,就是将关注点分离、组织、连接。 能够将不同的关注点分离开,再合理有序地组织起来,呈现在代码里,就离写出清晰可维护的代码不远了。

从“关注点分离”思想,可以推导出很多重要的软件开发和设计思想。

可阅: "The Art of Separation of Concerns"

抽象

优雅的编程始于抽象。

萃取出主要特征,而摒弃次要或不相关的特征;无需了解事物的内部实现细节而基于其提供的抽象来构造应用。声明与实现相分离;语义与细节分离。

计算机科学中的抽象俯拾即是,比如指令集是对机器硬件执行能力的基础抽象,高级程序语言是对机器硬件执行能力的更高层次的抽象,进程是对程序一次执行的抽象等。

可阅:“编程漫谈(三):抽象”“代码抽象与分层”

封装

由抽象直接引出的重要概念就是封装。封装是抽象的实现形式。比如,函数是算法的封装,对象是属性与关联逻辑的封装。封装是实现软件模块化、提高软件可维护性的重要思想,是许多软件工程思想的源头。比如隔离变化, 将变化的影响局部化等。封装在计算机软件领域内俯拾即是。比如系统调用,是对操作系统能力的封装; SDK 是利用某种语言或技术进行开发的封装;框架,是对通用技术逻辑流程的封装。

日常生活中,封装的例子也不计其数。任何一个产品设计,都是一种复杂事物的封装。举个例子,汽车方向盘和刹车油门就是汽车驾驶的封装。几乎不需要知道汽车的具体运转原理,只要掌握好方向盘和刹车油门就能驾驶汽车;比如火箭发射,不需要知道火箭的发射原理,只要知道按下哪个按钮即可。

可阅: “软件开发:如何表达和维护大型逻辑”

复用

没有封装就没有复用。复用是微小的编程能够构建起虚拟大千世界的最重要的秘诀。先构建一个小块,然后在这个小块上构建更大的块,就像建造房子一样。从泥到砖,从砖到墙,从墙到房子四围。最典型的例子就是积木。积木是可复用的组块,在积木的基础上可以建造各种各样的形状。

复用是软件工程领域的重要思想。从 LinuxShell、标准库函数,STL,JDK这样的代码级复用,到 Struts,Spring 的应用框架复用, 以及 设计模式的复用, 解决方案的复用, 复用无处不在。 复用是站在巨人的肩膀上, 能够直接利用专家级的知识和经验,何乐而不为?

新的更优的解决方案往往是现有可复用方案的组合创新。

可阅: “代码可复用性问题兼谈团队协作” 

分治

分治几乎是求解编程问题的万能思路。难以求解的问题,都可以分解成若干个可以在能力范围内解决的子问题;如果还不能求解,继续分解。自顶向下,逻辑树法,都是分治法的典型应用。

自顶向下,即是根据目标先分解成一系列的子目标,然后再根据这些子目标进一步分解成子子目标,就像公司的组织结构一样。直到子子目标可以被完成。自顶向下法可用于系统设计、任务分解等。

逻辑树,即是先根据一个论点,引出若干个子论点,然后每个子论点又可以引出若干个子子论点,每个子子论点都可以由相应的论据支撑,最终形成缜密的论证逻辑。逻辑树法可用于工作汇报、数学证明等。

实际工程应用中,分表、分库、分区、分片、微服务,都是分治思想的应用。可以按时间、冷热、业务ID、不同业务分。可以按范围分,也可以哈希映射。

可阅: “混合使用ForkJoin+Actor+Future实现一千万个不重复整数的排序(Scala示例)”

组合

有分即有合。单一逻辑单元能够做的事情很有限,但是多个逻辑单元组合起来就能完成很强大的功能。就像一个人的力量有限,但如果多个人的力量团结起来,就不可估量了。

函数式编程,即是充分利用短小函数及各种组合方式,构建出强大的功能。

组合是构建强大功能的最重要的方式之一。

可阅:“软件的组合结构:从指令到软件”“"完全"函数式编程”

缓存

缓存是性能加速的重要法宝。在计算机体系结构里,缓存的身影无处不在。

  • CPU 三级缓存
  • 存储器层次体系结构
  • 内存缓存/分布式缓存
  • 预编译缓存
  • 数据库缓存
  • 浏览器缓存
  • CDN缓存
  • 代理缓存
  • 域名系统
  • 计算结果缓存

可阅: “【整理】互联网服务端技术体系:高性能之缓存面面观”

解耦

解耦是软件应对多变需求和软件复杂性的重要思想。如果缺乏解耦,软件就会缠绕成一团,无法解开,难以修改,很容易出错。

现代软件工程与技术,实际上就是在各个层次上进行解耦。

  • 高级语言与编译器:将编程语义与实际机器执行实现解耦
  • 开闭原则:将基于同一数据源的不同业务的处理解耦
  • 插件架构:将基于同一系统的不同功能的定制解耦
  • 设计模式:将对象的职责与交互解耦
  • 事件与队列机制:将事件的发生与处理解耦;将消息的发送与接收处理解耦
  • 框架: 将应用业务逻辑与通用技术逻辑流程解耦
  • JVM: 将指令执行与操作系统平台解耦
  • 容器: 将应用运行环境与操作系统平台解耦
  • K8S: 将应用的部署与基础设施和资源利用解耦

解耦思想直接带来了软件工程领域的“高内聚、低耦合”的软件模块化实践。模块化实践大幅降低了软件长期维护的复杂性。

可阅: “软件工程中的耦合与解耦方式”


编码

上述思想都有点”玄虚“,但编码却是实实在在的实践和基本编程思想。

要写程序,必定要将现实中的实体和活动编码成 01 串,进行处理,最后再解码还原到现实世界。编码是指将现实中的万事万物编码成01串的操作。神乎其技兮!

当然,现代编程并不是要编码成 0,1 串,而是使用高级编程语言来编码。因为高级编程语言是 0,1 串编码的封装。

精通编码,就能洞察程序世界,如镜透明。

协议

计算机不是孤立地运行,人亦非孤岛般存在。让计算机能够彼此通信,让人们能够跨时空互联深入交流,是互联网的梦想。协议是网络程序相互协作的必不可少的要件和基石。协议解决计算机程序如何可靠通信的问题。

比如一次5个人的聚会吧,首先大家肯定要事先约定什么时候什么地点碰头,好各自安排行程;这是静态协议; 不过计划总赶不上变化。当实际去赴会时,会发现因为某位明星的到来导致路上特堵,结果2个人不能如约到达;这时候,就必须重新调整计划另约时间和地点,甚至还可能改变原来的游玩计划;或者在原计划中留下充分的缓冲余地。这就是协议要做的事情:约定与标准(碰头时间地点)、缓冲余地(容错)、动态调整(灵活性)。实际的通信协议可能更复杂,依据不同的应用而设定,不过原理是相同的。

协议是互联网通信的基本软件思想。最为人所熟知的莫过于 HTTP(S), TCP 和 IP 协议了。

可阅: “谁应该"吃"掉它”

容错

程序运行过程中,会遇到各种不可预料的情况,产生多姿多彩的错误。比如读文件,文件不存在、找不到、格式被破坏、有恶意代码;获取网络数据,网络波动或者中断;大流量下,扛不住压力而崩溃;甚至于直接掉电。

容错是设计健壮程序的不可或缺的重要考量因素。容错涉及到用户体验问题。试想,一个程序动不动就报错,谁能有耐心使用呢?

可阅: “【整理】互联网服务端技术体系:容错机制”

自动化

自动化使得人从繁琐易错的事情中解脱出来, 从事更具有创造性的工作,  使人与计算机并行协作, 在计算机干活的时候人休息一会也不耽搁事情。善于发现可自动化的流程、规律、事务, 尽可能使用程序自动化和简化手工活, 减少或消除繁琐易错的手工操作。

高效可靠的自动化几乎是程序(员)存在的最主要的使命和价值。

可阅:“VBA 操作 Excel 生成日期及星期”“订单导出的预发和线上的自动化对比工具”

预处理

预处理,是指先对待处理数据做一波前期的处理,转换结构,然后再对转换后的结构进行操作,起到加速或方便处理的作用。

比如要多次查询一个无序列表里的元素,可以先把无需列表转成一个映射,再进行查询。这样多次查询的效率就会很高。

比如 Rete 算法,即是将一系列规则或模式,转换成一个拓扑图结构,然后再进行匹配。这样就比用原始结构的匹配更容易效率更高。

预处理有一定的前期开销,但能够为后续处理提供很大的便利或效率价值。

可阅:“构造与使用分离:命中内容高亮及合并的展示问题解决实现”

时空权衡

时间换空间,或者空间换时间。

空间换时间,是提升性能的常用手段。比如缓存,就是用冗余的空间来换取快速的处理。

时间换空间,是解决内存不足的常用手段。比如要处理大量数据,不会一次性将所有数据都加载到内存里,而是分批次加载和处理,这样小内存也能解决大数据集,代价是处理时间会更长一些(因为需要更多次的加载)。


统筹规划

统筹规划思想,注重在全局视角下解决问题。

找到关键路径和节点,解决这些路径和节点的问题,往往会加速整体流程的效率。

不在次要路径和节点上耗时耗力。

统筹规划的典型应用是项目路径规划,最短路径、资源配置优化。

第二梯队思想

第二梯队思想,虽然不如第一梯队思想那么通用广泛,却也是编程领域的重要思想,有着举足轻重的地位。

递归

一个规模为N的问题的解可以由规模为S(S<=N)的同样性质的问题的解来构造。

举个简单的例子,1+2+3+4+5 = 1 + (2+3+4+5) = 1 + (2 + (3+4+5)) = 1 + (2 + (3 + (4+5))) = 1 + (2 + (3 + (4+(5))))。 N 个数的和等于第 N 个数加上 N-1 个数的和。

递归技术是一种非常有效的程序设计技术。很多数据结构都有递归特性。列表、字符串、二叉树、JSON等。

可阅: “一道关于二叉树的字节面试题的思考” 

索引

索引类似于书的目录。要定位某篇文章,只要按目录页码直接翻到对应页码即可,而无需一页一页地翻看和查找。

通过某种算法对数据项标识和设置索引值,从而能够按照索引值快速定位相应数据,提升查找效率。

索引是海量数据查询的技术基础,而海量数据查询是现代互联网应用的基石。

可阅: “【整理】互联网服务端技术体系:高性能之数据库索引” 

迭代

迭代是使用固定的计算规则集合不断用新值取代旧值趋向真实值的控制结构。比如牛顿迭代法求N的平方根 X(k+1) = (X(k) + N/X(k))/2 (k>=0) 就是一个迭代过程。可以指定初始值和终结迭代过程的迭代次数。迭代的重要指标是收敛性和收敛速度。

TDD 是一种迭代的测试开发方法论。先写好一个测试用例,运行测试用例,亮红灯,然后编程实现,再运行测试用例,亮绿灯。如此,一个测试用例就通过了。反复如此,直到所有测试用例都通过。迭代使得可控地趋于期望的目标。

可阅:“测试驱动开发笔记【初学者】”

遍历

遍历是从结构中的某个初始节点出发,使用算法访问结构中的所有节点。遍历有深度优先遍历和广度优先遍历。

深度遍历是一直往一个方向走(一条胡同走到底),先遍历根节点的第一个子节点,然后是第一个子节点的第一个子节点,如此递归,直到没有子节点,然后回退到上一个可选择路径的节点,遍历其下的第二个子节点,接着第二个子节点的第一个子节点,递归,如此,其依次直至所有节点都访问完毕。

广度遍历是先遍历根节点的所有第一层子节点,遍历完成后,从第一层的第一个子节点开始,遍历该节点下的第二层的所有节点;然后再从第一层的第二个子节点,遍历该节点下的第二层的所有节点;如此,其依次直至所有节点都访问完毕。

可阅: “图文详解深度优先,广度优先遍历”


中断

当你正投入工作状态的时候,领导发话了:开会开会! 于是你不得不放下手头“心爱”的事情,跑去听一段@#¥@#%@¥%@%#¥%#的讲话,讲话回来后再以莫名的心绪重新干活。当然,人是有记忆的,当你去开会前,实际上已经记忆了当时做的事情的一些重要细节和进程,这样在回来时就能从这些细节和进程逐渐恢复到当时状态继续干活,就像讲话似乎没发生过一样。这就是中断:做某件事,更高优先级事情插入,保存现场,完成更高优先级的事情,恢复现场,继续做原来的事情。

中断是计算机心脏 CPU 来应对系统内外各种事件的机制。当发生某种事件时,产生一个信号中断,CPU 会保存现场,去执行中断处理程序;待中断处理程序执行完成后,就会恢复现场,继续执行之前的查询。

中断提供了有效应对系统中各种事件、使得系统能够正常运转的重要机制。同时,它也是一种重要思想:保存现场,做更高优先级事情, 恢复现场。

可阅:“中断”

回滚

回滚是计算机执行发生错误时,希望能够回到某个保存点,以便下次能够接续执行。

回滚提供了一种可靠的中途接续执行机制。 事务机制是回滚思想的重要应用。

模板

通过替换静态模板中的变量, 从而生成动态文本的思想和技术。

如果多个输出都具有相似的格式或流程,就可以定义模板,通过模板与具体数据的结合,来实现具体的输出。

比如结婚请柬就是典型大的模板应用。请柬上的大部分内容是相同的,只有被邀请的人是不一样的,这部分是空出待填充的。

可阅: “Java实现简单的格式化信函生成器”“输入输出无依赖型函数的GroovySpock单测模板的自动生成工具”

模式

模式,俗称套路,是已有经验的提炼和总结。适当借助模式,能够有效地复用已有经验和实践,高效处理问题,避免重复踩坑。

模式,有编程模式、设计模式、分析模式、架构模式、业务模式等。善于发现模式、使用模式,是站在专家的肩膀上,能够看得更远。模式,即是学会汲取别人的丰富经验。

用模式“斩断”日常问题后,才能腾出更多精力做有创造性的事情。

可阅:“软件设计要素初探:基础设计模式概览”

代理

代理,顾名思义,就是代替某物做某事。

代理通常作为”中间人“,起着缓存、保护、延迟加载、负载均衡等作用。

第三梯队思想

第三梯队思想,是程序执行流程的高层构建块,能够用来构建灵活而强大的指令流程。

管道

管道是类 Unix 系统的经典发明。管道将左边程序的输出传给右边程序的输入,可以轻易将连接的多个实用程序组成一条流水线,实现强大而多样化的功能(小程序、微功能)。

比如如下命令:在当前目录及子目录下的所有 .java 文件中查找匹配模式 pattern 的字符串,显示文件名,行号及匹配的行内容。


find . -name '*.java' | xargs awk ' $0 ~ /pattern/ { printf "%s : Line %s :\n%s \n" , FILENAME, FNR, $0 } ' | sed 's/^[[:space:]]\{1,\}/ /'


并发

并发是同一个执行单元在一段时间内交替做多件事情。

比如一个人一边烧水,一遍看电视,一遍督促孩子做作业。看上去就像是在同一时间段内同时做了多件事情。由于某一项需要等待,往往在多项任务之间不断切换(保留现场和恢复现场)。切换会耗费一定的时间。

并发是利用多核的性能提升手段。事实上,并发也是世界运行的本质。这个世界正是由不计其数的人并发地去进行自己的活动所构建起来的。

可阅:“对象与并发:概述”“【整理】互联网服务端技术体系:高性能之并发(Java)”

并行

并行是多个互相独立的执行单元在同一段时间相互独立地做不同事情。

比如一个公司里,产品在理需求,研发在做技术调研,两个人都是朝同一目标前进,但做着不同的事情。

并行是并发的一种更严格的形式。

批量

涉及网络传输时,单个单个去获取数据和处理数据往往会比较慢,因为传输的次数增多,需要反复建立和销毁连接,会耗费不必要的连接建立时间和传输时间,尤其是连接建立成本、传输成本与处理成本相当甚至更大的情形。

比如传输一次需要 10s, 处理一个需要 5s。如果单个处理,那么处理 10 个需要 (10+5)10 = 150s;而如果一次批量获取10个,然后批量处理,则只需要 10 + 510 = 60s。 中间 90s 的传输成本都是不必要的。因此大量数据的获取和处理,往往是批量方式处理的。

比如运货,通常是一整箱一整箱地运送,而不是一个个的运送。

批量是提升大量数据处理的效率的常用手段。

可阅: “批量下载网站图片的Python实用小工具” 

异步

异步最初是为了解决响应的体验问题。在提交后台任务执行的同时,给前端先返回一个消息,让用户能够有所感知,而不是百无聊赖的等待。异步,多用于解耦后台耗时任务与前台展示。

多路 IO 复用,是异步方式解决高并发问题的技术基础。

可阅: “使用 jsPlumb 绘制拓扑图 —— 异步加载与绘制的实现”

回调

回调有点类似于埋点。指定做完一件事之后,要去处理一段特定逻辑,而这段特定逻辑可能是不同的。这段特定逻辑就称为回调逻辑。

回调能够让函数的功能更加灵活,是函数式编程的重要特征之一。回调的函数参数通常是函数指针。

可阅: “编程模式: 回调”“响应式编程库RxJava初探”

延迟

亦称惰性加载。仅在必要时才去加载、访问和计算。

通常用于创建需要耗费资源较大的对象,或者将访问和计算延迟到必要的时候才去进行,以减少不必要的消耗。

比如说,举办一次会议,仅在会前一天或几个小时,才有必要在会场去”铺设“饮用矿泉水。因为如果提前去铺设,万一会议取消了,或者转换场地了,那么这些努力(连带时间和精力)就都白费了;比如说颁奖,只有在需要颁奖的时候,才让获奖人上台领奖,而不是从一开始就在台前等着。

延迟加载的常见使用场景:

  • 大图片延迟加载。浏览网站时,往往会先展示图片的缩略图,点击之后才会展示原图,这即是一种延迟加载策略。

  • Stream 流计算。 从 Stream 流读取数据,可以进行 filter, map,flatmap 等各种操作,但只有在 collect 的时候才进行计算。

  • 初始化大对象。 对于创建开销高昂的对象,仅在需要的时候才去创建。

可阅: “Java8Stream函数式编程探秘”

定时

定时是在指定的时间周期性地做一件事。

软件系统中充满了各种定时任务。比如闹钟就是一个定时任务。

可阅:“定时任务的实现原理”

通知

与定时的指定时间不同,通知,是在所关注的事情发生变化时,发送消息通知,以便做相应处理。

比如订阅了一个公众号,公众号在发布文章时,就会给你推送一个消息。

通知往往是基于事件驱动机制或订阅推送机制来实现的。

可阅:“设计模式之观察者模式:实现配置更新实时推送”

阻塞

我们自然希望程序能够时时刻刻流畅无阻地永远运行下去,但人类的思维能力是有限的。面对现实的复杂多变,很难做到如此完美。

阻塞,也是一种重要的编程思想。阻塞用于两种情形:

  • 适当阻塞,可以消减大规模程序的快速运转状况下的时间差(这种时间差是人很难消除和预料的)导致的 BUG,让程序行为容易推断;

  • 程序在大流量的侵袭下,适当阻塞,能够让程序免于瞬间崩溃,有效利用系统资源。

当然,程序阻塞也可以实现让客户持续付费 😃

排队机制,也是一种阻塞实现。当数据量超过系统能力承载范围时,让数据先入队等待,待系统处理有余力时再出队处理。“生产者-消费者”模式就是一种经典的阻塞并发模式。

阻塞有利有弊。可控地阻塞时,可以让系统有节奏地处理; 重度阻塞时,就可能会导致系统运行进一步恶化,最终导致崩溃。不可不防。


小结

计算机编程领域日新月异,新技术层出不穷,却是由若干个简单的思想交织而成。这些思想,犹如一颗颗珍珠。理解这些基本思想,就能构建牢固的编程思想体系,更容易理解和汲取各种技术机制和技术知识,在做设计和求解问题时也有基本的思路。

技术易变,思想恒久。

posted @ 2022-09-07 07:31  琴水玉  阅读(1495)  评论(2编辑  收藏  举报