Skip to main content
 Web开发网 » 编程语言

从编程语言特性、核心概念到编程范式

2021年07月25日6060百度已收录

编程语言和编程都需要面对“软件的复杂度”、代码复用、需求不断变化的问题,为此,需要思考一下以下的一些问题:

领域问题如何映射到指令集?

数据如何表达?类型如何检查?

以数据及存储还是以数据处理或算法为核心?

代码如何组织?如何抽象分解?

如何确保运行效率和开发效率?

编程范式(programming paradigm)会试图对以上问题从指导思想的层面做出回答。编程范式指的是计算机编程的基本风格或典范模式,借用哲学的术语,如果说每个编程者都在创造虚拟世界,那么编程范式就是他们置身其中自觉不自觉采用的世界观和方法论。

如果每个程序员都在创造一个虚拟世界,那么编程范式就是世界观和方法论,他们被置于一种有意识或无意识的方式中。编程是要解决问题,解决问题可以有多种观点和思路,其中通用和有效的模式被总结为范式。

例如,编程中常用的“面向对象编程”是一种范式。由于焦点和思维方式的不同,相对的范式自然有其自身的聚焦和倾向,因此一些范式常被用来描述“oriented”。

托马斯·库尔提出“科学的革命”的范式论后,Robert Floyd在1979年图灵奖的颁奖演说中使用了编程范式一词。编程范式一般包括三个方面(以OOP为例):

① 学科的逻辑体系——规则范式:如 类/对象、继承、动态绑定、方法改写、对象替换等等机制。

② 心理认知因素——心理范式:按照面向对象编程之父Alan Kay的观点,“计算就是模拟”。OO范式极其重视隐喻(metaphor)的价值,通过拟人化,按照自然的方式模拟自然。

③ 自然观/世界观——观念范式:强调程序的组织技术,视程序为松散耦合的对象/类的组合,以继承机制将类组织成一个层次结构,把程序运行视为相互服务的对象之间的对话。

简单来说,编程范式是程序员看待程序应该具有的观点,代表了程序设计者认为程序应该如何被构建和执行的看法。应该说,包括编程语言看待世界的观点,以及开发者看待世界的观点。

常见的编程范式有:命令式(过程式+面向对象)、说明式(包括函数式)、泛型编程等。

有一个庖丁解牛的成语,在常人眼中复杂的牛体,庖丁经过抽象,已目无全牛,及至提刀分解,自是游刃有余。待牛如土委地,模块化即成。编程范式的基本思想大多不也如此?将程序分别抽象分解为过程、函数、断言、对象和进程,就依次成为过程式、函数式、逻辑式、对象式和并发式。泛型式虽未引入新类型的模块,其核心也是抽象出算法后与数据分解。以此类推,切面式的AOP 将程序抽象分解为切面。

从编程语言特性、核心概念到编程范式  范式 编程语言 特性 核心 概念 第1张

编程范式有两个关键属性:是否具有(用户)可见的不确定性,以及对状态的支持度。

如果用户用同一套配置开始执行,结果却不同,就叫做他们见到了不确定性。这非常不可取。只有在需要表现不确定性时,才应该让用户能够遇到不确定性。

关于状态,范式如何支持及时存储一系列值?状态可以是具名的或无名的、确定的或非确定的、串行的或并行的。并非所有组合都有用!

从编程语言特性、核心概念到编程范式  范式 编程语言 特性 核心 概念 第2张

需要注意的是,编程范式是编程语言的一种分类方式,它并不针对某种编程语言。就编程语言而言,一种语言可以适用多种编程范式。

一些编程语言是专门为某种特定范式设计的,例如C语言是过程式编程语言;Smalltalk和Java是较纯粹的面向对象编程语言;Haskell是纯粹的函数式编程语言。另外一些编程语言和编程范式的关系并不一一对应,如Python,Scala,Groovy都支持面向对象和一定程度上的函数式编程。C++是多范式编程语言成功的典范。C++支持和C语言一样的过程式编程范式,同时也支持面向对象编程范式,STL(Standard Template Library)使C++具有了泛型编程能力。支持多种范式可能是C++直到现在仍然具有强大的生命力的原因之一。

Swift是一门典型的多范式编程语言,即支持面向对象编程范式,也支持函数式编程范式,同时还支持泛型编程。Swift支持多种编程范式是由其创造目标决定的。Swift创造的初衷就是提供一门实用的工业语言。不同于Haskell这类出自大学和研究机构的学术性质的编程语言。苹果推出Swift时就带着着明确的商业目的:Mac OS和iOS系统的主要编程语言Objective-C已显老态,Swift将使得苹果系统的开发者拥有一门更现代的编程语言,从而促进苹果整个生态圈的良性发展。

1 编程语言的特性编程语言是计算机的符号,是人和计算机的通信符号和协议。学习一门新的编程语言时,应该观察这门语言的那些特性呢?《SICP》一书的作者列举了一下三点:

primitive elements(基本元素),表示语言的基本符号(基本数据类型,关键字等)也就是词法部分。

means of combination(组合手段),利用基本元素通过组合的过程构建大型程序的手段,不同的语言提供的组合手段是不同的。

means of abstraction(抽象手段),抽象是解决软件复杂度的重要手段,让软件的可读性,可扩展,可重复利用等得到提升。

1.1 数据类型系统

我们需要清楚地知道,无论哪种程序语言,都避免不了一个特定的类型系统。哪怕是可随意改变变量类型的动态类型的语言,我们在读代码的过程中也需要脑补某个变量在运行时的类型。

所以,每个语言都需要一个类型检查系统。

静态类型检查是在编译器进行语义分析时进行的。如果一个语言强制实行类型规则(即通常只允许以不丢失信息为前提的自动类型转换),那么称此处理为强类型,反之称为弱类型。

动态类型检查系统更多的是在运行时期做动态类型标记和相关检查。所以,动态类型的语言必然要给出一堆诸如:is_array(), is_int(), is_string() 或是 typeof() 这样的运行时类型检查函数。

大家平时经常会说,javascript是一个弱类型的语言,java语言是强类型的语言。将编程语言从类型系统的角度区分语言也很有趣。一般来说弱类型语言更偏向自然语言一些,语法也很自由活泼些。而现今语言的走势也趋向于弱类型方向。

我们知道计算机是结构化很强的,堆栈上一个二进制位的错误就会导致溢出等错误。所以语言层面的自由得益于编译器或者解释器的功劳。比如java,c等语言有很强的编译时类型检测机制,强类型的好处是驱使编程人员写出语法、语义错误非常少的代码,对IDE的支持也很棒,是大型技术团队的基石。

弱类型语言让我们获取了自由(不需要类型信息),让程序员少敲了许多键盘。但自由是有代价的,编译器或解释器中内含类型推理(infer type)。类型推理是利用归一方法,基于上下文中的变量显式类型、操作符、返回值等信息,利用栈和逐渐替换的过程来推导出类型。 弱类型虽然可以轻松编译通过(或者不需要编译而是解释执行),但都是有类型检查过程的,只是将此过程延迟到运行时了。所以弱类型语言结构化不强,编码时很难确保类型无误,IDE,大型团队开发也不友好。 但是通过一些分析器可以不断地检测语法,语义错误,相当于达到了强类型语言的IDE效果。

C的变量必须显式声明才能使用,必须有类型,类型固定。那么直觉来想应该算strong/static type。但是可以做隐式类型转换,且“强类型,弱检查”。C的值可以说是无类型的,可以在运行时由程序员任意解释的,那么直觉来算应该算weak/dynamic type。

而像Python/Lua这类,只有值是有类型的且不可变,变量名是没有类型的,所以直觉上strong/weak/static/dynamic type又不好区分。

像Scheme和Haskell,在语法层面似乎只有symbol和值的概念,没有变量的说法。Scheme在运行时会出现type错误,貌似算dynamic type。Haskell应该算static type。他们应该都算strong type吧。虽然可以这样简单地分类,但是并不严谨,因为没有严谨的定义,主观的想法比较多,当在语言实现的不同抽象层看的时候,这个分类可能就不那么正确了。另外,runtime、解释型和编译型,它们之间的关系也不好说。

1.2 组合手段

汇编语言算是最简单的词法和语法形式了,汇编器通过直译的过程将汇编代码翻译为二进制代码。 提供的primitive elements有:数字、字符、-、+、*、/、case、if、break、go、指令等基本元素。

C语言相比汇编语言更高级,让程序员脱离同寄存器、内存直接打交道的工作,提供了更多的组合手段:比如数组、结构体等数据结构。

Java语言自称是面向对象语言,所以比C语言更进一步,通过强大的类型系统手段来组合属性和方法。

Llisp语言以s-expression(著名的S表达式)来组合数据和函数。在Lisp中不区分数据和函数,一切皆为数据。

1.3 抽象手段

从C语言开始,以函数为单元提供了对程序的抽象。这样大大的提高了程序的可复用,模块化等。让团队合作编码也成为可能。

面向对象编程基本上隐藏了计算机的细节,开发者通过对象来抽象具体业务。但严格意义上来说Java也属于imperative-lang的范畴而且都是传值调用。比较而言,python、ruby更面向对象一些,python融合了面向对象和函数式编程范式,更接近自然语言些。

以Lisp为代表的函数式语言以函数和模块为抽象手段。common-lisp基于宏开发了一套object-oriented的编程方式。 函数式编程理念:函数的无副作用(不用考虑线程安全,特别是对于变态的Haskell),高阶函数,闭包,lambda等。

1.4 编译/解释

Java语言是解释型还是编译型的呢? 这个很难说,从java source code -> class byte code 的过程式javac编译器的过程。但是byte code 在jvm上执行的过程可能是解释执行也可能是编译执行的。解释型和编译型的内部都遵从编译原理的过程:词法分析 -> 语法分析 -> 语义解析 -> 编译器后端 -> native code的过程。 但有各自的优点:

解释器,加载code速度快;解释器需要维护运行时上下文等信息。所以加载必要的代码,片段解释执行。但是对于相同的代码都经过编译过程就很多余,造成时间浪费。

编译器,执行速度快。而且编译器后端也更容易优化中间代码,因为优化过程是一个结构化过程:往往需要遍历整个中间代码,整体优化代码,提高运行效率。

一般来说解释型语言需要在内存维护运行时上下文信息,服务于运行过程中变量的查找、绑定、scope等。 而编译语言基于堆栈模型执行代码,相对来说运行时简单,运行速度也快;

hotspot-jvm结合了解释和编译的各自优点,最先解释执行过程,如果方法被频繁执行,而且达到热点(hotspot)时,jvm会启动编译过程,将次代码编译为native-code,然后缓存起来,下一次的调研直接执行即可。 hotspot-jvm执行基于堆栈的指令byte code,这一点也是基于跨平台byte-code运行的考虑,而牺牲了寄存器指令(基于android操作系统上的dalvik虚拟机是基于寄存器指令的)。

所以说,设计一个语言往往是一个权衡过程;获取的“自由”越多,"牺牲“也更大。

计算机语言从低级发展到高级,渐渐远离机器,靠近人类,以牺牲部分性能和效率为代价,换来更高的开发效率和可维护性。中低级语言更适合中小型或底层应用,高级语言更适合大型应用。

最初从图灵为了解决莱布尼茨提出的是否存在一个通用模型来解决一切计算任务这个命题,提出了图灵机。到冯诺伊曼仿真人脑神经元思考过程产生第一台基于存储器、运算器的计算机EDVAC,至今,计算机硬件技术并没有实质性的变化,只是随着摩尔定律的破灭,人们发展了多级高级缓存、多核、多cpu技术来支撑越来越大的计算任务。 在这个过程中,随着人们对逻辑学、符号学、算法的不断研究,用来和计算机交互的编程语言也越来越抽象和丰富。我们通过这个形象的编程语言符号系统来抽象时间和空间,来表达指令的计算过程,来解决软件的复杂性问题,其发展越来越重目标轻过程、描述轻实现。

2 四个编程核心概念最重要的四个编程概念是记录、词法范围的闭包、独立性(并发)和具名状态。

① 记录是若干组数据项(比如结构),可通过索引访问其中的每一项。

② 词法范围的闭包,是把一个过程与其对外部的引用(定义闭包时引用的外部数据)结合起来。你可以创建一个‘工作包(packet of work)’在程序中传递,在之后某个时间才执行。

③ 独立性在这里指行为可以独立发生。即,它们可以并发地执行。关于并发,最受欢迎的两种范式是状态共享和消息传递。

④ 具名状态指我们可以给一个状态起名字,这是最简单的级别。但对于命名可变的状态,《Concepts, Techniques, and Models of Computer Programming》一书的作者范·罗伊( Peter Van Roy)有一个更为深刻和有趣的想法:在编程里,状态就是一个关于时间的抽象概念。函数式编程就没有时间概念……因为函数不会发生变化。现实世界则不同。在现实世界中,没什么东西像函数那样永恒不变。机体会成长和学习。机体在不同的时候受到相同的刺激,反应通常是不同的。对此,我们在程序里要如何建模?我们得创建一个具有唯一标识(它的名字)的实体模型,它的行为在程序运行过程中还会发生改变。为此,我们在程序里加入了一个抽象的时间概念。这个抽象时间概念只是一个序列,具有唯一名字的时间值的序列。

接着范·罗伊又给了一个建议:“最好让具名状态永远可见:应始终提供可以从外部访问该状态的途径。”(当谈及正确性时),又说“具名状态对系统的模块化很重要”(想想《 information hiding 》)。

数据抽象是根据精确规则来组织数据结构用法的方法,这些精确规则保证了数据结构被正确使用。数据抽象分别有一套内部接口、外部接口和两者间的接口。

数据抽象可以按照两个主要维度进行组织:是否使用具名状态,是否将操作绑定到具有数据的单个实体中。

从编程语言特性、核心概念到编程范式  范式 编程语言 特性 核心 概念 第3张

并发的核心问题是不确定性。

若程序的用户碰到不确定性,就会很难处理。用户可见的不确定性有时被称为竞态条件(race condition)…

若禁止不确定性,编写具有独立部分的程序将会受限。但我们能够限制不确定行为的可见程度。有两种选择:定义一种语言,所有不确定性都是不可见的;或把不确定性的可见范围限制在真正需要它的地方。

至少有四种编程模式是并发而又屏蔽了不确定性的(没有竞态条件):

并发范式

是否存在竞态

输入可以是不确定的

语言例子

声明式并发

Oz、Alice

约束编程

Gecode、Numerica

函数响应式编程

FrTime、Yampa

离散同步编程

Esterel、Lustre、Signal

消息传递并发

Erlang、E

有一类有趣的语言叫“双范式”语言。双范式语言通常用一种范式写小型程序,用另一种范式写大型程序。第二种范式通常用来支持抽象和模块化。比如,在面向对象语言中内置的支持约束编程的求解器。

更一般地说,范·罗伊看到了一个分层语言设计,它有四个核心层,这是一个在众多项目中都自发出现的结构:

常见语言的层次结构有四层:严格的功能核心,然后是声明性并发,然后是异步消息传递,最后是全局具名状态。这种分层结构天然地支持四种范式。

范·罗伊从他的分析中得出四个结论:

声明式编程是编程语言的核心。

在可预见的未来,声明式编程将保持其核心地位,因为分布式、安全性和容错是编程语言需要支持的基本主题。

确定性并发是一种重要的并发形式,不应忽视。这是充分利用多核处理器并行性的妙招。

用于处理一般并发的正确默认方法是消息传递,而非共享状态。

对于大型软件,范·罗伊认为我们需要采用自给自足的系统设计风格,系统可以自行配置、修复、调整等。系统将组件作为一等实体(由闭包规定),可通过高阶编程来操作。组件间通过传递消息来通信。由具名状态和事务来支持系统配置和维护。除此之外,系统本身应设计为一组联动反馈回路(interlocking feedback loops)。

3 编程的本质Program = Algorithm(Logic + Control) + Data Structure

代码复杂度的原因:

① 业务逻辑的复杂度决定了代码的复杂度;

② 控制逻辑的复杂度 + 业务逻辑的复杂度 → 程序代码的混乱不堪;

③ 绝大多数程序复杂混乱的根本原因:业务逻辑与控制逻辑的耦合。

如何分离 control 和 logic 呢?就需要考虑合适的编程范式来解耦。

Logic 部分才是真正有意义的(What)

Control 部分只是影响 Logic 部分的效率(How)

4 各类编程范式4.1 命令式编程范式 (Imperative)与过程式编程-重在how命令式编程的主要思想是关注计算机执行的步骤,即一步一步告诉计算机先做什么再做什么。指令式编程的概念,与计算机硬件的工作方式相近,更容易具体表现于硬件。大部分编程语言都支持指令式编程。

过程式程序设计(Procedural+programming),又称程序式编程、程序化编程,有时被视为“命令式”编程的同义词。

命令编程范式的程序开发,被视为一个命令/指令序列的开发。程序是一个语句的序列,这些指令通过操作数据而改变机器的状态。按照图灵机(Turing machine),“状态变化”是命令编程范式的关键词汇,以至于SICP指出:"广泛采用赋值的程序设计被称为命令式程序设计"。

从本质上讲,命令式编程是“冯·诺依曼机”运行机制的抽象,它的编程思想方式源于计算机指令的顺序排列。

(也就是说,过程化语言模拟的是计算机机器的系统构造,而并不是基于语言的使用者的个人能力和倾向。这一点我们应该都很清楚,比如我们最早曾经使用过的单片机的汇编语言。)

不管你用的是 C、C++ 还是 C#、Java,、Javascript、BASIC、 Python、 Ruby 等等,你都可以以这个方式写。

程序流程图是命令式语言进行程序编写的有效辅助手段。

在使用低级语言编程的年代,程序员站在直接使用指令的角度去思考,习惯按照自己的逻辑去写,指令之间可能共享数据,这其中最方便的写法就是需要用到哪块逻辑就 goto 过去执行一段代码,然后再 goto 到另外一个地方。当代码规模比较大时,就难以维护了,这种编程方式便是非结构化编程。

迪克斯特拉(E.W.dijkstra)在 1969 年提出结构化编程,摒弃了 goto 语句,而以模块化设计为中心,将待开发的软件系统划分为若干个相互独立的模块,这样使完成每一个模块的工作变得单纯而明确,为设计一些较大的软件打下了良好的基础。按照结构化编程的观点,任何算法功能都可以通过三种基本程序结构(顺序、选择和循环)的组合来实现。

命令式语言特别适合解决线性(或者说按部就班)的算法问题。它强调“自上而下(自顶向下)”“精益求精”的设计方式。这种方式非常类似我们的工作和生活方式,因为我们的日常活动都是按部就班的顺序进行的。

从编程语言特性、核心概念到编程范式  范式 编程语言 特性 核心 概念 第4张

命令式语言趋向于开发运行较快且对系统资源利用率较高的程序。命令式语言非常的灵活、强大,同时有许多经典应用范例,这使得程序员可以用它来解决多种问题。

命令式语言的不足之处就是它不适合某些种类问题的解决,例如那些非结构化的具有复杂算法的问题。问题出现在,命令式语言必须对一个算法加以详尽的说明,并且其中还要包括执行这些指令或语句的顺序。实际上,给那些非结构化的具有复杂算法的问题给出详尽的算法是极其困难的。

广泛引起争议和讨论的地方是:无条件分支,或goto语句,它是大多数过程式编程语言的组成部分,反对者声称:goto语句可能被无限地滥用;它给程序设计提供了制造混乱的机会。目前达成的共识是将它保留在大多数语言中,对于它所具有的危险性,应该通过程序设计的规定将其最小化。

命令式对实际事物处理一般可以拆分为以下两种模式:

流程驱动:类似 一般就是主动轮询 在干活中还要分心 主动去找活干 这样有空余的时间也完全浪费掉了采用警觉式者主动去轮询 ( polling),行为取决于自身的观察判断,是流程驱动的,符合常规的流程驱动式编程 ( Flow-Driven Programming)的模式。

事件驱动(有事我叫你,没事别烦我):类似 比如公司有一个oa系统 你干完活的时候只需要看下oa系统有没分配给你活 没有可以干自己的事 不用担心还有其他事没干完采用托付式者被动等通知 (notification),行为取决于外来的突发事件,是事件驱动 的,符合事件驱动式编程 ( Event-Driven Programming,简称 EDP)的模式。

其实,基于事件驱动的程序设计在图形用户界面(GUI)出现很久前就已经被应用于程序设计中,可是只有当图形用户界面广泛流行时,它才逐渐形演变为一种广泛使用的程序设计模式。

在过程式的程序设计中,代码本身就给出了程序执行的顺序,尽管执行顺序可能会受到程序输入数据的影响。

在事件驱动的程序设计中,程序中的许多部分可能在完全不可预料的时刻被执行。往往这些程序的执行是由用户与正在执行的程序的互动激发所致。

事件:就是通知某个特定的事情已经发生(事件发生具有随机性)。

事件与轮询:轮询的行为是不断地观察和判断,是一种无休止的行为方式。而事件是静静地等待事情的发生。事实上,在Windows出现之前,采用鼠标输入字符模式的PC应用程序必须进行串行轮询,并以这种方式来查询和响应不同的用户操做。

事件处理器:是对事件做出响应时所执行的一段程序代码。事件处理器使得程序能够对于用户的行为做出反映。

事件驱动常常用于用户与程序的交互,通过图形用户接口(鼠标、键盘、触摸板)进行交互式的互动。当然,也可以用于异常的处理和响应用户自定义的事件等等。

事件的异常处理比用户交互更复杂。

事件驱动不仅仅局限在GUI编程应用。但是实现事件驱动我们还需要考虑更多的实际问题,如:事件定义、事件触发、事件转化、事件合并、事件排队、事件分派、事件处理、事件连带等等。

其实,到目前为止,我们还没有找到有关纯事件驱动编程的语言和类似的开发环境。所有关于事件驱动的资料都是基于GUI事件的。

属于事件驱动的编程语言有:VB、C#、Java(Java Swing的GUI)等。它们所涉及的事件绝大多数都是GUI事件。

此种程化范式要求程序员用按部就班的算法看待每个问题。很显然,并不是每个问题都适合这种过程化的思维方式。这也就导致了其它程序设计范式出现,包括我们现在介绍的面向对象的程序设计范式。

当软件还非常简单的时候,我们只需要面向过程编程:

定义函数:

函数一、函数二、函数三、函数四。

定义数据:

数据一、数据二、数据三、数据四。

最后各种函数,数据的操作。

当软件发展起来后,我们的软件变得越来越大,代码量越来越多,复杂度远超Hello World的时候,我们的编写就有麻烦了:函数和数据会定义得非常多,面临两个问题。首先是命名冲突,英文单词也就那么几个,可能写着写着取名时就没合适的短词用了,为了避免冲突,只能把函数名取得越来越长。然后是代码重复,我们可以用函数里面调用函数的方法,但是函数调函数(比如一个功能多个方法(函数),几个功能混用方法)不便于维护。

4.2 面向对象编程范式 - 组织成类和继承层次人们将领域问题又开始映射成实体及关系(程序 = 实体 + 关系),而不再是数据结构和算法(过程)了,这就是面向对象编程,核心特点是封装、继承和多态。

面向对象程序设计(Object-oriented programming OOP)是种通过类、方法、对象和消息传递,来支持面向对象的程序设计范式。一个显著的特点是将数据和处理数据的代码组织到类,将类组织为可能的继承层次。对象则指的是类的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性,对象里的程序可以访问及经常修改对象相关连的数据。在面向对象程序编程里,程序会被设计成彼此相关的对象。

面向对象程序设计可以看作一种在程序中包含各种独立而又互相调用的对象的思想,这与传统的思想刚好相反:传统的程序设计主张将程序看作一系列函数的集合,或者直接就是一系列对计算机下达的指令。面向对象程序设计中的每一个对象都应该能够接受数据、处理数据并将数据传达给其它对象,因此它们都可以被看作一个小型的“机器”,即对象。即把事情交给最适合的对象去做。

面向对象和面向过程的区别最直观的比喻就如:摇(狗尾巴)和 狗.摇尾巴()的区别。

面向对象编程的三个基本概念:

封装,面向对象程序设计隐藏了某一方法的具体执行步骤,取而代之的是通过消息传递机制传送消息给它。经过深入的思考,做出良好的抽象,给出“完整且最小”的接口,并使得内部细节可以对外隐藏;

继承,在某种情况下,一个类会有“子类”。子类比原本的类(称为父类)要更加具体化,形成一个纵向的继承层次关系;

多态,指由继承而产生的相关的不同的类,其对象对同一消息会做出不同的响应;

使用面向对象编程语言,易于构建软件模型。因为,对象很类似乎很容易和现实世界上的所有事物和概念。

类,类是相似对象的集合。物以类聚——就是说明。每个对象都是其类中的一个实体。类中的对象可以接受相同的消息。换句话说:类包含和描述了“具有共同特性(数据元素)和共同行为(功能)”的一组对象。

接口,每个对象都有接口。接口说明类应该做什么但不指定如何做的方法。一个类可以有一个或多个接口。

方法,方法决定了某个对象究竟能够接受什么样的消息。面向对象的设计有时也会简单地归纳为“将消息发送给对象”。

面向对象技术一方面借鉴了哲学、心理学、生物学的思考方式,另一方面,它是建立在其他编程技术之上的,是以前的编程思想的自然产物。

如果说结构化软件设计是将函数式编程技术应用到命令式语言中进行程序设计,面向对象编程不过是将函数式模型应用到命令式程序中的另一途径,此时,模块进步为对象,过程龟缩到class的成员方法中。OOP的很多技术——抽象数据类型、信息隐藏、接口与实现分离、对象生成功能、消息传递机制等等,很多东西就是结构化软件设计所拥有的、或者在其他编程语言中单独出现。但只有在面向对象语言中,他们才共同出现,以一种独特的合作方式互相协作、互相补充。

如果按照面向过程的方法去设计汽车,汽车厂商需要采购一大堆零件,然后研究如何调试、调用这一大堆零件以完成一个功能。

从编程语言特性、核心概念到编程范式  范式 编程语言 特性 核心 概念 第5张

但是如果采用面向对象的方法去设计汽车,那么汽车厂商可以采用外包的方式交给专业的制动系统厂商来设计,只需要约定需要开放哪些public方法,输入什么输出什么就可以了。

三种面向对象方式:

① 静态函数包对象

将功能有联系的一批函数放在一起封装成一个类。这种类可以完全没有内部数据,也可以有数据。当有数据时,这些数据充当的其实就是配置(配置对于一个设计优秀的对象,是透明的,对象本身内部的函数根本不知道有配置这个东西,它只知道它需要的每一个数据在它new之后就已经存在this里了,随取随用。配置的给予或获取方式,是构建对象(new)时才需要去考虑的)这种对象的特点是,它的每一个函数(或方法)对这些数据都是只读的,所以不管方法有无被调用,被谁调用,被调用多少次,它也不会改变它的状态。

② 领域模型对象

这个概念是相对于传统的面向数据库的系统分析和设计而言的。数据库虽然只用了外键就描述了复杂的大千世界,但软件开发的难点在于适应变化,并且能够安全地修改。关系模型看似简单,但它却像一张蜘蛛网一样将所有table和栏位包在一块,牵一发而动全身,让你在修改时如履薄冰,一不小心就会顾此失彼,bug此起彼伏。而OO的封装特性则刚好可以用来解决这个问题。将业务数据整理成一个个独立的对象,让它们的数据只能被自己访问。留给外界的基本上只是一些接口(方法),数据除非万不得已,一个都不会公开。外界只能向它发送消息,它自己则通过修改自身数据来响应这种消息。这种对象与第一种对象刚好相反,它一定有数据,而且它的每一个函数存在的目的就是修改自己的数据。且每一次修改都是粗粒度的,每一次修改后,对象也还是处在valid状态。

③ 临时对象

其它用来解决过程式开发时,超多的变量,超复杂的流程而整理出来的小对象。这些对象一起协作,最后完成一个传统成千上万行的过程式代码才能完成的功能。例如现在要连接sql server执行查询语句并取得结果返回。不使用任何类库和工具,所有步骤都自己进行,例如解析协议、socket网络连接、数据包收发等。这时候从头到尾用一个个函数来完成,绝对没有先划分出一个个职责分明的对象,让各对象协作完成这件事情来得更简单。

但编程实践表明,并不是任何东西成为对象都是一件好事情。举一个Java中的蹩足的例子:Java中只有对象才能作为参数传入函数(当然还有原始类型primitive type)。所以为了将函数传递给另外一个函数,你需要将函数包裹在一个对象中,通常会用一个匿名类,因为这个类不会有其他作用,只是为了让Java的一切皆为对象的设计高兴。

Java拥有纯粹的面向对象概念。它从设计之初,就希望以一切皆为对象的纯对象模型来为世界建模。但发展到现在,Java中加入了越来越多非对象的东西。引入了闭包,从而获得了函数式编程中的一级函数;引入泛型,从而获得了参数化的类型。这可能暗示了,这个世界是如此得丰富多彩,使用单一模式为世界建模并不会成功。

4.3 声明式编程范式 - 重在what声明式编程,描述目标的性质,而非流程。声明式编程通常被看做是形式逻辑的理论,把计算看做推导。通常用作解决人工智能和约束满足问题。

声明式编程通过函数、推论规则或项重写规则,描述变量之间的关系。其语言解释器采用了一个固定的算法,以便从这些关系产生结果。

声明式编程是一个大的概念,其下包含一些有名的子编程范式:

约束式编程:变量之间的关系是在约束中说明的,定义了问题的解的范围。这些约束然后被应用程序来求解,以使得每个变量获得一个值,并让最多的约束得到满足。约束式编程经常被用作函数式编程、逻辑编程甚至命令式编程的补充。

领域专属语言:一些著名的声明式领域专属语言(DSLs)包括yacc语法分析器,编译说明语言Make,Puppet管理配置语言,正则表达式和SQL的一些子集(例如Select+queries等)。DSLs有时非常有用,并且不需要是图灵完全的,这往往让其很容易以一种纯宣告式的方式来表达。很多文本标记语言例如HTML、MXML、XAML和XSLT往往是声明式的。

函数式编程:函数式编程,特别是纯函数式编程,尝试最小化状态带来的副作用,因此被认为是宣告式的。大多数函数式编程语言,例如Scheme、Clojure、Haskell、OCaml、Standard+ML和Unlambda,允许副作用的存在。

逻辑式编程:逻辑式编程语言如Prolog声明关系并且对关系进行提问。同函数式编程一样,许多逻辑编程语言允许副作用的存在。

声明式编程是以数据结构的形式来表达程序执行的逻辑。它的主要思想是告诉计算机应该做什么,但不指定具体要怎么做。

SQL 语句就是最明显的一种声明式编程的例子,例如:

SELECT * FROM collection WHERE num > 5除了 SQL,网页编程中用到的 HTML 和 CSS 也都属于声明式编程。

通过观察声明式编程的代码我们可以发现它有一个特点是它不需要创建变量用来存储数据。

另一个特点是它不包含循环控制的代码如 for、while。

函数式编程和声明式编程是有所关联的,因为他们思想是一致的:即只关注做什么而不是怎么做。但函数式编程不仅仅局限于声明式编程。

4.4 函数式编程范式-通过数学函数表达式的方式来避免状态和可变的数据函数式编程(functional programming)或称函数程序设计、泛函编程,是一种编程范式,它将计算机运算视为函数运算,并且避免使用程序状态以及易变对象。其中,λ演算(lambda calculus)为该语言最重要的基础。而且,λ演算的函数可以接受函数当作输入(引数)和输出(传出值)。

函数式编程关心类型(代数结构)之间的关系,命令式编程关心解决问题的步骤。函数式编程中的lambda可以看成是两个类型之间的关系,一个输入类型和一个输出类型。lambda演算就是给lambda表达式一个输入类型的值,则可以得到一个输出类型的值,这是一个计算,计算过程满足 -等价和 -规约。函数式编程的思维就是如何将这个关系组合起来,用数学的构造主义将其构造出你设计的程序

比起命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。

命令式编程是面向计算机硬件的抽象,有变量(对应着存储单元),赋值语句(获取,存储指令),表达式(内存引用和算术运算)和控制语句(跳转指令),一句话,命令式程序就是一个冯诺依曼机的指令序列。

而函数式编程是面向数学的抽象,将计算描述为一种表达式求值,一句话,函数式程序就是一个表达式。

函数式编程最重要的特点是“函数第一位”,即函数可以出现在任何地方,比如你可以把函数作为参数传递给另一个函数,不仅如此你还可以将函数作为返回值。

4.4.1 函数式编程的本质

函数式编程中的函数这个术语不是指计算机中的函数(实际上是Subroutine),而是指数学中的函数,即自变量的映射。也就是说一个函数的值仅决定于函数参数的值,不依赖其他状态。比如sqrt(x)函数计算x的平方根,只要x不变,不论什么时候调用,调用几次,值都是不变的。

从编程语言特性、核心概念到编程范式  范式 编程语言 特性 核心 概念 第6张

在函数式语言中,函数作为一等公民,可以在任何地方定义,在函数内或函数外,可以作为函数的参数和返回值,可以对函数进行组合。

纯函数式编程语言中的变量也不是命令式编程语言中的变量,即存储状态的单元,而是代数中的变量,即一个值的名称。变量的值是不可变的(immutable),也就是说不允许像命令式编程语言中那样多次给一个变量赋值。比如说在命令式编程语言我们写“x = x + 1”,这依赖可变状态的事实,拿给程序员看说是对的,但拿给数学家看,却被认为这个等式为假。

函数式语言的如条件语句,循环语句也不是命令式编程语言中的控制语句,而是函数的语法糖,比如在Scala语言中,if else不是语句而是三元运算符,是有返回值的。

严格意义上的函数式编程意味着不使用可变的变量,赋值、循环和其他命令式控制结构进行编程。

从理论上说,函数式语言也不是通过冯诺伊曼体系结构的机器上运行的,而是通过λ演算来运行的,就是通过变量替换的方式进行,变量替换为其值或表达式,函数也替换为其表达式,并根据运算符进行计算。λ演算是图灵完全(Turing completeness)的,但是大多数情况,函数式程序还是被编译成(冯诺依曼机的)机器语言的指令执行的。

4.4.2 函数式编程的特性

函数是"一等公民":函数优先,和其他数据类型一样。

只用"表达式",不用"语句":通过表达式(expression)计算过程得到一个返回值,而不是通过一个语句(statement)修改某一个状态。

无副作用:不污染变量,同一个输入永远得到同一个数据。

不可变性:不修改变量,返回一个新的值。

由于变量值是不可变的,对于值的操作并不是修改原来的值,而是修改新产生的值,原来的值保持不变。

通常来说,算法都有递推(iterative)和递归(recursive)两种定义。

由于变量不可变,纯函数编程语言无法实现循环,这是因为For循环使用可变的状态作为计数器,而While循环或DoWhile循环需要可变的状态作为跳出循环的条件。因此在函数式语言里就只能使用递归来解决迭代问题,这使得函数式编程严重依赖递归。

函数式语言当然还少不了以下特性:

高阶函数(Higher-order function):就是参数为函数或返回值为函数的函数。有了高阶函数,就可以将复用的粒度降低到函数级别,相对于面向对象语言,复用的粒度更低。

偏应用函数(Partially Applied Functions):一个函数接收一个有多个参数的函数,返回一个需要较少参数的函数。偏函数将一到多个参数在内部固定,然后返回新函数,返回的函数接收剩余的参数完成函数的应用。

柯里化(Currying):输入一个有多个参数的函数, 返回一个只接收单个参数的函数。

闭包(Closure):闭包就是有权访问另一个函数作用域中变量的函数。闭包的三个特性:

① 闭包是定义在函数中的函数 。

② 闭包能访问包含函数的变量。

③ 即使包含函数执行完了, 被闭包引用的变量也得不到释放。

4.4.3 函数式编程的好处

由于命令式编程语言也可以通过类似函数指针的方式来实现高阶函数,函数式的最主要的好处主要是不可变性带来的。没有可变的状态,函数就是引用透明(Referential transparency)的和没有副作用(No Side Effect)。

函数即不依赖外部的状态也不修改外部的状态,函数调用的结果不依赖调用的时间和位置,这样写的代码容易进行推理,不容易出错。这使得单元测试和调试都更容易。

由于(多个线程之间)不共享状态,不会造成资源争用(Race condition),也就不需要用锁来保护可变状态,也就不会出现死锁,这样可以更好地并发起来,尤其是在对称多处理器(SMP)架构下能够更好地利用多个处理器(核)提供的并行处理能力。

函数式编程语言还提供惰性求值-Lazy evaluation,也称作call-by-need,是在将表达式赋值给变量(或称作绑定)时并不计算表达式的值,而在变量第一次被使用时才进行计算。这样就可以通过避免不必要的求值提升性能。

函数式编程语言一般还提供强大的模式匹配(Pattern Match)功能。在函数式编程语言中可以定义代数数据类型(Algebraic data type),通过组合已有的数据类型形成新的数据类型,如在Scala中提供case class,代数数据类型的值可以通过模式匹配进行分析。

函数式编程天生亲和单元测试(特别是黑盒测试),因为FP关注就是输入与输出。反观Java或者C++,仅仅检查函数的返回值是不够的:代码可能修改外部状态值,因此我们还需要验证这些外部的状态值的正确性。在FP语言中呢,就完全不需要。

调试查错方面,因为FP程序中的错误不依赖于之前运行过的不相关的代码。而在一个指令式程序中,一个bug可能有时能重现而有些时候又不能。因为这些函数的运行依赖于某些外部状态, 而这些外部状态又需要由某些与这个bug完全不相关的代码通过某个特别的执行流程才能修改。在FP中这种情况完全不存在:如果一个函数的返回值出错了,它一直都会出错,无论你之前运行了什么代码。而整个程序就是函数接龙。

以上几种范式的简单比较↓

从编程语言特性、核心概念到编程范式  范式 编程语言 特性 核心 概念 第7张

4.5 元编程(Metaprogramming)元编程(Metaprogramming),简称MP。此处的前缀‘meta-’常译作‘元’,其实就是‘超级’、‘形而上’的意思。比如,元数据(Metadata)是关于数据的数据,元对象(Metaobject)是关于对象的对象,依此类推,元编程自然是关于程序的程序,或者说是编写、操纵程序的程序。

元编程又称超编程,是指某些计算机程序能够编写和操纵其他程序(或者自身),或者在运行时完成部分本应在编译时完成的工作。编写元程序的语言称之为元语言。被操纵的程序的语言称之为“目标语言”。

一门编程语言同时也是自身的元语言的能力称之为「反射」或者「自反」。反射是促进元编程的一种很有价值的语言特性。支持泛型编程的语言也使用元编程能力。元编程通常通过两种方式实现。

其一是通过应用程序编程接口(APIs)将运行时引擎的内部信息暴露于编程代码。

另一种是动态执行包含编程命令的字符串表达式。因此,“程序能够编写程序”。虽然两种方式都能用于同一种语言,但大多数语言趋向于偏向其中一种。

元程序将程序作为数据来对待,能自我发现、自我赋权和自我升级,有着其他程序所不具备的自觉性、自适应性和智能性,可以说是一种最高级的程序。

编译器本身就是元编程的典型范例——把高级语言转化为汇编语言或机器语言的程序,也就是能写程序的程序。

元编程的例子比比皆是:许多IDE如Visual Studio、Delphi、Eclipse 等均能通过向导、拖放控件等方式自动生成源码;UML 建模工具将类图转换为代码;Servlet 引擎将JSP 转换为Java 代码;包括Spring、Hibernate、XDoclet在内的许多框架和工具都能从配置文件、annotation/attribute 等中产生代码。

另外,除了在编译期间生成源代码的静态元编程,还有能在运行期间修改程序的动态元编程。从低级的汇编语言到一些高级的动态语言如Perl、Python、Ruby、JavaScript、Lisp、Prolog等均支持此类功能。比如,许多脚本语言都提供eval函数,可以在运行时将字符串作为表达式来运算。

在传统的编程中,运算是动态的,但程序本身是静态的;在元编程中,二者都是动态的。元程序将程序作为数据来对待,能自我发现、自我赋权和自我升级,有着其他程序所不具备的自觉性、自适应性和智能性,可以说是一种最高级的程序。它要求编程者超越常规的编程思维,在一种崭新的高度上理解编程。

4.6 泛型编程在编程语言中,类型系统的出现主要是对容许混乱的操作加上了严格的限制,以避免代码以无效的数据使用方式编译或运行。例如,整数运算不可用于字符串;指针的操作不可用于整数上,等等。但是,类型的产生和限制,虽然对底层代码来说是安全的,但是对于更高层次的抽象产生了些负面因素。比如在C++语言里,为了同时满足静态类型和抽象,就导致了模板技术的出现,带来了语言的复杂性。

我们需要清楚地明白,编程语言本质上帮助程序员屏蔽底层机器代码的实现,而让我们可以更为关注于业务逻辑代码。但是因为,编程语言作为机器代码和业务逻辑的粘合层,是在让程序员可以控制更多底层的灵活性,还是屏蔽底层细节,让程序员可以更多地关注于业务逻辑,这很难两全,需要 trade-off。

所以,不同的语言在设计上都会做相应的取舍。比如,C语言偏向于让程序员可以控制更多的底层细节,而Java和Python则让程序员更多地关注业务功能的实现。而C++则是两者都想要,导致语言在设计上非常复杂。

泛型编程(Generic Programming),简称GP,为程语言提供了更高层级的抽象,即参数化类型。换句话说,就是把一个原本特定于某个类型的算法或类当中的类型信息抽象出来。这个抽象出来的概念在C++的STL(Standard Template Library)中就是模版(Template)。STL展示了泛型编程的强大之处,一出现就成为了C++的强大武器。除C++之外,C#,Java,Haskell等编程语言都引入了泛型概念。

STL 有3 要素:算法、容器和和迭代器。算法是一系列可行的步骤;容器是数据的集合,是抽象化的数组;迭代器是算法与容器之间的接口,是抽象化的指针。算法串联数据,数据实化算法。

泛型编程是一个稍微局部一些的概念,它仅仅涉及如何更抽象地处理类型,即参数化类型。这并不足以支撑起一门语言的核心概念。我们不会听到一个编程语言是纯泛型编程的,而没有其他编程范式。但正因为泛型并不会改变程序语言的核心,所以在大多数时候,它可以很好的融入到其他的编程方式中。C++,Scala,Haskell这些风格迥异的编程语言都支持泛型。泛型编程提供了更高的抽象层次,这意味着更强的表达能力。这对大部分编程语言来说都是一道美味佐餐美酒。

在Swift中,泛型得到广泛使用,许多Swift标准库是通过泛型代码构建出来的。例如Swift的数组和字典类型都是泛型集。这样的例子在Swift中随处可见。

其基本思想是:将算法与其作用的数据结构分离,并将后者尽可能泛化,最大限度地实现算法重用。这种泛化是基于模板(template)的参数多态(parametric polymorphism),相比OOP基于继承(inheritance)的子类型多态(subtyping polymorphism),不仅普适性更强,而且效率也更高。

template <class Iterator, class Act, class Test>void process(Iterator begin, Iterator end, Act act, Test test)// 对容器中在给定范围内(即起于begin止于end)所有满足给定条件的元//素(即test(元素)==true)进行处理(即act(元素)){ for ( ; begin != end; ++begin) // 从头至尾遍历容器内元素// 若当前元素满足条件,则对其采取行动 if (test(*begin)) act(*begin);}泛型编程不仅能泛化算法中涉及的概念(数据类型),还能泛化行为(函数、方法、运算)。

泛型编程是算法导向(Algorithm-Oriented)的,即以算法为起点和中心点,逐渐将其所涉及的概念(如数据结构、类)内涵模糊化、外延扩大化,将其所涉及的运算(函数、方法、接口)抽象化、一般化,从而扩展算法的适用范围。

5 过程 VS 结果:各类范式的比较命令式编程的主要思想是关注计算机执行的步骤,即一步一步告诉计算机先做什么再做什么。

声明式编程是以数据结构的形式来表达程序执行的逻辑。它的主要思想是告诉计算机应该做什么,但不指定具体要怎么做。SQL 语句就是最明显的一种声明式编程的例子。

函数式编程和声明式编程是有所关联的,因为他们思想是一致的:即只关注做什么而不是怎么做。但函数式编程不仅仅局限于声明式编程。

函数式编程最重要的特点是“函数第一位”,即函数可以出现在任何地方,比如可以把函数作为参数传递给另一个函数,不仅如此你还可以将函数作为返回值。

5.1 过程式编程(Procedural)

过程式编程和面向对象编程的区别并不在于是否使用函数或者类,也就是说用到类或对象的可能是过程式编程,只用函数而没有类的也可能是面向对象编程。那么他们的区别又在哪儿呢?

面向过程其实是最为实际的一种思考方式,可以说面向过程是一种基础的方法,它考虑的是实际地实现。一般的面向过程是从上往下步步求精,所以面向过程最重要的是模块化的思想方法。当程序规模不是很大时,面向过程的方法还会体现出一种优势。因为程序的流程很清楚,按照模块与函数的方法可以很好的组织。

关键部分实现代码:

def get_shannon_info(output): """获取shannon类型flash卡信息 """ def check_health(): time_left = float(sub_info["life_left"]) if time_left < DISK_ALARM_LIFETIME: message = "time left is less than {}%".format(DISK_ALARM_LIFETIME) return message temperature = float(sub_info["temperature"].split()[0]) if temperature > DISK_ALARM_TEMPERATURE: message = "temperature is over than {} C".format(DISK_ALARM_TEMPERATURE) return message return "healthy" result = {} all_info = _get_shannon_info(output) for info in all_info: sub_info = {} sub_info["available_capacity"] = info.get("disk_capacity", "") sub_info["device_name"] = info.get("block_device_node", "") sub_info["firmware_version"] = info.get("firmware_version", "") sub_info["interface"] = "PCIe" sub_info["life_left"] = str(info.get("estimated_life_left", "").replace("%", "")) sub_info["pcie_id"] = info.get("pci_deviceid", "") sub_info["pcie_length"] = "" sub_info["pcie_type"] = "" sub_info["physical_read"] = info.get("host_read_data", "") sub_info["physical_write"] = info.get("total_write_data", "") sub_info["serial_number"] = info.get("serial_number") sub_info["temperature"] = info.get("controller_temperature") sub_info["type"] = info["type"] sub_info["error_msg"] = check_health() sub_info["status"] = "ok" if sub_info["error_msg"] == "healthy" else "error" if sub_info["serial_number"]: result[sub_info["serial_number"]] = sub_info else: result[sub_info["device_name"]] = sub_info return result代码问题:

1.逻辑冗长,局部修改必须阅读整段代码2.对外部变量有依赖3.内部存在共享变量4.函数内部存在临时变量过程式的测试代码效果远不如函数式有效,过程式的实现逻辑过于冗长,导致测试效果并不够好。

5.2 面向对象编程(Object-Oriented)

并不是使用类才是面向对象编程。如果你专注于状态改变和封装抽象,你就是在用面向对象编程。类只是帮助简化面向对象编程的工具,并不是面向对象编程的要求或指示器。封装是一个过程,它分隔构成抽象的结构和行为的元素。封装的作用是分离抽象的概念接口及其实现。类只是帮助简化面向对象编程的工具,并不是面向对象编程的要求或指示器。

随着系统越来越复杂,系统就会变得越来越容易崩溃,分而治之,解决复杂性的技巧。面对对象思想的产生是为了让你能更方便地理解代码。有了那些封装、多态、继承,能让你专注于部分功能,而不需要了解全局。

关键部分实现代码:

class IFlash(six.with_metaclass(abc.ABCMeta)): def __init__(self): pass @abc.abstractmethod def collect(self): """收集flash卡物理信息 """ pass class FlashShannon(IFlash): """宝存的Flash卡 """ def __init__(self, txt_path, command, printer): super(FlashShannon, self).__init__() self.txt_path = txt_path self.command = command self.printer = printer def collect(self): result = {} for info in self._get_shannon_info(): life_left = str(info.get("estimated_life_left", "")).replace("%", "") temperature = info.get("controller_temperature", "") error_msg = self._get_health_message(life_left, temperature) sub_info = { "available_capacity": info.get("disk_capacity", ""), "device_name": info.get("block_device_node", ""), "firmware_version": info.get("firmware_version", ""), "interface": "PCIe", "life_left": life_left, "pcie_id": info.get("pci_deviceid", ""), "pcie_length": "", "pcie_type": "", "physical_read": info.get("host_read_data", ""), "physical_write": info.get("total_write_data", ""), "serial_number": info.get("serial_number", ""), "temperature": temperature, "type": info["type"], "error_msg": error_msg, "status": "ok" if error_msg == "healthy" else "error" } if sub_info["serial_number"]: result[sub_info["serial_number"]] = sub_info else: result[sub_info["device_name"]] = sub_info return result class FlashFio(IFlash): """fio的Flash卡 """ def __init__(self, txt_path): super(FlashFio, self).__init__() self.txt_path = txt_path def collect(self): disk_info = {} adapter_info = self._get_adapter_info() for info in adapter_info: serial_number = info["fio_serial_number"] for io in info["iomemory"]: data = self._combining_io_memory(io) data["serial_number"] = serial_number disk_info[serial_number] = data return disk_info5.3 函数式编程(Functional)

当谈论函数式编程,会提到非常多的“函数式”特性。提到不可变数据,第一类对象以及尾调用优化,这些是帮助函数式编程的语言特征。提到mapping(映射),reducing(归纳),piplining(管道),recursing(递归),currying(科里化),以及高阶函数的使用,这些是用来写函数式代码的编程技术。提到并行,惰性计算以及确定性,这些是有利于函数式编程的属性。

函数式编程最主要的原则是避免副作用,它不会依赖也不会改变当前函数以外的数据。

声明式的函数,让开发者只需要表达 “想要做什么”,而不需要表达 “怎么去做”,这样就极大地简化了开发者的工作。至于具体 “怎么去做”,让专门的任务协调框架去实现,这个框架可以灵活地分配工作给不同的核、不同的计算机,而开发者不必关心框架背后发生了什么。

关键部分实现代码:

def get_shannon_info(output): """查询shannon类型flash卡信息 """ lines = checks_string_split_by_function(output, is_shannon_flash_device) info = map(parser_shannon_info, lines) # map(lambda x: x.setdefault("type", "shannon"), info) for item in info: item["type"] = "shannon" data = map(modify_the_properties, info) return reduce(combining_data, map(convert_data_format, data))以上代码带有自描述性,通过函数名就可知在做什么,这也是函数式的一个特性: 代码是在描述要干什么,而不是怎么干。

测试代码:

@pytest.mark.parametrize("line, result", [("Found Shannon PCIE", False),("Found Shannon PCIE Flash car", False),("Found Shannon PCIE Flash card a", True),("Found Shannon PCIE Flash card", True),("Found Shannon PCIE Flash card.", True),])def test_is_shannon_flash_device(line, result): assert functional.is_shannon_flash_device(line) == result @pytest.mark.parametrize("line, result", [("a=1", True),("b=2", True),("c=2333", True),("d x=abcde", True),("Found Shannon PCIE=1", True),("abcdedfew=", False),("Found Shannon PCIE", False),(" =Found Shannon PCIE", False),("=Found Shannon PCIE", False),("Found Shannon PCIE=", False),("Found Shannon PCIE= ", False),])def test_is_effective_value(line, result): assert functional.is_effective_value(line) == result @pytest.mark.parametrize("line, result", [("a=1", {"a": "1"}),("b=2", {"b": "2"}),("a=a", {"a": "a"}),("abc=a", {"abc": "a"}),("abc=abcde", {"abc": "abcde"}),])def test_gets_the_index_name_and_value(line, result): assert functional.gets_the_index_name_and_value(line) == result @pytest.mark.parametrize("output, filter_func, result", [("abcd\nbcd\nabcd\nbcd\naa\naa", lambda x: "a" in x, ["abcd\nbcd", "abcd\nbcd", "aa", "aa"]),(open(os.path.join(project_path, "fixtures", "shannon-status.txt")).read(), functional.is_shannon_flash_device, [ open(os.path.join(project_path, "fixtures", "shannon-sctb.txt")).read(), open(os.path.join(project_path, "fixtures", "shannon-scta.txt")).read()])])def test_checks_string_split_by_function(output, filter_func, result): assert functional.checks_string_split_by_function(output, filter_func) == result命令式编程、面向对象编程、函数式编程,虽然受人追捧的时间点各不相同,但是本质上并没有优劣之分。

从编程语言特性、核心概念到编程范式  范式 编程语言 特性 核心 概念 第8张

最早是非结构化编程,指令可以随便跳,数据可以随便引用。后来有了结构化编程,人们把 goto 语句去掉了,约束了指令的方向性,过程之间是单向的,但数据却是可以全局访问的。再到面向对象编程的时候,人们干脆将数据与其紧密耦合的方法放在一个逻辑边界内,约束了数据的作用域,靠关系来查找。最后到函数式编程的时候,人们约束了数据的可变性,通过一系列函数的组合来描述数据从源到目标的映射规则的编排,在中间它是无状态的。可见,从左边到右边,是一路约束的过程。

从编程语言特性、核心概念到编程范式  范式 编程语言 特性 核心 概念 第9张

不难看出,编程语言的发展就是一个逐步远离计算机硬件,向着待解决的领域问题靠近的过程。

面向对象和函数式、过程式编程也不是完成独立和有严格的界限,在抽象出各个独立的对象后,每个对象的具体行为实现还是由函数式和过程式完成。

从编程语言特性、核心概念到编程范式  范式 编程语言 特性 核心 概念 第10张

现代的程序员应该很少有门派之见了,应该集百家之所长,学习其它范式(语言)的优秀设计理念,集成到自己的代码(产品、语言)中,形成多范式,提升工作效率。

6 编程语言、范式、核心概念人们把一个个具体的领域问题跑在图灵机模型上,然后做计算,而领域问题和图灵机模型之间有一个很大的 gap(What,How,Why),这是程序员主要发挥的场所。编程范式是程序员的思维底座,决定了设计元素和代码结构。程序员把领域问题映射到某个编程范式之上,然后通过编程语言来实现。显然,编程范式到图灵机模型的转化都由编译器来完成,同时这个思维底座越高,程序员做的就会越少。

从编程语言特性、核心概念到编程范式  范式 编程语言 特性 核心 概念 第11张

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

从编程语言特性、核心概念到编程范式  范式 编程语言 特性 核心 概念 第12张

与成百种编程语言相比,编程范式要少得多。多数范式之间仅相差一个或几个概念,比如函数编程范式,在加入了状态(state)之后就变成了面向对象编程范式。

过程式编程的核心概念在于模块化,在实现过程中使用了状态,依赖了外部变量,导致很容易影响附近的代码,可读性较少,后期的维护成本也较高。

函数式编程的核心概念在于“避免副作用”,不改变也不依赖当前函数外的数据。结合不可变数据、函数是第一等公民等特性,使函数带有自描述性,可读性较高。

面向对象编程的核心概念在于抽象,提供清晰的对象边界。结合封装、集成、多态特性,降低了代码的耦合度,提升了系统的可维护性。

两个面向对象的核心理念:

"Program to an ‘interface’, not an ‘implementation’."

使用者不需要知道数据类型、结构、算法的细节。

使用者不需要知道实现细节,只需要知道提供的接口。

利于抽象、封装、动态绑定、多态。

符合面向对象的特质和理念。

"Favor ‘object composition’ over ‘class inheritance’."

继承需要给子类暴露一些父类的设计和实现细节。

父类实现的改变会造成子类也需要改变。

我们以为继承主要是为了代码重用,但实际上在子类中需要重新实现很多父类的方法。

继承更多的应该是为了多态。

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

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

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

自然,相对而言,范式是更高层面的东西。

ref

/

冒号课堂:编程范式与 OOP 思想/郑晖著

-End-

评论列表暂无评论
发表评论
微信