微信限制:不能放置链接,代码样式比较奇怪,发布后不能更新。。。
编程最重要的事情,其实是让写出来的符号,能够简单地对实际或者想象出来的“世界”进行建模。一个程序员最重要的能力,是直觉地看见符号和现实物体之间的对应关系。 —— 王垠
为什么要写这篇文章 TL;DR
#1
一位朋友曾经曾经和我说:“千万别和不知道回调函数的人,解释什么是回调函数”。(见 如何浅显的解释回调函数)我本以为回调函数(callback function)是一个非常简单的概念,但和许多刚入门编程的人解释这个概念的时候,他们都觉得很费解。
直到现在,我才发现,原来要理解回调函数,就需要先接受 函数是一等公民(first-class function)的事实(函数和数据一样都可以被存储、传递),然后理解 高阶函数(higher-order function)的概念(函数可以作为参数传递到另一个函数里)。
这对于没有接触过函数式的人来说,简直是世界观的颠覆:
面向过程里,数据是数据、操作是操作
面向对象里,数据和操作放到对象里,属于对象的一部分
#2
为了批判面向对象里 “操作必须放到对象里” 的回调思想,写了一篇文章 回调 vs 接口(后来读到 陈硕 也有一篇类似的文章 以boost::function和boost:bind取代虚函数),但境界还不够,一直没有发现这个问题的本质——函数式 vs 面向对象。
#3
之前我也跟风写过一篇 高阶函数:消除循环和临时变量,讲的是如何使用 //// 之类的高阶函数。现在想才明白了问题的本质—— 使用函数式的方法,实现面向对象的 内部迭代(internal iteration)(属于 迭代器模式(iterator pattern)的一种),从而消除循环和迭代器临时变量。
#4
最近终于读懂了几篇 王垠的博客,大概能理解了文章的思想(虽然比较偏激,但论述非常严谨):
解密“设计模式”:批判(面向对象)设计模式(备份)
Purely functional languages and monads:批判“纯”函数式(备份)
编程的宗派:批判“纯”面向对象、“纯”函数式 和 说“各有各的好处”的“好好先生”(备份)
在《解密“设计模式”》提到,面向对象的 “设计模式” 是为了解决 “一切皆对象” 思想导致的问题。即使是 Erich Gamma(设计模式作者之一)粉丝的我,也深有感触:
由于 函数不是一等公民、不支持高阶函数,我们需要 创建型模式(creational patterns)(两种工厂、原型、创建者)
由于 不支持闭包、数据必须放到对象里,我们需要 结构型模式(structural patterns)(适配器、桥接、组合、装饰器、代理)
由于 操作必须放到对象里,我们需要 行为型模式(behavioral patterns)(命令、责任链、观察者、中介者、状态、策略、模板方法、迭代器、访问者、解释器)
Happy Coding
(by Erich Gamma)
本文总结一下自己的一些体会,并用一个例子加以阐述。
三种范式
我只接触过的三种 编程范式(programming paradigm):
面向过程(procedural programming)
面向对象(object-oriented programming)
函数式(functional programming)
面向过程
Algorithms + Data Structures = Programs(算法 + 数据结构 = 程序)—— Niklaus Wirth
数据和计算是两个不同的角色:
数据存储了程序运行的状态
计算 通过修改数据,变换运行的状态
参考:图灵机(Turing machine)
面向对象
封装(encapsulation):将数据和计算放到一起,并引入访问控制
继承(inheritance):共享数据和计算,避免冗余
多态(polymorphism):派发同一个消息(调用同一个方法),实现不同的操作(核心)
参考:浅谈面向对象编程
函数式
由于数据是有状态的(stateful),而计算是无状态的(stateless);所以需要将数据绑定(bind)到函数上,得到“有状态”的函数,即 闭包(closure)。通过构造、传递、调用 闭包,实现复杂的功能组合。
参考:λ 演算(lambda calculus)
例子:运行时动态选择计算操作
用 C++ 模拟一个简单的编辑器,点击界面上不同的按钮,执行不同的操作,并讨论各个范式如何在运行时动态选择计算操作(很简单,点击“新建”则新建文件,点击“打开”则打开文件,点击“保存”则保存文件):
新建打开保存
在程序的系统内:
点击事件的发送者是各个按钮
点击事件的接收者是程序所操作的文件
面向过程
代码链接
定义文件数据结构(用一个 封装起来):
定义文件操作(传递数据进行操作,返回 表示是否成功):
(点击事件的接收者)定义文件数据(实例化数据结构,存储程序的运行状态):
(点击事件的发送者)定义如何操作数据(跟据不同的 修改数据 ,选择不同的操作,变换程序的运行状态):
上边代码的主要问题是:事件的发送者直接依赖事件的接收者,即点击时直接用 调用 函数。这导致按钮不能复用:如果按钮点击后执行其他操作,就需要继续添加 语句。
为了让代码更灵活,我们可以使用面向对象的方法。
面向对象
代码链接
定义文件类(class)(将文件的数据和操作封装到一起,分别作为类的字段和方法;引入访问控制,隐藏数据,暴露操作;并通过异常(exception)表示是否成功):
定义命令接口 (利用 命令模式(command pattern),消除点击事件的接收者和发送者之间的依赖):
定义新建/打开/保存文件对应的实际命令 (实现 接口,执行实际的操作;存储上下文(context)数据,并在执行操作时使用):
(点击事件的接收者)定义文件对象(object)(实例化类,存储对应的数据和操作):
(在点击事件的接收者和发送者之间)定义中间层(indirection)(将点击事件的接收者 传入 的上下文;并利用 依赖注入(dependency injection)的方法,为不同的 分配不同的 ):
(点击事件的发送者)定义如何操作数据(通过中间层派发消息,利用多态机制,动态选择具体执行的操作):
在点击保存按钮时,消息的传递流程是:
命令模式最核心的地方就是:事件的发送者 发送 消息给 ,对于不同类型的 对象,会执行不同的操作,从而实现动态选择行为。这样,事件的发送者不依赖于事件的接收者 —— 按钮不关心自己被点击之后执行什么操作,照着命令去做就行。
为了解耦(decouple)事件的发送者和接收者,面向对象就引入了好几种不同的 设计行为型模式(behavioral patterns)。
这代码还可以化简吗?可以,用函数式的方法化简!
函数式
代码链接
复用上一个例子的文件类和对象(点击事件的接收者)。
重新定义 (将命令定义为一个函数):
利用新的 定义类似的中间层(将 作为上下文构造闭包,再分配给各个 ):
(点击事件的发送者)重新定义如何操作数据(通过中间层选择函数,并执行对应的函数):
和面向对象方法的区别在于:
面向对象 把数据和操作放到对象里
函数式 把数据和计算放到 lambda 里
对于 C++,上面的代码本质上是通过面向对象实现的:
是基于带有 抽象类,在构造时利用泛型技巧,抹除传入的 可调用(callable)对象的类型,仅保留调用的签名(原理 / 代码)
lambda 表达式 会被编译为带有 的类,并构造时捕获当前的上下文(类似前面的 );可以传入 封装为更抽象的可调用对象
写在最后
一切并不都是“对象”,一切也并不都是“函数”。最简单明了的,才是最好的。
有些东西本来就是有随时间变化的“状态”的,如果你偏要用“纯函数式”语言去描述它,当然你就进入了那些 monad 之类的死胡同。 ... 如果你进入另一个极端,一定要用对象来表达本来很纯的数学函数,那么你一样会把简单的问题搞复杂。 —— 王垠
本文仅是我的一些个人理解。如果有什么问题,欢迎交流。
Delivered under MIT License © 2019, BOT Man
领取专属 10元无门槛券
私享最新 技术干货