编程范式是程序员的思维底座,决定了设计元素和代码结构。程序员把领域问题映射到某个编程范式之上,然后通过编程语言来实现。显然,编程范式到图灵机模型的转化都由编译器来完成,同时这个思维底座越高,程序员做的就会越少。
Python 支持多种编程范式(面向对象、面向过程、函数式等等)
编程范式的历史
机器语言和汇编语言极其难以维护,人们开始寻求与机器无关的高级语言,较为恰当的抽象,屏蔽了计算机硬件的诸多细节,人们开发的程序规模逐渐膨胀,更进一步的远离计算机硬件,靠近解决问题的领域。
从结构化编程到面向对象编程,再到函数式编程,离图灵机模型越来越远,但抽象程度越来越高,与领域问题的距离越来越近。
面向对象编程
- 动机:随着软件种类的不断增多,软件规模的不断膨胀,人们希望可以更小粒度的对软件进行复用和裁剪。
将全局数据拆开,并将数据与其紧密耦合的方法放在一个逻辑边界内,这个逻辑边界就是对象。用户只能访问对象的 public 方法,而看不到对象内部的数据。对象将数据和方法天然的封装在一个逻辑边界内,可以整体直接复用而不用做任何裁剪或隐式关联。
人们将领域问题又开始映射成实体及关系(程序 = 实体 + 关系),而不再是数据结构和算法(过程)了,这就是面向对象编程。所以说,面向对象编程的核心特点是:封装、继承和多态。
封装是面向对象的根基,它将紧密相关的信息放在一起,形成一个逻辑单元。我们要隐藏数据,基于行为进行封装,最小化接口,不要暴露实现细节。
继承分为两种,即实现继承和接口继承。实现继承是站在子类的视角看问题,而接口继承是站在父类的视角看问题。很多程序员把实现继承当作一种代码复用的方式,但这并不是一种好的代码复用方式,推荐使用组合。
对于面向对象而言,多态至关重要,接口继承是常见的一种多态的实现方式。正因为多态的存在,软件设计才有了更大的弹性,能够更好地适应未来的变化。只使用封装和继承的编程方式,我们称之为基于对象编程,而只有把多态加进来,才能称之为面向对象编程。可以这么说,面向对象设计的核心就是多态的设计。
面向对象编程的优点:
- 对象自封装数据和行为,利于理解和复用。
- 对象作为“稳定的设计质料”,适合广域使用。
- 多态提高了响应变化的能力,进一步提升了软件规模。
- 对设计的理解和演进优先是对模型和结构的理解和调整。不要一上来就看代码,面向对象的代码看着看着很容易断,比如遇到虚接口,就跟不下去了。通常是先掌握模型和结构,然后在结构中打开某个点的代码进行查看和修改。请记住,先模型,再接口,后实现。
面向对象编程的缺点:
- 业务逻辑碎片化,散落在离散的对象内。类的设计遵循单一职责原则,为了完成一个业务流程,需要在多个类中跳来跳去。
- 行为和数据的不匹配协调,即所谓的贫血模型和充血模型之争。后来发现可通过 DCI(Data、Context 和 Interactive)架构来解决该问题。
- 面向对象建模依赖工程经验,缺乏严格的理论支撑。面向对象建模回答了从领域问题如何映射到对象模型,但一般只是讲 OOA 和 OOD 的典型案例或最佳实践,属于归纳法范畴,并没有严格的数学推导和证明。
函数式编程
函数式编程的起源是数学家 Alonzo Church 发明的 Lambda 演算(Lambda calculus,也写作 λ-calculus)。所以,Lambda 这个词在函数式编程中经常出现,你可以把它简单地理解成匿名函数。
函数式编程有很多特点:
函数是一等公民。一等公民的含义:(1)它可以按需创建;(2)它可以存储在数据结构中;(3)它可以当作参数传给另一个函数;(4)它可以当作另一个函数的返回值。
纯函数。所谓纯函数,是符合下面两点的函数:(1)对于相同的输入,返回相同的输出;(2)没有副作用。
惰性求值。惰性求值是一种求值策略,它将求值的过程延迟到真正需要这个值的时候。
不可变数据。函数式编程的不变性主要体现在值和纯函数上。值类似于 DDD 中的值对象,一旦创建,就不能修改,除非重新创建。值保证不会显式修改一个数据,纯函数保证不会隐式修改一个数据。当你深入学习函数式编程时,会遇到无副作用、无状态和引用透明等说法,其实都是在讨论不变性。
递归。函数式编程用递归作为流程控制的机制,一般为尾递归。
函数式编程还有两个重要概念:高阶函数和闭包。所谓高阶函数,是指一种比较特殊的函数,它们可以接收函数作为输入,或者返回一个函数作为输出。闭包是由函数及其相关的引用环境组合而成的实体,即闭包 = 函数 + 引用环境。
闭包有独立生命周期,能捕获上下文(环境)。站在面向对象编程的角度,闭包就是只有一个接口(方法)的对象,即将单一职责原则做到了极致。可见,闭包的设计粒度更小,创建成本更低,很容易做组合式设计。在面向对象编程中,设计粒度是一个 Object,它可能还需要拆,但你可能已经没有意识再去拆,那么上帝类大对象就会存在了,创建成本高。在函数式编程中,闭包给你一个更精细化设计的能力,一次就可以设计出单一接口的有独立生命周期的可以捕获上下文的原子对象,天然就是易于组合易于重用的,并且是易于应对变化的。
有一句话说的很好:闭包是穷人的对象,对象是穷人的闭包。有的语言没有闭包,你没有办法,只能拿对象去模拟闭包。又有一些语言没有对象,但单一接口不能完整表达一个业务概念,你没有办法,只能将多个闭包组合在一起当作对象用。
对于函数式编程,数据是不可变的,所以一般只能通过模式匹配和递归来完成图灵计算。当程序员选择将函数式编程作为思维底座时,就需要解决如何将领域问题映射到数据和函数(程序 = 数据 + 函数)。
函数式设计的思路就是高阶函数与组合,背后是抽象代数那一套逻辑。下面这张图是关于高阶函数的,左边是将函数作为输入,右边是将函数作为输出:
可见,函数式设计的基本方法为:借助闭包的单一接口的标准化和高阶函数的可组合性,通过规则串联设计,完成数据从源到结果的映射描述。这里的映射是通过多个高阶函数的形式化组合完成,描述就像写数学公式一样放在那,等源数据从一头传入,然后经过层层函数公式的处理,最后变成你想要的结果。数据在形式化转移的过程中,不仅仅包括数据本身,还包括规则的创建、返回和传递。
通过公式的层层嵌套完成一个算法的描述,所以核心就是设计有哪些高阶函数以及它们的组合规则,这是函数式设计中最难的,就是抽象代数的部分。
函数式编程的优点:
高度的抽象,易于扩展。函数式编程是数据化表达,非常抽象,在表达范围内是易于扩展的。
声明式表达,易于理解。
形式化验证,易于自证。
不可变状态,易于并发。数据不可变不是并发的必要条件,不共享数据才是,但不可变使得并发更加容易。
函数式编程的缺点:
对问题域的代数化建模门槛高,适用域受限。现实是复杂的,不是在每个方面都是自洽的,要找到一套完整的规则映射是非常困难的。在一些狭窄的领域,可能找得到,而一旦扩展一下,就会破坏该狭窄领域,你发现以前找到的抽象代数建模方式就不再适用了。
在图灵机上性能较差。函数式编程增加了很多中间层,它的规则描述和惰性求值等使得优化变得困难。
不可变的约束造成了数据泥团耦合。领域对象是有状态的,这些状态只能通过函数来传递,导致很多函数有相同的入参和返回值。
闭包接口粒度过细,往往需要再组合才能构成业务概念。