一周重温架构简洁之道:编程范式的特性与认知【玩转架构】
背景
前段时间我整理了一篇开发设计文档的经验——《磨刀不误砍柴工,分享编写前端技术设计文档的二三经验》,做为对于2023年的收尾。
2024年1月,一年之初,正是立Flag的好时机。因为我今年有几本小说作品的计划,所以被分去了一部分写作精力。有限的精力,想要发挥更高的效率,还是需要一些策略,于是我想到了可以借鉴一下总结开发设计文档的经验。
每月中的某一周阅读一本技术图书,然后再用一周时间产出技术收获。这样读书写作两不误,我便不没有精力有限的顾虑了。
本月的阅读计划是:《架构简洁之道》。距离我上次阅读这本书,已经过去很长时间了,除了标题,内容已经记不太清了。
子曰:“温故而知新。”我十分期待带着点“历尽千帆”的心境去重新阅读这本书而获得的收获。
文章速读
本篇主要分享关于编程范式的知识内容:
- 什么是编程范式?
- 编程范式的用途。
- 三个编程范式:结构化编程、面向对象编程、函数式编程,它们的特征及应用。
编程范式概览
编程范式(paradigm)
编程范式,指程序的编写模式。它是一类典型的编程风格,最主要的用途:告诉开发者在什么时候采用什么样的代码结构。
我们先来明确一个目标,一个好的软件设计的终极目标:
使用最小的人力成本来满足构建和维护该软件的需求。
满足软件需求所需的成本,是一个软件架构的衡量标准。而这个标准的基础构件便是编程范式,因为任何软件架构的实现都是从第一行代码开始的。
开始第一行代码开始之前,开发者已经考虑用哪种编程范式了。
都有哪些编程范式呢?
结构化编程
作为第一个普遍被采用的编程范式,结构化编程是由 Edsger Wybe Dijkstra 于1968年最先提出。
结构化编程对程序控制权的直接转移进行了限制和规范。
基本结构
- 顺序结构:该结构表示程序中的各操作是按照它们出现的先后顺序执行的。
- 分支结构:该结构表示程序的处理步骤出现了分支,它需要根据某一特定的条件选择其中的一个分支执行。选择结构有单选择、双选择和多选择三种形式。
- 循环结构:该结构表示程序反复执行某个或某些操作,直到某条件为假(或为真)时才可终止循环。循环结构的基本形式有两种:当型循环和直到型循环。
原则
结构化程序设计采用自顶向下、逐步求精的设计方法,各个模块通过“顺序、选择、循环”的控制结构进行连接,并且只有一个入口、一个出口。
价值
结构化编程范式中最有价值之处是:赋予了使用者创造可证伪程序单元的能力。
两个为什么
结合结构化编程范式的价值以及 Dijkstra 的研究结论,可以很好理解两个为什么:
- 为什么现代编程语言一般不支持无限制的 goto 语句。
- 为什么在架构设计领域,功能性降解拆分仍然是最佳实践之一。
1、为什么现代编程语言一般不支持无限制的 goto 语句
Dijkstra 在研究过程中发现:goto 语句的某些用法会导致某个模块无法被递归拆分成更小的、可证明的单元,这会导致无法采用分解法来将大型问题进一步拆分成更小的、可证明的部分。
Dijkstra 意识到 goto 的实际效果其实和更简单的分支结构(if-then-else)以及循环结构(do-while)是一致的。如果代码中只采用了这两类控制结构,则一定可以将程序分解成更小的、可证明的单元。
人们可以用顺序结构、分支结构、循环结构这三种结构构造出任何程序。这三种结构便是结构化编程的基本结构。
于是,随着编程语言的演进,goto 语句的重要性越来越小,最终甚至消失了。如今大部分的现代编程语言中都已经没有了 goto 语句。
2、为什么在架构设计领域,功能性降解拆分仍然是最佳实践之一
结构化编程范式可将模块递归降解拆分为可推导的单元,这意味着模块也可以按功能进行降解拆分。
模块的降解拆分促成了一项很重要的工作:将一个大型问题拆分为一系列高级函数的组合,然后再将这些高级函数各自拆分为一系列低级函数。
每个被拆分出来的函数都是可以用结构化编程范式来书写。
这样一来就促成了程序员进行"将大型系统设计拆分成模块和组件,然后将这些模块和组件拆分为更小的、可证明的函数"的设计实践。
面向对象编程
作为第二个被普遍采用的范式编程,面向对象编程是在1966年由Ole Johan Dahl和Kriste Nygaard在论文中总结归纳出来的。
面向对象编程对程序控制权的间接转移进行了限制和规范。它有三个特性:封装、继承、多态。
封装
面向对象编程中,可以通过采用封装特性,把一组相关联的数据和函数圈起来,使圈外面的代码只能看见部分函数,数据则完全不可见。
在 JavaScript 中,封装对象的方法有很多,其中常见的是class语法。
class User {
constructor(name) {
this.name = name;
}
getName() {
return '用户名称:' + this.name;
}
}
上面代码中 getName() 函数是无法直接使用的。直接调用会报错:
getName();
>>>eferenceError: getName is not defined
需要先生成类的实例,然后通过实例调用 getName() 函数。
var userName = new User('李雷');
console.log(userName.getName());
>>>用户名称:李雷
继承
面向对象编程中,继承的主要用途是让程序员可以在某个作用域内对外部定义的某一组变量与函数进行覆盖。
在 JavaScript 中,Class 可以通过 extends 关键字实现继承,让子类继承父类的属性和方法。
class User {
constructor(name) {
this.name = name;
}
getName() {
return '用户名称是' + this.name;
}
}
class OrderUser extends User {
constructor(name, order) {
super(name);
this.order = order;
}
getOrderInfo() {
return this.order + '的' + super.getName(); // 调用父类的getName()
}
}
var userName = new OrderUser('李雷', '订单12581');
在上面的代码中子类 OrderUser 继承了父类 User 的 getName 方法,所以可以进行调用操作。
console.log(userName.getOrderInfo());
>>>订单12581的用户名称是李雷
多态
多态是类型理论中的一个概念,一个名称可以表示很多不同类的对象,这些类和一个共同超类有关。
面向对象编程语言虽然在多态上并没有理论创新,多态的强大性,使面向对象编程能够以多态为手段来对源代码中的依赖关系进行控制的能力。而这种能力让软件架构师可以构建出某种插件式架构,让高层策略性组件与底层实现性组件相分离,底层组件可以被编译成插件,实现独立于高层组件的开发和部署。
函数式编程
函数式编程是第三个被普遍采用的范式编程,它是基于与阿兰·图灵同时代的数学家Alonzo Church在1936年发明的λ演算的直接衍生物。
函数式编程对程序中的赋值进行了限制和规范。
不可变性
函数式编程语言中的变量是不可变的。
可变变量会涉及到竞争问题、死锁问题、并发更新问题等问题。如果变量不可被更改的,那么就不会有竞争或并发更新问题。如果锁状态是不可变的,那就不会有死锁问题。
所以软件架构师们对变量可变性的高度关心,也是为了避免前面提到的问题。
可变性的隔离
不可变性的可行性方案之一,便是将应用程序或者是应用程序的内部服务进行切分,区分成可变的和不可变的两种组件。
其中,不可变组件用纯函数的方式来执行任务,任务期间不更改任何状态。
另外,需要采用合适的机制来保护可变量。
总结
我们来看下《架构简洁之道》中对于三种编程范式的精辟总结:
- 结构化编程是对程序控制权的直接转移的限制。
- 面向对象编程是对程序控制权的间接转移的限制。
- 函数式编程是对程序中赋值操作的限制。
此外,我们可以有以下清晰的认知:
- 每个编程范式都是对于新的能力的增加,而是某种编写代码的约束方式。即编程中程序员什么时候采用什么样的代码结构。
- 软件/程序可以由顺序结构、分支结构、循环结构和间接转移这几种行为组合而成的,不多不少,缺一不可。
作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)