原文名称“面向对象编程语言中的函数式编程--为命令模式和访问者模式正名 ”
http://blog.csdn.net/shendl/article/details/2064218
函数式编程和命令式编程
函数式编程是最近被热炒的一个概念。国内外众多大牛纷纷发表文章,认为函数编程可能会再度兴起。搞得一向喜欢跟风的小弟我如坐针毡。因此,也抽空研究了一下函数式编程这个时髦的概念。
上个世纪,我曾经在图书馆借了一本介绍所有主要计算机语言的书,那本书简单得介绍过Lisp和其他语言的语法。其中提到,Lisp是一门函数语言。当然,那时对这句话没什么概念。
命令式编程是一种用程序状态描述计算的方法。使用这种范型的编程人员用语句改变程序状态。这就是为什么,像 Java 这样的程序是由一系列让计算机执行的命令 (或者语句) 所组成的。
另一方面,函数式编程是一种强调表达式的计算而非命令的执行的一种编程风格。表达式是用函数结合基本值构成的,它类似于用参数调用函数。
也就是说,函数式编程主要是函数调用,而不是其它的程序语句。
而命令式编程,是通过程序语句的执行运行的。程序语句的执行,会改变程序中保存的状态。
实际上,我们一般使用的命令式语言,如C++,Java,C#等的代码中,也可以看到大量的函数调用。
一个优秀的软件工程师使用面向对象编程语言编写出来的代码,除了少数的创建对象实例的代码外,大量的代码都是函数调用。
因此,尽管传统上认为C++,Java,C#等面向对象编程语言是命令式编程语言。但我们一样可以在面向对象编程语言中实现函数式编程风格!
实际上,可能你写的不少代码也是采用了函数式编程风格,只是你不知道罢了!
什么是函数编程?
在经常被引用的论文 “Why Functional Programming Matters”(请参阅 参考资料) 中,作者 John Hughes 说明了模块化是成功编程的关键,而函数编程可以极大地改进模块化。在函数编程中,编程人员有一个天然框架用来开发更小的、更简单的和更一般化的模块, 然后将它们组合在一起。函数编程的一些基本特点包括:
支持闭包和高阶函数。
支持懒惰计算(lazy evaluation)。
使用递归作为控制流程的机制。
加强了引用透明性。
没有副作用。
闭包和高阶函数和命令模式
闭包和高阶函数
函数编程支持函数作为第一类对象,有时称为 闭包或者 仿函数(functor)对象。实质上,闭包是起函数的作用并可以像对象一样操作的对象。与此类似,FP 语言支持 高阶函数。高阶函数可以用另一个函数(间接地,用一个表达式) 作为其输入参数,在某些情况下,它甚至返回一个函数作为其输出参数。这两种结构结合在一起使得可以用优雅的方式进行模块化编程,这是使用 FP 的最大好处。
看到这里,我想有设计模式经验的朋友一定会联想到“命令模式”。
是的!面向对象编程中的命令模式,不就是函数式编程中的闭包和告诫函数吗?!
命令模式
别名:Action动作模式,Transaction事务模式。我也叫它“参数回调模式”,因为本质上,命令模式和C的参数回调是一样的。
意图
|
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。
|
适用性
|
- 抽象出待执行的动作以参数化某对象,你可用过程语言中的回调(c a l l b a c k )函数表达这种参数化机制。所谓回调函数是指函数先在某处注册,而它将在稍后某个需要的时候被调用。C o m m a n d 模式是回调机制的一个面向对象的替代品。
- 在不同的时刻指定、排列和执行请求。一个C o m m a n d 对象可以有一个与初始请求无关的生存期。如果一个请求的接收者可用一种与地址空间无关的方式表达,那么就可将负责该请求的命令对象传送给另一个不同的进程并在那儿实现该请求。
- 支持取消操作。C o m m a n d 的E x c u t e 操作可在实施操作前将状态存储起来,在取消操作时这个状态用来消除该操作的影响。C o m m a n d 接口必须添加一个U n e x e c u t e 操作,该操作取消上一次E x e c u t e 调用的效果。执行的命令被存储在一个历史列表中。可通过向后和向前遍历这一列表并分别调用U n e x e c u t e 和E x e c u t e 来实现重数不限的“取消”和“重做”。
(这是 指,备忘录模式中,撤销管理器通过方法的参数,把状态加到管理器中呢?!同时,容器中的所有状态都是一个接口的实现类,然后,都可以执行相同的方法:
undo/redo。
)
- 支持修改日志,这样当系统崩溃时,这些修改可以被重做一遍。在C o m m a n d 接口中添加装载操作和存储操作,可以用来保持变动的一个一致的修改日志。从崩溃中恢复的过程包括从磁盘中重新读入记录下来的命令并用E x e c u t e 操作重新执行它们。
- 用构建在原语操作上的高层操作构造一个系统。这样一种结构在支持事务( t r a n s a c t i o n )的信息系统中很常见。一个事务封装了对数据的一组变动。C o m m a n d 模式提供了对事务进行建模的方法。C o m m a n d 有一个公共的接口,使得你可以用同一种方式调用所有的事务。同时使用该模式也易于添加新事务以扩展系统。
|
C++中STL和Boost等类库中广泛使用的仿函数类,也是命令模式的一种实现。
在面向过程编程语言,或者函数编程语言中,通过把函数指针作为函数的参数,可以实现参数回调。
在面向对象编程语言中,通过把某个仿函数接口的指针作为函数的参数,也可以实现类似于面向过程语言的函数参数的回调。
面向对象编程语言实现命令模式有几种变体。
如,可以把某个仿函数接口的指针作为类的一个实例变量保存在类中。
闭包和高阶函数和命令模式
函数式编程中的闭包,对应于命令模式中,用于回调的接口。这个接口封装了一个或者多个函数。
如:public interface Comparable<T>
有一个方法
int
|
compareTo( T o)
比较此对象与指定对象的顺序。
|
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
。
可以通过调用
Object
|
invoke( Object obj, Object... args)
对带有指定参数的指定对象调用由此
Method 对象表示的底层方法。
|
这个方法,调用所需要的函数。
这样,在java中,我们实际上可以使用Method类作为方法的参数,进行回调!
.NET中也有类似的机制。
另外,C#有一个关键字delegate,委派,这实际上也就是方法的面向对象的对等物。委派的声明,实际上就是方法的声明。是
C中函数指针/参数回调机制的直接对应物。
Delegate和Java的Method实际上是同一个东西。我们可以用Method来模拟C#的委派。
为访问者模式正名
意图
|
表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
|
适用性
|
- 一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作。
- 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而你想避免让这些操作“污染”这些对象的类。Vi s i t o r 使得你可以将相关的操作集中起来定义在一个类中。当该对象结构被很多应用共享时,用Vi s i t o r 模式让每个应用仅包含需要用到的操作。
- 定义对象结构的类很少改变,但经常需要在此结构上定义新的操作。改变对象结构类需要重定义对所有访问者的接口,这可能需要很大的代价。如果对象结构类经常改变,那么可能还是在这些类中定义这些操作较好。
|
访问者模式,就是把一个类分成两个部分。一个部分是数据。把它们封装到一个类中。这种类常被叫做“数据容器类”。
另一部分是对数据的操作。根据不同的关注点,把函数分成一个或者几个接口。
在使用时,把接口和数据容器类组合起来使用。
最典型的访问者模式,就是著名的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风格的代码。