xuzhengfu / pilot

进入编程世界的第一课
1 stars 0 forks source link

p1-7-oo-1 第七章 理解对象与类:起源篇 #25

Open xuzhengfu opened 4 years ago

xuzhengfu commented 4 years ago

1. 了解 对象和类 概念的基本背景

在现代编程语言中,对象和类是极其常见的概念,绝大部分现代编程语言都或多或少的支持 对象和类 的概念,Python 也不例外。

对象和类 出自于 “面向对象(object-oriented, OO)” 这一经典的抽象模型。

后面我们还会介绍近年流行起来(其实渊源比面向对象更久远)的“函数式(functional)”抽象,这些都是软件编程里的思维方法和设计方法,也就是说,并不是一些特定语言或技术,而是一类泛用的方法论(methodology),他们有个共同的高大上名字叫 “范型(paradigm)”。

要回答这些问题,就要从软件开发的根本困难说起。

2. 了解 软件开发的根本困难

先给结论:软件开发的根本困难在于管理软件系统的复杂度。

软件系统的复杂度包括物理上的规模,比如包含多少个源文件,总共多少行源代码;也包含逻辑上的规模,比如有多少子系统,多少模块,多少个功能点,多少个用户界面等;还包含开发和维护系统的人的规模,一个人开发和维护的系统,比一百个人开发和维护的系统要简单多了。

计算机软件的本质是人类教计算机干活的一系列指令,如果这些指令错了计算机肯定干不对,有时候甚至会干出可怕的后果(比如波音 737 MAX 型客机连续出现空难的根源就是其自动控制系统中存在软件缺陷)。软件系统的复杂度上去之后,就会带来一系列问题,核心就是软件开发者不能简单清晰的知道 软件代码到底是如何在计算机中执行的,那么也就更加无法保证软件在各种情况下的正确性了。

如果我们的软件只是打印一句 “Hello world”,那什么也不用讲究,因为它的复杂度几乎为零,不过现实中真正有用的软件往往有着不低的复杂度,动辄成千上万行源代码,要很多人很多年才能做出来。对初学者甚至不需要那么大规模,有一百行源代码就晕了。

其实让计算机在显示器上打印出一句话的复杂度是很高的,只不过大量的工作被操作系统和编程语言工具(比如编译器)做了,如果我们界定那些软件都是可靠的,那么就可以说 Hello World 本身复杂度很低。这恰恰是软件开发的核心理念之一 “化整为零、责任分离” 的例子。

3. 了解 软件危机

我们多次说过,计算机软件的本质是人类教计算机干活的一系列指令,在人类有计算机的初期,软件真的就是一大堆给计算机的指令的列表,比如这样的:

  1. 从某个地方读一个数 a;
  2. 从另一个地方读一个数 b;
  3. 把 a 和 b 加起来;
  4. 在终端上打印结果;
  5. 结束。

但这样平铺直叙只能做很简单的事情,稍微复杂的事情就需要借助“条件判断”才能实现,比如这样:

  1. 从某个地方读一个数 a;
  2. 从另一个地方读一个数 b;
  3. 如果 b 是 0 或者 正数,执行第 4 步,否则执行第 5 步;
  4. 计算 a + b;跳到第 6 步;
  5. 计算 a - b;
  6. 在终端上打印结果;
  7. 结束。

这里就有了条件判断、分支和跳转,注意这里的分支跳转是根据输入的数决定的,而这些数是在软件运行时才确定的,开发的时候程序员并不确定程序的实际执行顺序。这还只是一个非常简单的例子,现实世界的软件是由成千上万这样的代码叠加构成的,其结构和执行路径简直就是“一大锅意大利面”。在那个年代,随着计算机硬件不断提升,软件做的事越来越复杂,于是出现了大批一直完不成的软件项目(一直出错甚至一直没法完整运转起来),软件开发的先行者们发明了一个词“软件危机(software crisis)”来形容这种窘境。

“软件危机” 这个看上去像好莱坞大片或者游戏大作的词来自北约组织,想想也不意外,那个时候只有政府和军事机构用得起计算机。

软件危机促进了软件开发的第一次革命,人们把 工程化方法 应用在软件开发领域,并建立了一系列重要的方法论体系来应对软件危机,这其中最重要的两个分别是 “软件质量管理(software quality management, SQM)” 和 “结构化编程(procedural programming)”。前者把 文档化、软件测试 等质量管理方法引入软件开发的完整生命周期中(从需求产生到开发、上线、维护直至被废弃);而后者则引入了软件开发 “模块化(modularity)” 的重要概念,这个概念现在也未过时(可预见的将来也不会),其后新的思想和方法都可以看做是它的发扬光大。

4. 了解 模块化

模块化是结构化编程的核心理念,其实很简单,就是 “化整为零、分而治之” 的古老智慧的编程版本。既然大型软件系统的复杂度不可避免,那我们就通过 “分解” 和 “组装” 来简化。

分解:大型系统分解成中型,中型分解成小型,小型系统分解成一个个子系统和模块,模块分解成若干代码段,最后这些代码段都足够简单,对给定输入给出可预期的输出,易于描述、易于实现、易于测试,这样的代码段通称过程(procedure)或者 “函数(function)”,也有各种其他称谓,本质相同;

组装:通过调用简单函数来完成更复合、更复杂的任务,不断重复这个堆积木的函数,只要小模块都是正确的,那么组合而成的系统也应该是正确的。

  • 定义函数 add,输入是两个数,输出是两个数相加之和;
  • 定义函数 sub,输入是两个数,输出是第一个数减去第二个数的差;
  • 定义函数 print,输入是一个数,print 函数将其显示在缺省终端上;
  • 定义函数 main,完成下述流程:

    1. 从某个地方读一个数 a;
    2. 从另一个地方读一个数 b;
    3. 如果 b >= 0 则定义 c = add(a, b),否则 c = sub(a, b);
    4. 调用 print(c);
    5. 结束。

这样整件事被分解成了 add、sub、print 和 main 四个函数,每个函数都只做很简单的、易于验证的事情,每个函数可以被不同的人编写和测试,复杂度被分解和降低了;而这些函数可以用严格定义的程序流程(条件分支、循环等)组合起来,形成更大的函数,如此我们就可以从非常简单的积木出发,最终构建起宏伟的城堡。

这种 “分而治之” 的思想,有个专门的术语来表达,叫做 “责任分离(separation of concern, SoC)”,这里面除了 “分”,还隐含着 “黑盒” 的理念,也就是每个函数搞定自己的任务,调用你的函数不用管你怎么做到的,也不用担心你会做错,各司其职,互不干预,只要输入输出的格式不发生变化,整个系统就能正常运作。这也给了每个函数的实现者最大的自由,可以互不影响地不断优化迭代(比如修正错误、提升性能等)。

模块化另一个显而易见的好处就是更好的 “复用性(re-usability)”,一个模块如果解决了一个普遍性问题,而且实现又正确又高效又健壮(软件的健壮性是个很有意思的专门话题,以后我们会专门讨论),就可以用在很多地方,不需要多次实现。

5. 了解 软件设计范型

先驱们在软件工程实践中发展出了各种方法论体系,也就是我们今天看到的 面向对象函数式编程范型,它们除了提供模块化的特性以外,还力求做到:

  • 容易学习和掌握;
  • 统一的术语和编程模式;
  • 对经常遇到的问题有开箱即用的解决方案。

这些方法论体系一般是在特定软件系统和特定开发团队中萌芽并逐步发展起来的,所以必然有各自侧重的领域。目前主流的编程语言都是多范型(multi-paradigm)的,即融合了多种编程范型的特性和优势。

一般应该这样:

  • 在具体编程语言和场景中学习,而不是为学而学;
  • 理解一种范型的核心价值,它最擅长解决的是什么问题,是通过什么独特的思想和工具解决的;
  • 牢记软件开发的根本困难和模块化等本质性思想,不断用之来检验具体方案。

讲完了历史,下一章我们介绍面向对象的基本概念。

Logging

2020-02-21 23:25:33 initialize