Tag Archives: 设计模式

状态模式、面向对象与函数式

最近好友墙裂向我推荐Clojure,就仔细看了一下这门Lisp方言。《黑客与画家》中经常提到的一个概念就是Lisp等语言的比其他的编程语言在表达能力上更加强大。我对Clojure仅仅看了两天,不过Javascript倒是用了两年了,Js的设计中借鉴了很多函数式语言的特性,又同时借鉴了一下Java的语法,Java->Javascript->Lisp倒是形成了一个不错的学习曲线。

设计模式是OO编程中必不可少的一部分,但同时,很多人又批评这完全是由于OO语言自身的缺陷导致的。最著名的莫过于Peter Norvig在1996年的一份讲稿: Design Patterns in Dynamic Programming。这里指出23种模式中,有16种模式在Lisp是语言本来就有的特性或者实现起来更加简单的(16 of the 23 patterns in Design Patterns were “invisible or simpler” in Lisp)。先来看一个简单的模式,状态模式。

状态模式

我相信使用Java或者C++的童鞋对状态模式一定不陌生,简单易懂,经常用到。

状态模式

如上图。Context类有多个状态,有一个currentState属性,当调用Context.handle时,委托给currentState.handle()。通过实现State接口,可以定义多种状态,通过切换具体的object,可以实现状态切换。

是否需要这么多State类

状态模式的精髓在于,通过不同的State类来代表不同的状态。等等,真的是类嘛?实际上是State各个实现类实例化之后的对象。什么是一个对象?对象的概念在很多语言中都有,无论在哪,都可以看成是一个数据集合,同时具有多种方法,即对象是有行为的属性集合。在状态模式中,我们实际使用中,仅仅使用了一个object,并没有多次使用这个类(作为object的模板),即没有出现也不需要出现具有不同属性的object。那为什么不直接定义object呢?至少第一眼看上去,可以节省额外的Class开销。

这里我们回顾一下,面向对象编程三大特性:

  1. 封装
  2. 继承
  3. 多态

如果我们直接定义一个个object(或者function),放弃强类型,这三个特性会怎么样?

封装

这个不会有太大的改变,因为不同的object/function仅仅暴露方法就行了,甚至自己就是那个方法,内部运作仍然可以封装。

继承

如果使用object,将不需要一个State接口。接口的意义一方面是多态,即让currentState引用可以指向多个实现类的对象,但对于弱类型语言,这个问题不存在;另一方面是,在编码(其实是编译)阶段防止程序员犯错,State实现类必须实现handle接口,但如果直接定义的是handle function,这个问题也无关紧要。特别是,如果没有完善的测试用例,程序员仍然会犯错,有了完善的测试用例,这点防范措施实在没啥意义。

多态

仍然是多态的,通过切换object/function。

JavaScript实现

大概实现一下。

var context = {
        handle:null,
        init: function(){
            handle = initStateHandle;
        },
        switchState: function(){
            handle = otherStateHandle;
        }
    };

    var initStateHandle = function(){
        //do init state stuff
    }
    var otherStateHandle = function(){
        //do other stuff
    }
    
    //init
    context.init();
    context.handle();
    
    //change state
    context.switchState();
    context.handle();

很久之前我也实现过一个State Pattern in JavaScript,实现思路是类似的。这段代码也可以用Lisp实现,由于刚接触Lisp,就不献丑了。

可以看出,用Js实现的代码短,直接而自然,一看就明白,不需要很多类文件,也不需要费尽心思用继承、委托等等OO技术,仅仅关注好业务逻辑就可以了。

依赖注入

在状态模式中,我们的质疑点是,State的实现类的定义仅仅为了一个对象。Java确实有这么一个框架,帮你实例化一次某个类,然后在全局,任意的注入这个对象,就是Spring。如果把Class定义看做对象定义(仅仅多写几个单词),确实可以应对这个质疑点。但是看看额外的开销吧,要启动一个完整的Spring框架。

当然,依赖注入是一个十分先进的思想,即当我想要任何对象的时候,不需要自己去找,像神一样,说:“要有光”,就把光的对象给你了。十分火的前端框架Angularjs就使用了依赖注入的思想,在函数的参数列表里声明好需要的对象,框架就给你注入了,你完全不用管这个对象是哪来的。

回到Java

喷Java的程序员多了去了,写Ruby、Python、Lisp的程序员,鄙视Java由来已久。但即使这样,为什么Java还这么有市场呢?所有人都会反感Java的Hello world,因为Java一定需要定义一个类,产生了很多额外的代码。纯面向对象的特性,最直接的就是,理解所有的代码的思路是一样的,所有人写出来的代码,也都是一样的。所有的类,都定义在某个package中,有自己的namespace,要解决特定的问题,嗯,一定是找某一个类,并通过某种方式实例化对象。某种意义上,这反而让思维简单了,只需要通过一种单一的方式来理解代码。剩下的,就是用这种单一的方式,该如何处理各种复杂的问题,就出现了设计模式。

所以,Java程序员可以很容易的培训出来,一切都是严谨的,规范的,不可突破的,换句话说,没什么黑客精神的。黑客强调的个性、简单、自由,在这里都没有,带来的好处是,生产力的大大提高,Java程序员遍地都是,从手工艺者,变成了工人。

参考:

  1. Design Patterns in Dynamic Programming
  2. Head First Design Patterns