编程范式

所谓编程范式(Programming Paradigm),指的是计算机编程的基本风格或典范模式。借用哲学的术语,如果说每个编程者都在创造虚拟世界,那么编程范式就是他们置身其中自觉不自觉采用的世界观和方法论。

编程是为了解决问题,而解决问题可以有多种视角和思路,其中普适且行之有效的模式被归结为范式。由于着眼点和思维方式的不同,相应的范式自然各有侧重和倾向,因此一些范式常用 "oriented" 来描述。

编程范式是抽象的,必须通过具体的编程语言来体现。它代表的世界观往往体现在语言的核心概念中,代表的方法论往往体现在语言的表达机制中。一种范式可以在不同的语言中实现,一种语言也可以同时支持多种范式。任何语言在设计时都会倾向某些范式,同时回避某些范式,由此形成了不同的语法特征和语言风格。

开发技术

什么是框架(Framework)?它与一般的库(Library)和工具包(Toolkit)有何不同?

框架就是一组协同工作的类,它们为特定类型的软件构筑了一个可重用的设计。与库和工具包不同之处在于前者侧重设计重用而后者侧重代码重用

框架并不限于 OOP,可以是协同工作的类,也可以是协同工作的函数。一个足够复杂的应用软件开发,为确保快速有效,通常采取的方式是:在宏观管理上选取一些框架以控制整体的结构和流程;在微观实现上利用库和工具包来解决具体的细节问题。框架的意义在于使设计者在特定领域的整体设计上不必重新发明轮子;库和工具包的意义在于使开发者摆脱底层编码,专注特定问题和业务逻辑。

框架与库和工具包看起来很相似————都是一些代码集合,都提供一些 API(应用编程接口),是什么使得它们不同呢?

框架与工具包最大的差别在截然相反的设计理念上:库和工具包是为程序员带来自由的,框架是为程序员带来约束的。具体地说,库和工具包是为程序员提供武器装备的,框架则利用控制反转(IoC)机制实现对各模块的统一调度,从而剥夺了程序员对全局的掌控权。

那么设计模式(Design Pattern)和架构(Architecture)呢?

设计模式和架构不是软件产品,而是软件思想。设计模式是软件的战术思想,架构是软件的战略决策。设计模式是针对某些经常出现的问题而提出的行之有效的设计解决方案,它侧重思想重用,因此比框架更抽象、更普适,但多限于局部解决方案,没有框架的整体性。与之相似的还有惯用法(Idiom),也是针对常发问题的解决方案,但偏重实现而非设计,与实现语言密切相关,是一种更底层更具体的编程技巧。至于架构,一般指一个软件系统中最高层次的整体结构和规划,一个架构可能包含多个框架,而一个框架可能包含多个设计模式。

在没有打好基础前,架构只是空中楼阁,因此不可能现在谈它。至于框架,不同的应用领域有不同的框架,如表现层的 Struts、业务层的 Spring、持久层的 Hibernate 等等,即使相同领域的框架也有多个选择,更不用说不同的语言框架还不一样,从何谈起?而设计模式,一共就那么几十个,一本 GoF 的书足矣,自己慢慢啃,又何须多谈?简而言之,一个谈之过早,一个无从谈起,一个不必多谈。

命令范式

命令式编程(Imperative Programming)是指程序由命令序列组成,即一系列祈使句:“先做这,再做那”,强调“怎么做”。更学术点说,命令式编程是电脑————准确地讲,是冯诺依曼机————运行机制的抽象,即依序从内存中获取指令和数据,然后去执行。从范式的角度看,其世界观是:程序是由若干行动指令组成的有序列表。其方法论是:用变量来存储数据,用语句来执行指令

声明式

声明式编程(Declarative Programming)是指由若干规范(Specification)的声明组成的,即一系列陈述句:“已知这,求解那”,强调“做什么”而非“怎么做”。声明式编程是人脑思维方式的抽象,即利用数理逻辑或既定规范对已知条件进行推理或运算。

声明式编程发轫于人工智能的研究,主要包括函数式编程(Functional Programming,简称FP)和逻辑式编程(Logic Programming,简称LP)。其中,函数式编程将计算描述为数学函数的求值,而逻辑式编程通过提供一系列事实和规则来推导或论证结论。

命令式编程是行动导向(Action-Oriented)的,因而算法是显性而目标是隐性的;声明式编程是目标驱动(Goal-Driven)的,因而目标是显性而算法是隐性的。

为了便于说明,我们分别用3种代表性的语言来实现阶乘(Factorial)运算:

# C (命令式)
int factorial(int n) {
    int f = 1;
    for(; n > 0; --n) f *= n;
    return f;
}

# Lisp (函数式)
(defun factorial(n)
    (if (= n 0) 1    // 若n等于0,则n!等于1
    (* n (factorial(- n 1)))))    // 否则n!等于n*(n-1)

# Prolog (逻辑式)
// 0! 等于1
factorial(0,1)
// 若M等于N-1且M!等于Fm且F等于N*Fm,则N!等于F
factorial(N,F) : - M is N - 1, factorial(M,Fm), F is N * Fm.

C 语言明确给出了阶乘的迭代算法,而 Lisp 仅描述了阶乘的递归定义,Prolog 则陈述了两个关于阶乘的断言。

对于已经习惯编程思维的人来说,第一种方式(C 命令式)更容易接受。但是刚刚开始学习编程的话还是不太习惯i=i+1之类的语句。因为我们最早接触的变量是代数方程中的x,y,z等,本质上是抽象化的符号,变量值是该符号在给定约束条件下的允许值。而命令式编程中的变量本质上是抽象化的内存,变量值是该内存的存储内容。通俗地说,前者好比姓名,所指之人是固定的;后者好比住址,所住之人是变化的。此外,等号在代数中是一种约束,而在许多命令式语言中则表示赋值。

声明式编程让我们重回数学思维:函数式编程类似代数中的表达式变换和计算,逻辑式编程则类似数理逻辑推理。其中的变量也如数学中的一样,是抽象符号而非内存地址。因此,没有赋值运算,不会产生变量被改写的副作用(Side-effect),也不存在内存分配和释放的问题。这既简化了代码,也减少了调试,因为很大程度上避免了因为某个变量被意外改写或内存管理不慎造成 Bug.

总的来说,在命令式语言中融入声明式的元素应当是一种趋势。尤其是函数式,它的一些特征已经在许多命令式语言中得到了支持。比较而言,声明式编程重目标、轻过程,专注问题的分析和表达而不致陷入算法的迷宫,其代码也更加简洁清晰、易于修改和维护。从这种意义上说,声明式语言天然地就比命令式语言更高级。

早在命令式语言引入函数从而进化为过程式语言时,就已经开始向声明式过渡了。比方说调用一个函数的语句:doWhat(),这不正是在声明 "what to do" 吗?至于 "how to do",即函数的具体实现细节,则不劳调用者费心。这种声明式的风格,提高了语言的抽象能力和开发效率,促成了语言的升级。

既然声明式编程有这么多好处,为什么命令式语言不仅占大多数,而且流行程度也不减呢?

这是因为编程语言的流行程度与其擅长的领域关系密切。声明式语言————尤其是函数式语言和逻辑式语言————擅长基于数理逻辑的应用,如人工智能、符号处理、数据库、编译器等,对基于业务逻辑的、尤其是交互式或事件驱动型的应用就不那么得心应手了。而大多数软件是面向用户的,交互性强、多为事件驱动、业务逻辑千差万别,显然命令式语言在此更有用武之地。

归根结底,编程是寻求一种机制,将指定的输入转化为指定的输出。3种范式对此提供了截然不同的解决方案:命令式把程序看作一个自动机,输入是初始状态,输出是最终状态,编程就是设计一系列指令,通过自动机执行以完成状态转变;函数式把程序看作一个数学函数,输入是自变量,输出是因变量,编程就是设计一系列函数,通过表达式变换以完成计算;逻辑式把程序看作一个逻辑证明,输入是题设,输出是结论,编程就是设计一系列命题,通过逻辑推理以完成证明。

对象范式

软件设计最重要的并不是编程语言,甚至也不是编程范式,而是抽象思维。OOP 以对象为基本模块单位,而对象是现实中具体事物和抽象概念的模拟,这使得编程设计更自然更人性化。

尽管 OOP 最大的卖点是其高度的可重用性,相比其他范式却并不具明显优势,但它更接近人类的认知模式,编程者更容易也更乐于用这种方式编程,这是它深入人心的一个重要原因。与其说 OOP 更具重用性,不如说更具易用性

过程式编程的模块以函数为单位,OOP 的模块以对象为单位,二者的区别是:函数是被动的实体,对象是主动的实体过程式程序的世界是君主制的,主函数是国王,其他函数是臣民,等级分明,所有臣民在听命于上级的同时也对下级发号施令,最终为国王服务;OOP 程序的世界是民主制的,所有对象都是独立而平等的公民,有权利保护自己的财产和隐私并向他人寻求服务,同时有义务为他人提供承诺的服务,公民之间通过信息交流来协作完成各种任务。更进一步地,封装使得公民拥有个体身份,须要对自己负责;继承使得公民拥有家庭身份,须要对家庭负责;多态使得公民拥有社会身份,须要对社会负责。

并发范式

并发式编程(Concurrent Programming)绝不只是调用线程 API 或使用 synchronizedlock之类的关键字那么简单。从宏观的架构设计,到微观的数据结构、流程控制乃至算法,相比通常的串行式编程都可能发生变化。随着硬件性能和用户需求的双重提升,并发式编程已成为不可回避的主题。不同的编程范式采用不同的视角和方法来设计和开发软件。并发式编程以进程为导向(Process-Oriented)、以任务为中心将系统模块化。

但是此处可能有个疑问:过程式中引入了函数模块,对象式中引入了对象模块,但并发式似乎没有在语法上引入新型模块,比如 C 和 Java 中的线程其实不过是函数或方法的包装而已?

其实相比串行式,并发式在模块之间引入了新的通信和控制方式。也就是说,原先的一些模块的定义和划分一定是建立在线程机制的基础上的。如果失去线程的支持,它们的合理性自然会打上问号,说不定整体设计都会受到牵连。这也体现了编程范式的渗透性和全局影响力。

除了用户主观上的需求和硬件客观上的可能外,并发式显得格外重要的另一深层原因是许多程序是现实世界的模拟,而我们生活的世界不折不扣是并发式的。从某种意义上看,并发式的模拟比对象式的模拟更贴近世界。

并发式编程以资源共享与竞争为主线————这是对当今世界形式的一个逼真模拟。这意味着程序设计将围绕进程的划分与调度、进程之间的通信与同步等来展开。合理的并发式设计需要诸多方面的权衡考量。

同步(synchronization)只在采用共享内存(shared memory)的并发模型中需要,在采用消息传递(message passing)的并发模型中并不需要。本文主要以前一种并发模型为讨论对象,它也是大多数语言或库(包括 C、C++、Java、C# 等)所支持的模型。

专门为并发式而设计的语言大多仅限于学术研究而非商业应用,Erlang语言是少数的例外。顺带一提,前面提到的声明式语言具有无副作用的特性,尤其适合于并发式编程。

Erlang 是由爱立信开发的一种通用编程语言,支持函数式和并发式,采用消息传递的并发模型。

标签: none

评论已关闭