冒号和他的学生们
——程序员提高班纪事
22.抽象思维
是谓无状之状,无物之象,是谓惚恍
——《老子·道经》
冒号健步走进教室,学员们立刻正襟危坐,进入战备状态。
“如果说咱们是在合演一场戏,那么前面五节课只是一个过门。”冒号俨然一副自导自演的架势。
众人暗暗吃惊:这过门也忒长了点吧。
冒号随即探问:“还记得在范式总结中提到的迭代学习法吧?”
引号迅速应答:“就是在具体知识与抽象理论之间做折返跑。”
“记性不错。”冒号赞道,“在上本班之前,你们已经具备了一定的编程语言的基础,因此我们先从抽象的编程范式谈起,此后又回归到编程语言的讨论。”
句号推测:“照此逻辑,下面我们将再次返回编程范式?”
“我们的确要来个For Loop,但相信这是一个增量式的循环。”冒号用编程语言来强调他的学习理论。
逗号有些失望:“按计划不是该对Java语言作专题讨论吗?”
“你放心,Java它跑不了。”冒号看出他的心事,“语言是形,范式是神,这次我们将二者融合,争取做到形神兼备。具体地说,范式以OOP为主,语言以Java为主,同时可能涉及C、C++或C#等语言。另外,示例代码也会明显增多。”
逗号脸上的一抹乌云顿时消散开来。
冒号接着提醒道:“不过,秉承开班发言中的理念,我们的重心不在知识的枝节,而在知识的本源。因此无论讨论Java还是OOP,我们不追求系统和全面,但力求从不同的选点、角度和深度来展示知识的活性。”
讲到此处,冒号冷不丁提问:“程序员最重要的能力是什么?”
众人的答案五花八门:学习能力、逻辑思维能力、解决问题能力、专注力、沟通能力等等。
“毫无疑问,你们所说的都很重要。这本是个见仁见智的问题,我只是借此展开今后的话题。在我看来,抽象思维能力是最重要的。当然,不独计算机领域,其他科学同样需要这种能力。更广泛地说,抽象是人类认识和描绘世界最首要的工具。”不知不觉冒号又走上了形而上的路线。
叹号这时想起:“前面谈切面范式时,似乎专门提到过抽象与分解的重要性。”
“我们也曾提到,不同的范式正是对软件进行了不同角度的抽象和分解。”冒号加以补充,“那么什么是抽象呢?不妨概括为:去粗取精以化繁为简;由表及里以异中求同。再精炼些,抽象就是作减法和除法。”
问号半信半疑:“减法好理解,通过甄选减去非本质和不重要的部分,即去粗取精。可除法呢?”
句号忽然悟道:“透过现象看本质,发现不同事物之间的相同之处,即异中求同。同类归并,那就是除法了。”
冒号进一步解释:“用离散数学或抽象代数的语言来说,通过抽象而产生等价关系以及相应的等价类,便是集合的商运算。”
逗号嘀咕:“本来快明白了,经这么一描述,重新糊涂了。”
冒号笑道:“如果嫌数学语言高深,就用算术语言吧。乘法可看作同类复制,作为逆运算的除法自然是同类归并了。”
逗号眼中的迷惘渐渐散去,若有所悟:“嗯,经过减法和除法,大数变小数,复杂变简单。”
“能否把抽象说得再具体些?”问号话一出口便自感悖论之嫌:抽象的能具体吗?
冒号自明其意:“首先,抽象有角度之分。相同的实体(entity)经过不同角度的抽象,得到的模型(model)也会不同。就拿人这个实体来说,在拓扑学家眼里是三维连通集合,在理论力学家眼里是质点,在化学家眼里是碳水化合物——”
叹号接嘴:“在情人眼里是西施。”
“过滤缺点,抽取优点,西施就是这样炼成的。”冒号故意拉长了尾音。
众人不禁一乐。
冒号继续讲解:“其次,抽象还有程度之别。抽象程度越高,细节越少,普适性越强。典型的例子如:从矩形到多边形、从多边形到一般形状。”
引号问道:“抽象对于软件设计有何现实意义?”
冒号回答:“软件设计者的任务是将复杂混沌的现实世界映射到精确严格的虚拟世界,要完成这种多对一的映射,抽象无疑是必由之路。在软件需求分析阶段,多通过属性导向式抽象(property-oriented
abstraction)用逻辑语言来描述系统;在软件设计阶段,多通过模型导向式抽象(model-oriented
abstraction)用模型语言来设计系统;在编码阶段,常用两种抽象机制:一种是参数抽象(abstraction
by parameterization),一种是规范抽象(abstraction
by specification)。”
句号望文生义:“参数抽象是不是指将函数代码中的一些特殊值作为参数来传递?”
“不错,这是最普通最常用的一种抽象方式。函数的每一个参数都是一种泛化,是对它所代表的所有可能值的一种抽象。我们看一个简单的例子。”冒号说完在黑板上写下一段代码——
int gcd(int a, int b)
{
while (a != b)
(a > b) ? (a -= b) : (b -= a);
return a;
}
冒号考问:“请问这个函数是干什么的?”
众人看了好一阵,有人犹豫地说,好像是求最大公约数吧?
冒号肯定道:“它确实是求最大公约数的,由两个正整数辗转相减而得。该函数通过参数抽象让a和b分别代表任意正整数,使之具有更强的普适性和重用性。问题是除了该代码的作者,其他人如何重用此函数?你不能假设用户知道gcd是Greatest Common Divisior的缩写,也不能假设他的数学程度,甚至不能确定他是否能看到源代码。短短两行代码的函数尚且如此,何况更加复杂的函数?”
引号答道:“那只能靠文档注释了。”
“非常正确!”冒号颔首,“这是注释文档最重要的作用。没有文档的API如同没有说明书的产品,用户是不敢轻易使用的。合格的文档注释中至少应包括先验条件(precondition)和后验条件(postcondition),分别指代码执行前后必须满足的条件。对于函数gcd而言,先验条件是:a、b均为正整数,后验条件是:返回输入二数的最大公约数。有了文档注释或规范说明(specification)的函数成为使用者与实现者之间的一种契约——使用者只能依赖规范,实现者必须满足规范。这种通过规范使代码的功能与实现相分离的方法便称为规范抽象。其好处是显而易见的:一方面,使用者不必阅读代码即可了解并使用它们;另一方面,实现者不必阅读或改写其他代码,只需在遵循规范的基础上修改本地代码,并且不用担心影响客户代码。比如我们可以用辗转相除法重新实现gcd。”
冒号随手又写了几行代码——
int gcd(int a, int b)
{
return (b != 0) ? gcd(b, a % b) : a;
}
逗号未见其妙:“这两种抽象机制在实际编程中经常用到,只是以前不知道它们的学名罢了。”
冒号正色道:“它们看起来虽然很基本,但平淡之中见真功。如果一个程序员能合理设计参数、严格遵循规范、有效制定规范,便已是难得之才了。”
众人扪心自问,离此要求确有相当距离。
冒号续道:“借助参数抽象和规范抽象,我们可以实现过程抽象(procedural
abstraction)和数据抽象(data abstraction)。其中过程抽象容易理解,任何一个函数都是过程抽象的结果,它赋予程序员定义新运算或子程序的能力,是结构化编程(Structured
programming)和命令式编程(Imperative
programming)的关键。下面我们重点谈谈数据抽象,它是对象式编程(OOP)的起源。”