函数式编程和命令式编程 函数式编程是最近被热炒的一个概念。国内外众多大牛纷纷发表文章,认为函数编程可能会再度兴起。搞得一向喜欢跟风的小弟我如坐针毡。因此,也抽空研究了一下函数式编程这个时髦的概念。 上个世纪,我曾经在图书馆借了一本介绍所有主要计算机语言的书,那本书简单得介绍过Lisp和其他语言的语法。其中提到,Lisp是一门函数语言。当然,那时对这句话没什么概念。 命令式编程是一种用程序状态描述计算的方法。使用这种范型的编程人员用语句改变程序状态。这就是为什么,像 Java 这样的程序是由一系列让计算机执行的命令 (或者语句) 所组成的。 另一方面,函数式编程是一种强调表达式的计算而非命令的执行的一种编程风格。表达式是用函数结合基本值构成的,它类似于用参数调用函数。 也就是说,函数式编程主要是函数调用,而不是其它的程序语句。 而命令式编程,是通过程序语句的执行运行的。程序语句的执行,会改变程序中保存的状态。 实际上,我们一般使用的命令式语言,如C++,Java,C#等的代码中,也可以看到大量的函数调用。 一个优秀的软件工程师使用面向对象编程语言编写出来的代码,除了少数的创建对象实例的代码外,大量的代码都是函数调用。 因此,尽管传统上认为C++,Java,C#等面向对象编程语言是命令式编程语言。但我们一样可以在面向对象编程语言中实现函数式编程风格! 实际上,可能你写的不少代码也是采用了函数式编程风格,只是你不知道罢了! 什么是函数编程? 在经常被引用的论文 “Why Functional Programming Matters”(请参阅 参考资料) 中,作者 John Hughes 说明了模块化是成功编程的关键,而函数编程可以极大地改进模块化。在函数编程中,编程人员有一个天然框架用来开发更小的、更简单的和更一般化的模块, 然后将它们组合在一起。函数编程的一些基本特点包括: 支持闭包和高阶函数。 支持懒惰计算(lazy evaluation)。 使用递归作为控制流程的机制。 加强了引用透明性。 没有副作用。 闭包和高阶函数和命令模式 闭包和高阶函数 函数编程支持函数作为第一类对象,有时称为 闭包或者 仿函数(functor)对象。实质上,闭包是起函数的作用并可以像对象一样操作的对象。与此类似,FP 语言支持 高阶函数。高阶函数可以用另一个函数(间接地,用一个表达式) 作为其输入参数,在某些情况下,它甚至返回一个函数作为其输出参数。这两种结构结合在一起使得可以用优雅的方式进行模块化编程,这是使用 FP 的最大好处。 看到这里,我想有设计模式经验的朋友一定会联想到“命令模式”。 是的!面向对象编程中的命令模式,不就是函数式编程中的闭包和告诫函数吗?! 命令模式 别名:Action动作模式,Transaction事务模式。我也叫它“参数回调模式”,因为本质上,命令模式和C的参数回调是一样的。
C++中STL和Boost等类库中广泛使用的仿函数类,也是命令模式的一种实现。 在面向过程编程语言,或者函数编程语言中,通过把函数指针作为函数的参数,可以实现参数回调。 在面向对象编程语言中,通过把某个仿函数接口的指针作为函数的参数,也可以实现类似于面向过程语言的函数参数的回调。 面向对象编程语言实现命令模式有几种变体。 如,可以把某个仿函数接口的指针作为类的一个实例变量保存在类中。 闭包和高阶函数和命令模式 函数式编程中的闭包,对应于命令模式中,用于回调的接口。这个接口封装了一个或者多个函数。 如:public interface Comparable<T> 有一个方法
Comparable接口就是一个闭包。它的具体实现类就是闭包的具体实现。 Comparable接口仅仅封装了一个方法。它常常用作方法的参数,方法体内进行调用参数的compareTo方法。 使用了回调参数的那个函数,就是函数式编程中的“高阶函数”。 为命令模式正名 一直以来,在面向对象编程语言的世界中,对GOF提出的命令模式的非议一直不断。那些纯粹的面向对象编程专家看到命令模式中那些个只是封装了一个函数的接口感到恐惧。 那是函数,还是类?这还是OOP吗? 面向对象编程,使用接口描述世界。纯粹的OOP语言,如Java,Ruby和C#中,只有类是第一类的语言元素。函数都是封装在一个个类中的。 但是,类只是我们对于世界的一种描述,一种观点。对于世界中那些纯粹的功能,怎么办?难道非要给它们加上它们并不需要的数据吗? 还是还函数以本来面目吧!使用命令模式,用一个接口把函数封装起来!实话实说:我们就是需要一个函数!怎么样?不行吗! 据支持函数式编程的大牛们说,函数式编程比命令式编程更加强大。而且这是有数学依据的。到底是不是真的,我不知道。我对理论没什么兴趣。 至少,一贯支持命令模式的我,为命令模式找到了强援。如果面向对象社区不要命令模式,那么我们就索性声称命令模式就是函数式编程得了! 命令模式是面向对象编程语言中模仿函数式编程的一种模式! 下面再说说命令模式其他的实现方式 除了使用一个仿函数接口(或者说函数的包装接口)外,对于某些语言,命令模式还有其他的实现方式。 Java和C#都有很强大的动态能力。它们的反射机制可以动态得到类、函数、属性。 在Java中,Method类就是对所有函数的封装类。 public final class Method extends AccessibleObject implements GenericDeclaration, Member Method 提供关于类或接口上单独某个方法(以及如何访问该方法)的信息。所反映的方法可能是类方法或实例方法(包括抽象方法)。Method 允许在匹配要调用的实参与底层方法的形参时进行扩展转换;但如果要进行收缩转换,则会抛出 IllegalArgumentException 。 可以通过调用 这个方法,调用所需要的函数。 这样,在java中,我们实际上可以使用Method类作为方法的参数,进行回调! .NET中也有类似的机制。 另外,C#有一个关键字delegate,委派,这实际上也就是方法的面向对象的对等物。委派的声明,实际上就是方法的声明。是C中函数指针/参数回调机制的直接对应物。 Delegate和Java的Method实际上是同一个东西。我们可以用Method来模拟C#的委派。 为访问者模式正名
访问者模式,就是把一个类分成两个部分。一个部分是数据。把它们封装到一个类中。这种类常被叫做“数据容器类”。 另一部分是对数据的操作。根据不同的关注点,把函数分成一个或者几个接口。 在使用时,把接口和数据容器类组合起来使用。 最典型的访问者模式,就是著名的DAO数据访问对象模式。 把数据库表的字段用一个数据容器类封装起来。对类对象的操作,用一个DAO接口封装起来。 如对用户表数据的操作。创建2个类: User类: Integer id String name IUserDao接口 List<User> queryAll(); User query(Integer id); Void delete(Integer id); Void delete(User user); Void reLoad(User user); 调用者: UserService类: IUserDao dao; List<User> query (条件){ dao的各个函数调用。 } 这些DAO类,封装了一些函数,本身不保存数据。操作时所有的数据都在各个函数中通过参数传入。 访问者模式中的访问者接口,仅仅封装了多个函数,而没有数据,也可以看作是函数式编程的闭包。 调用访问者的函数的函数,就是函数式编程的高阶函数。 访问者模式,和命令模式一样,在OOP社区也饱受争议。一些程序员认为,访问者模式是罪大恶极,恶贯满盈! 本来一个好好的既包含数据,又包含操作数据的函数的类,被该死的访问者模式硬生生掰成2个甚至多个类。 数据容器类,只有数据,没有操作,那还能算是真正的类吗?! 访问者类,只有函数,没有数据,和命令模式一个样,能算是真正的类吗?! 实际上,在程序中,往往数据的结构是最稳定的。而操作数据的函数,由于业务上的原因,是非常不稳定的。因此,访问者模式把数据和操作数据的函数分开。并且让访问者来访问数据。而数据并不知道访问者对象的存在和它们有哪些函数。(GOF提出的访问者模式中,访问者和被访问者<数据容器类>是互相关联的关系,我认为这样做不好。应该是访问者知道被访问者这样的单向关系。数据没有必要知道自己会被怎样操作。它只管保存数据和公开数据即可!) 而且,类不同的用户,需要对数据进行的操作是不一样的。不同的用户,需要不同的访问者函数。如果不使用访问者模式,那么所有用户将不得不得到作用于数据上的所有方法。而其中绝大部分是该用户并不需要的。这显然是浪费,逻辑上也说不过去。 现在,访问者模式和函数式编程也攀上了关系。如果OOP社群不要访问者模式,那么,我们可以不好意思的说:其实…我用的是函数式编程:) 结论 兄弟,你用过命令模式,访问者模式,DAO模式吗?那么你已经在OOP语言中使用了函数式编程。 你写过只有函数,没有数据的类吗?那么你已经写过函数式编程了。 你用过Method类的invoke方法吗?当时你在用函数式编程。 你用过delegate吗?当时你也在用函数式编程。 你用过函数指针吗?当时你在用函数式编程。 类,必须要有数据吗?不必! 类,必须要有函数吗?也不必! 是故,你可以用C而不是C++写出OOP风格的代码。 |
|