这是一本能陪你走到职业生涯终点的书籍。
概述
一般而言,相比较"术"而言,讲述"道"的书才可能成为经典。而这本成书于1986年的紫色书籍,恰好是这样一部经典之作。这里只是记录笔者一些个人的思想总结。虽然粗浅,但贵在真实。
代码是供人读的
很多推荐本书的人在总结本书的核心思想时候都会提及以下两个观点:
- 程序首先是给人读的,只是恰好可以被机器执行。
- 与程序而言,最最重要的是控制软件的复杂度;相比较之下,算法和数据结构在此之后。
借SICP视频里的一句话: “machine does not care, i care!”。所以代码规范真的不能只落在口头上啊。
wishful thinking(按愿望编程)
- 书写递归的必备思想。
- 真正接受这个思想时候,你的设计自然就会变成依赖于抽象,自然而然达到松耦合。
- 经典例子就是三步实现汉诺塔,我们以四阶举例:
- 先将前三个移动到中转位置;
- 再将最后一个移动到目标位置;
- 最后将前三个移动到目标位置;
- 这里面还有一个非常重要的疑问:为什么我们可以这么做? 为什么我们可以这么去假设,这么去wishful thinking?
- 因为我们在减小问题的规模;
- 我们将问题从移动四个,变成了移动三个;
- 依此递归下去,我们最终就能将问题减小到可以一步解决;
- 最终实现整个问题的解决。
layer system(按层次划分系统)
- 应对需求变更的终极大招。
- 我们将系统构建成一个layer system的目的就是为了当外界需求发生变化时, 我只需要修改某一层的某一部分, 而这一层的上下层都对这个变化没有感知。
- layer system的基本思想是上面一层完全依托于下面一层的api进行构建, 也就是说,其实在layer system中每一层都是拥有构建完整系统的能力的。
- 当新需求到来时,如果发现在上一层无法被解决, 我们就可以进行降层, 在下面一层尝试进行解决. 最终将需求就被限定到一个尽量小的范围。
- 这种思路在所有的大型软件中都能看到, linux就是最好的例子。
Abstract(抽象)
- it is not for machine, it is for human. because our brain can not put all things in it at once。好吧, 这句英文是我自己的总结。
- 很早以前开始软件的复杂度就已经大大超出了人类大脑所能控制的范围,再明确一些就是如果你想将这些软件的细节一次性地都放进你的大脑,这是绝无可能。
- 所以我们需要一些方法, 而这些方法并不是软件行业独创的, 而是人类使用大脑思考以来就一直在使用的方法. Abstract就是这其中非常关键的一种.
- 正是有了Abstract, 在软件规模继续保持暴增的情况下, 人类依然牢牢掌握着主动权。
- 抽象其实无处不在, 一提到抽象就想到接口, 抽象类。其实变量名,方法就是一种抽象。抽象的另外一层意思就是要学会去忽略细节。就像上面提到的, 软件里最终是要构建一个layer system, 这样就能做到最小化改变适用新需求. 而这个layer system的表现就是:
- 不论你处在抽象层次中的哪一层 ,当你对底层进行分析查看 ,就会发现依然无数的抽象层次的存在。
- 所以你需要做的事情之一就是学会忽略某些细节。
- 理解复杂事物的关键之一就在于:你需要清楚知道应该忽略掉那些次要的东西,而将注意力集中到那些真正需要考虑的事物上。
推迟作决定的时机
单看这句话会比较玄幻, 其实举几个例子就理解了。
- 很多时候我们不想要被此时的决定束缚住手脚,导致之后的工作因当前的决定而步履维艰。但我们又必须作出某种决定以便让接下来的工作得以进行,于是我们可以假装实现了这个功能(假装已经做出了这个决定,Wishful Thinking)来让工作得以继续;待到真正需要这个功能时,才去实现它(真正地做出来决定)。使用这种方法可以让我们的程序变得非常稳固,能够自如地应对外界的变化。
- “面向接口编程”,相信此类言论已经被你们听烂了吧。那我给一种新的解释,希望能带来一点不一样的感悟 —— 建立接口的目的就是为了推迟作决定的时机。
- 定义接口就是假装作出了这个决定。
- 而实现接口则是真正地作出这个决定。
- 上面两个行为是有时间差的。而且这个时间差越大,你的程序就越灵活。
- 熟悉Java的同学应该都知道多态属于一种"动态绑定"吧。所谓的动态绑定不就是到真正需要的时候才去找实现者吗?
- 而我们所熟悉的反射也是“推迟作决定的时机” 又一佐证。
高阶函数
接受这种思想的结果就是极度模糊化 data与function之间的界限。
直接看例子
var consResult = cons(34,35);
car(consResult); // 34 ; 获取第一个参数
cdr(consResult); // 35 ; 获取第二个参数
看到以上的结果, 出现在你大脑中的实现过程大概应该是这样的:
function cons(x, y) {
return [x,y];
}
function car(p) {
return p[0];
}
function cdr(p) {
return p[1];
}
但是其实我们可以在只使用Process的情况下实现同样的效果.
function cons(x, y) {
return function (z) {
if (z === 1) {
return x;
} else {
return y;
}
};
}
function car(p) {
return p(1);
}
function cdr(p) {
return p(2);
}
模糊化data与function的好处:
- 高阶函数让我们拥有了更高的抽象层次。
- 通过接收function, 我们即可以实现多态的功能。这也符合OOP指导思想中的"使用组合代替继承"。
- 在下面要讲到的Stream Process中你会发现, 为了达到和命令式编程一样的性能,我们就需要求助于这种思想。
Stream Process(流式处理)
按照书中作者的介绍,这种处理思路是从电气化工程师那里学来的。这里我们不去八卦这些趣闻,直接以一个例子"遍历一颗正整数组成的树,获取其中所有的奇数,并取它们的平方和?"来开始本小节的讨论。
一般方式是使用递归,其中包含了遍历,树节点类型判断,奇偶判断, 平方计算,和的计算等,这些操作被糅合在一起循环往复,我们的大脑需要在这些操作中不断地进行跳跃并时刻警惕自己当前所处的环节以及上下环节的影响乃至当前操作对全局的影响,说得高深一些就是我们要时刻警惕时间因素的影响。我们要时刻记住自己当前在处理什么,在进行何种操作。
而使用流处理, 你就能将它们拆解出来, 这样处理过程就变得非常清晰:
- 遍历出树中所有的数据;
- 过滤出其中的所有奇数;
- 将上面过滤出来的奇数进行平方的映射操作;
- 将上一步的结果进行求和;
以上的流处理过程,非常符合人类大脑的思维习惯。使用Stream Process之后, 我们就将问题拆解为一系列协议接口(Convention Interface)的顺序调用,只要遵从了彼此间的这些协议, 上面步骤中的每一步都是可以被替换的,这样系统就天然具有了松耦合的特性。
对于接触过.NET,Java的研发人员,以上思想是不是非常熟悉?
结语
- SICP这本书在专业书籍里一直被笔者放在第一的位置,从未动摇。
- 这本书的吞噬时间的能力真是非常恐怖,导致笔者这种半路出家,大学毕业之后三年才转入IT行业的,最终选择了视频的方式来接受两位作者的教诲。
- 非常感谢LearningSICP群主的努力,无法直接分担汉化的工作,就只能做一些小的捐助聊表心意了。
- 最近才知道两位教授还健在,祝愿他们身体健康吧。其实我蛮想要一本有着他们签名的英文版SICP。