推荐序
Bob大叔举了一个相当有趣的例子,如果又要保证操作原子性又要能精确还原各时刻的状态,有个办法是这样的:只提供CR操作,而不提供完整的CRUD操作(就像MySQL的binlog那样)。平时只要追加操作记录即可,各时刻的状态永远通过重放之前的操作记录得出,这样就彻底避免了状态的错乱。这个办法看起来古怪,但我真的在之前的开发中用过(当然是在程序生命周期有限的场景下),而且真的从没出过错。
第2部分 从基础构件开始:编程范式
直到今天,我们也一共只有三个编程范式,而且未来几乎不可能再出现新的
第3章 编程范式总览
它们分别是结构化编程(structured programming)、面向对象编程(object-oriented programming)以及函数式编程(functional programming)
- 结构化编程:我们可以将结构化编程范式归结为一句话:结构化编程对程序控制权的直接转移进行了限制和规范。
- 面向对象编程
- 事实上,这个编程范式的提出比结构化编程还早了两年,是在1966年由Ole Johan Dahl和Kriste Nygaard在论文中总结归纳出来的。这两个程序员注意到在ALGOL语言中,函数调用堆栈(call stack frame)可以被挪到堆内存区域里,这样函数定义的本地变量就可以在函数返回之后继续存在。这个函数就成为了一个类(class)的构造函数,而它所定义的本地变量就是类的成员变量,构造函数定义的嵌套函数就成为了成员方法(method)。这样一来,我们就可以利用多态(polymorphism)来限制用户对函数指针的使用。
- 我们也可以用一句话来总结面向对象编程:面向对象编程对程序控制权的间接转移进行了限制和规范。
- 函数式编程
- 从理论上来说,函数式编程语言中应该是没有赋值语句的。大部分函数式编程语言只允许在非常严格的限制条件下,才可以更改某个变量的值。
- 这里可以将函数式编程范式总结为下面这句话:函数式编程对程序中的赋值进行了限制和规范。
每个编程范式的目的都是设置限制。这些范式主要是为了告诉我们不能做什么,而不是可以做什么。这三个编程范式分别限制了goto语句、函数指针和赋值语句的使用。
这些编程范式的历史知识与软件架构有关系吗?当然有,而且关系相当密切。譬如说:
- 多态是我们跨越架构边界的手段
- 函数式编程是我们规范和限制数据存放位置与访问权限的手段
- 结构化编程则是各模块的算法实现基础。
这和软件架构的三大关注重点不谋而合:功能性、组件独立性以及数据管理。
第4章 结构化编程
可推导性
Dijkstra认为程序员可以像数学家一样对自己的程序进行推理证明。换句话说,程序员可以用代码将一些已证明可用的结构串联起来,只要自行证明这些额外代码是正确的,就可以推导出整个程序的正确性。
Dijkstra在研究过程中发现了一个问题:goto语句的某些用法会导致某个模块无法被递归拆分成更小的、可证明的单元,这会导致无法采用分解法来将大型问题进一步拆分成更小的、可证明的部分。
goto语句的其他用法虽然不会导致这种问题,但是Dijkstra意识到它们的实际效果其实和更简单的分支结构if-then-else以及循环结构do-while是一致的。如果代码中只采用了这两类控制结构,则一定可以将程序分解成更小的、可证明的单元。
测试
结构化编程范式促使我们先将一段程序递归降解为一系列可证明的小函数,然后再编写相关的测试来试图证明这些函数是错误的。如果这些测试无法证伪这些函数,那么我们就可以认为这些函数是足够正确的,进而推导整个程序是正确的。
第5章 面向对象编程
究竟什么是面向对象?
对于这个问题,一种常见的回答是“数据与函数的组合”。这种说法虽然被广为引用,但总显得并不是那么贴切,因为它似乎暗示了o.f()与f(o)之间是有区别的,这显然不是事实。面向对象理论是在1966年提出的,当时Dahl和Nygaard主要是将函数调用栈迁移到了堆区域中。数据结构被用作函数的调用参数这件事情远比这发生的时间更早。
还有些人在回答这个问题的时候,往往会搬出一些神秘的词语,譬如封装(encapsulation)、继承(inheritance)、多态(polymorphism)。其隐含意思就是说面向对象编程是这三项的有机组合,或者任何一种支持面向对象的编程语言必须支持这三个特性。
封装
通过采用封装特性,我们可以把一组相关联的数据和函数圈起来,使圈外面的代码只能看见部分函数,数据则完全不可见。譬如,在实际应用中,类(class)中的公共函数和私有成员变量就是这样。
然而,这个特性其实并不是面向对象编程所独有的。其实,C语言也支持完整的封装

显然,使用point.h的程序是没有Point结构体成员的访问权限的。它们只能调用makePoint()函数和distance()函数,但对它们来说,Point这个数据结构体的内部细节,以及函数的具体实现方式都是不可见的。
这正是完美封装
而C++作为一种面向对象编程语言,反而破坏了C的完美封装性。
由于一些技术原因[2](C++编译器必须要知道每个类实例的大小),C++编译器要求类的成员变量必须在该类的头文件中声明。这样一来,我们的point.h程序随之就改成了这样:

好了,point.h文件的使用者现在知道了成员变量x和y的存在!虽然编译器会禁止对这两个变量的直接访问,但是使用者仍然知道了它们的存在。而且,如果x和y变量名称被改变了,point.cc也必须重新编译才行!这样的封装性显然是不完美的。
当然,C++通过在编程语言层面引入public、private、protected这些关键词,部分维护了封装性。但所有这些都是为了解决编译器自身的技术实现问题而引入的hack——编译器由于技术实现原因必须在头文件中看到成员变量的定义。
而Java和C#则彻底抛弃了头文件与实现文件分离的编程方式,这其实进一步削弱了封装性。因为在这些语言中,我们是无法区分一个类的声明和定义的。
由于上述原因,我们很难说强封装是面向对象编程的必要条件。而事实上,有很多面向对象编程语言[3]对封装性并没有强制性的要求。
继承


请仔细观察main函数,这里NamedPoint数据结构是被当作Point数据结构的一个衍生体来使用的。之所以可以这样做,是因为NamedPoint结构体的前两个成员的顺序与Point结构体的完全一致。简单来说,NamedPoint之所以可以被伪装成Point来使用,是因为NamedPoint是Point结构体的一个超集,同时两者共同成员的顺序也是一样的。
上面这种编程方式虽然看上去有些投机取巧,但是在面向对象理论被提出之前,这已经很常见了[5]。其实,C++内部就是这样实现单继承的。
同时应该注意的是,在main.c中,程序员必须强制将NamedPoint的参数类型转换为Point,而在真正的面向对象编程语言中,这种类型的向上转换通常应该是隐性的。
多态
在面向编程对象语言被发明之前,我们所使用的编程语言能支持多态吗?答案是肯定的,请注意看下面这段用C语言编写的copy程序
#include <stdio.h>
void copy()

最低0.47元/天 解锁文章

被折叠的 条评论
为什么被折叠?



