谈谈编程(3) 编程实践鸠集遗失,鉴玩整理,昼夜精勤,每获一卷,遇一画,毕孜孜葺缀,竟日宝玩,可致者必货敝衣, 减粮食。妻子童仆切切嗤笑,或曰:终日为无益之事何补哉。既而叹曰:若复不为无益之事,则安能悦有涯之生。
--- 唐 张彦远 《历代名画记》
1 编程的要素编程有3个要素:语言、环境和思想。
1.1 语言有人喜欢争论语言的优劣。其实,除了汇编语言,各种语言、脚本、标准库、类库、框架都蕴含着大量成熟的编程经验和思想。程序员应该多熟悉一些语言,特别是有代表性的语言。
个人觉得,一个程序员应该掌握一两种汇编语言(CISC的X86、RISC的ARM)、一种面向过程语言(C)、两、三种面向对象语言(C++、Java、Delphi)、一两种脚本语言(perl、python、ruby)。如果有时间,可以再学习一些学术性较强的语言,例如Scheme。
学习新的语言,不仅可以吸收语言中蕴含的设计理念,还可以打开连接新空间的大门,使我们可以学习、复用使用该语言的各种资源,例如源代码、文章、书籍。
1.2 环境1.2.1 开发时和运行时环境可以被理解为程序所有外界环境的总和,包括开发时环境、运行时环境。我们在写一段程序时,应该对该程序的相关环境有清楚的了解。
开发时环境包括我们使用的编译(链接)环境、复用的代码(框架、类库、控件等),系统的逻辑结构、代码的文件组织、需要的工具软件、调试环境等等。
运行时环境包括程序运行时环境中所包含、发生的一切,特别是与我们的程序相关的部分。从小处看,我们应该知道如何确定每段代码运行的context(线程和堆栈),每个变量所使用的空间的信息。从大处看,我们应该了解系统运行的来龙去脉,各个模块(逻辑概念)如何相互配合,各个线程(调度单位)如何相互通信,什么时候可能发生调度,系统中有哪些不确定的因素等等。
1.2.2 软件类型比较“典型”的软件类型包括:
Windows操作系统;
Linux操作系统;
编译器;
虚拟机;
调试器;
Windows的单机应用程序;
Windows的驱动程序;
Linux的应用程序和驱动程序;
基于socket的客户端-服务器程序;
数据库应用程序;
使用数据库的Web应用程序;
使用RTOS的嵌入式软件;
不使用RTOS的单片机程序;
DSP程序;
嵌入式环境的第三方程序(J2ME应用、BREW应用、symbian的应用程序等等);
各种中大型程序的脚本环境,插件的开发、运行环境;
Office应用程序开发;
flash编程; 软件的种类实在太多,所谓“典型”只是我个人的理解,肯定还有很多软件类型没有被列出来。程序员应该了解开发和运行这些软件时,究竟发生了什么。
对于Windows、Linux操作系统,我们应该有个概要的了解:从BIOS程序读Master Boot Record,到系统的装载运行;应用程序或动态连接库(Linux的SO)的装载;操作系统的基本模块(包括内核)的功能;Windows如何通过COM机制将各个功能模块组合起来等等。这些内容是PC程序的基本运行环境。
嵌入式环境相对PC环境要简单一些,特别是用NOR flash,代码直接从ROM运行的系统。一方面,程序员通常可以看到系统运行的所有代码;另一方面,嵌入式环境有时不提供第三方程序运行机制,有时提供比较简单的机制,有时用java虚拟机当作第三方程序运行机制。不过,智能手机的应用处理器一般使用NAND flash,从BIOS运行一小段启动代码,将系统装载到RAM运行,同时支持应用程序装载运行,已经很接近PC环境。
编写单机应用程序,除了语言外,主要要熟悉各种库、框架、组件。一些通用库提供了常用函数、各种容器和算法、GUI、典型的程序框架,系统API的封装等等,例如:
移植性比较好的C标准库、STL、boost、TK等;
Windows上的MFC、VCL(Delphi/C++ Builder);
Linux上的ncurses、X Windows、GTK、Qt等;
访问数据库:VC++的ODBC、DAO、ADO,Borland的BDE等;
Borland做了一些在源代码级跨平台的库:Delphi/Kylix的CLX、dbExpress;
CPAN上的大量perl模块、java的类库等等; 上面列出的是一些比较通用的库。还有用于各种语言的大量专用库,各种提供二进制接口的组件、控件。
编译器、虚拟机也是单机应用程序(在嵌入式环境,虚拟机可能是系统软件的一个模块)。不过它们的地位比较特殊。作为程序员,我们应该了解编译器、集成开发环境、软件框架、虚拟机、操作系统分别为我们做了什么。
作为程序员,我们同样应该理解调试器也是一个应用程序,调试器的基本原理,它能做什么,有什么限制。如果调试器与目标程序运行在不同的CPU上,调试是如何实现的,有哪些不同的实现方式?例如JTAG调试利用了目标CPU的调试接口直接调试目标程序;串口调试要求将一段调试代码和目标程序链接在一起下到目标CPU中,由嵌入的调试代码与PC机的调试器通信实现调试。不同的实现手段决定了调试器的能力和限制。
目前最热门的软件类型就是使用数据库的Web应用了,例如各种网站、网络游戏、各种企业、行业、政府机构的管理系统,在这个领域集中了处于食物链不同环节的大量厂商,各种Web服务器、基础平台、应用开发框架。随便列一下,也能列出一堆名词:
HTML/CSS和CGI;
java applet、java script、ActiveX控件;
php、asp、jsp、servlet;
.net家族:asp.net、ado.net等;
J2EE with/without EJB、Spring、Struts、Hibernate;
Ruby on Rails、Plone等;
在一种软件类型上,集中了这么多开发技术、框架、模式,也可以称得上蔚为壮观了。不过,这个领域里确实是各种最新的编程思想、方法、设计模式的演武厅,如同当年的编译器,值得所有程序员研究、学习。
1.3 思想COM可以被看作OLE发展的衍生品。但它的重要性远远超过了OLE。它首被独立出来,成为OLE、ActiveX的基础,然后逐步成为在Windows进行二进制集成的基础。COM和RPC的结合产生了DCOM。DCOM和MTS的结合产生了COM+。虽然这些技术都是用于Windows平台的,但组件技术的基本思想是独立于具体环境的。也就是说,对程序员而言,存在着独立于语言和环境之外的领域,这就是编程的思想。
例如看看Qualcomm的BREW,就会发现它从COM中学习了多少东西。嵌入式平台的程序员使用PC平台的技术,这就是编程思想的价值。对于程序员来说,各种编程思想、设计模式,是最为宝贵的东西。这里所说的设计模式并不局限于GOF的《设计模式》,任何惯用的手法都可以被看作模式。
一些基本的编程思想起源于更普遍的智慧。例如:
心智的活动,除了尽力产生各种简单的认识之外,主要表现在以下三个方面:1)将若干简单认识组合成一个复合认识,由此产生出各种复杂的认识。2)将两个认识放在一起对照(在这样做时并不将它们合而为一),不管它们如何简单或者复杂,由此得到有关它们的相互关系的认识。3)将有关认识与那些在实际中和它们同在的所有其他认识隔离开,这就是抽象。所有具有普遍性的认识都是这样得到的。
--- John Locke, 有关人类理解的随笔
程序员从这段1690年的文字中能看到什么。组合、对照、抽象,这些基本的思维工具同样也是程序员最基本的工具。
2 编程规则编程是一门技艺,如同我们必须跳到水里才能学会游泳,我们必须多看、多写程序,才能学会编程。
任何技艺的学习都包括知识和能力两个部分。知识是明确的,可以用时间换取。能力是说不清的,不易掌握的。一般而言,能力高的人能够更快地积累知识,善于从已有的实践中总结规律,善于复用已有的模式,他们能更快地对复杂的环境形成清晰的认识,用最简洁、优雅的方式解决问题。如何提高编程能力应该是因人而异,每个人应该找到自己的学习路径,有自己的规划。
编程的表象千变万化,但在表象背后,还是有一定规律的。在某些条件下,这样做会比那样做更好一些。我们可以将这些东西称作经验、原则、规则,或者其它任何名词。
下面松散地列举了一些规则。我们应该在实践中检验已知的规则,总结自己的规则。
规则一 这世界上唯一的真理就是不要盲目相信真理
从某种意义看,原则、规矩这类东西就是用来打破的。所谓原则,就是对历史上某种经验的总结。如果我们踏入的河流和前人非常接近,我们可以参考前人的经验。我们是主动地拿来,而不是被动的遵守。
规则二 未蕴而变,自欺也;知律而变,智者之道也
这句话谈的是学词,必须先了解格律,然后才谈得上变化,否则就是自欺欺人了。在编程上,也是一样。在有资格打破一条原则前,首先要了解这条原则。先了解事物的规律,然后才是变化和灵活的应用。不要打倒自己不了解的东西。
规则三 寻找结构和成本的平衡点。当结构不能容纳变化时,重构代码到新的平衡点
开闭原则要求:一个软件实体应该对扩展开放,对修改关闭。即软件的结构要达到:允许在不修改软件的前提下扩展软件功能。
但好结构是要花成本的。程序员总是在折衷,寻找结构和成本的平衡点。我们会放一些余量,让结构能承受一定的变化。
规则四 要针对接口编程,不要针对应用编程
前面谈过了。我们总希望通过工作得到一些收获,积累一些经验。只有将代码分解成尽可能独立的模块,然后针对接口编程,才能保护我们的成果,让我们有所积累。
规则五 最少知识原则:模块对外界的了解应当尽可能少
前面也谈过了。所谓“圣人之治”,经常是“虚其心,实其腹;弱其志,强其骨,常使民无知无欲”。让被管理的对象尽可能得弱,对象间的关系尽可能得简单,可以降低管理的成本。
在面向对象编程中“组合优于继承”,就是因为继承将基类的实现细节暴露给子类,两者的耦合性太强了。另外,继承的实现是静态的,而组合可以更灵活地实现。
规则六 尊重习惯
在任何领域编程,应该尽量了解这个领域的习惯,尽量符合这些习惯。这样,别人可以更容易地理解和使用我们的程序,更不容易出错。
规则七 程序中不要出现magic number或其它神奇的东西
magic number就是诸如17、42这类数字。请用有描述性的常数名替代它们。常数名、变量名、函数名就是最好的注释。结构清晰的代码甚至可以不需要其它注释。
“需要很多注释的代码”是代码坏味道的一种,常常意味着需要重构代码,使它更容易被理解。
规则八 “两顶帽子”和“小步前进”
“两顶帽子”前面讲过了。面对复杂的问题,我们不仅要从结构上分解它,还要将实现步骤尽可能地分小,在一时一地以压倒性优势解决问题的一小部分。n-1个总比n个好对付,我们越来越强大,敌人越来越弱小,我们要做的只是将优势保持到最后。“保持优势”的关键是正确的理解、合理的划分。
规则九 适当地引入中间层,可以解决所有问题
这条规则是实质是“抽象”。在遇到问题时,多引入一层接口,将问题封装起来。
抽象本身并没有消除系统的复杂性,但它减少了在某个时刻需要处理的细节的数量。或者说,抽象将复杂性控制在接口和模型的后面,可以推迟我们处理这些复杂性的时刻。
这条规则的另一个说法是“多做白日梦”。假设我们已经得到了需要的东西,然后会怎么样?通过“白日梦”我们可以在更高的层次考察问题,可能找到解决问题的线索,也有可能发现这个问题毫无价值。即使毫无收获,我们也的损失也很小。
规则十 不要依赖调试,尽可能地依赖分析和推理
我经常会在头脑中预演调试的过程,计算我要做什么,我能得到什么。“预演”的结果经常是我没有必要这样调试,或者应该试一试另一个方法。
根据测不准原理,任何实际的测量、调试都会改变、影响被研究的对象。用调试辅助、验证我们的推测,但不要依赖调试。