Java设计模式在Common Lisp中的体现
2013-08-04 by lizherui最近在进一步学习Java的过程中,我愈发感到Java把面向对象的编程思想玩到了登峰造极的地步。而Java中最能表现面向对象编程思想的,就是大名鼎鼎的设计模式了。
这是Wikipedia中对设计模式的定义:设计模式并不直接用来完成代码的编写,而是描述在各种不同情况下,要怎么解决问题的一种方案。面向对象设计模式通常以类型或对象来描述其中的关系和相互作用,但不涉及用来完成应用程序的特定类型或对象。设计模式能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免会引起麻烦的紧耦合,以增强软件设计面对并适应变化的能力。
在学习了一些设计模式后,我们会发现这23种设计模式都围绕着以下三大基本原则:
- 中意于组合而不是继承。
- 依赖于接口而不是实现。
- 高内聚,低耦合。
不过,除了以上这些收获和感悟以外,我发现了一些更让人感兴趣的东西,这也是这篇文章马上要探讨的重点——Java设计模式在Common lisp中的体现。
Java和Common lisp, 前者作为OO的代表,喜欢玩类和对象;后者作为Functional Programming的代表,喜欢玩函数和闭包。乍一看它俩应该风马牛不相及,井水不犯河水,但实际上我们会惊讶地发现Java设计模式中有着太多Common Lisp的对照,这种现象让人越来越强烈地感觉:软件设计虽然有众多的门派、风格和表现形式,但总体思想上似乎是殊途同归的,比如构造抽象屏障和提高抽象层次。
Java至少有16种设计模式能对应到Common Lisp中,如下图:
Talk is cheap, let me show you the code.
我们先来看看Singleton模式。
Singleton模式有一个形象的比喻:Singleton就像追妹子一样,你不能大街上随便拉一个妹子就说是我对象,也不能自己做一个妹子出来说是自己对象。只能通过某种方法(追妹子),来产生一个对象。同时在调用这个方法的时候,会对你是否已经有对象进行检查。如果有,那么方法会直接返回,或者产生一个异常(抽你一巴掌,或者new Exception(”流氓!”)),从而保证你只有一个对象。
这是一个线程安全的Java单例模式:
class Singleton
{
private static Singleton myInstance;
public static Singleton getInstance()
{
if (myInstance == null)
{
synchronized(Singleton.class)
{
if (myInstance == null)
{
myInstance = new Singleton();
}
}
}
return myInstance;
}
private Singleton()
{
// Constructor code goes here.
}
}
这个例子简单明了,核心思想是利用Java中函数 的访问权限控制实现单例。
但是在Common Lisp中,没有访问权限控制,那怎么去实现单例呢?这就变得非常有意思了。不仅是Common Lisp,Python、Ruby等动态语言中都有一种非常强悍的编程机制——元编程。
这是Wikipedia中对元编程的定义:元编程是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作。多数情况下,与手工编写全部代码相比,程序员可以获得更高的工作效率, 或者给与程序更大的灵活度去处理新的情形而无需重新编译。
我们需要用到元编程思想中的一个重要应用——元类,即类的模板,元类的实例也是类。
我们先定义一个元类:
(defclass singleton-class (standard-class)
((instance :initform nil)))
这段代码阻止程序员直接实例化singleton-class,有点儿像Java的抽象类。
然后为了严谨,我们对这个元类进行一些继承方面的限定。
允许这个元类继承标准类:
(defmethod validate-superclass ((class singleton-class) (superclass standard-class))
t)
允许其它元类继承这个元类:
(defmethod validate-superclass ((class singleton-class) (superclass singleton-class))
t)
不允许其它标准类继承这个元类:
(defmethod validate-superclass ((class standard-class) (superclass singleton-class))
nil)
然后定义初始化实例函数:
(defvar *singleton-classes* '())
(defmethod initialize-instance :after ((c singleton-class) &key)
(pushnew c *singleton-classes*))
再定义真正的make-instance:
(defmethod make-instance ((class singleton-class)
&key)
(with-slots (instance) class
(or instance
(setf instance (call-next-method)))))
还可以定义一个reset-singleton-classes:
(defun reset-singleton-classes ()
;; This means you will get new singletons from now on.
(loop for c in *singleton-classes*
do (setf (slot-value c 'instance) nil)))
元类完事了,我们定义两个标准类:
(defclass foo1()
()
(:metaclass singleton-class))
(defclass foo2 ()
()
(:metaclass singleton-class))
让我们测试一下,载入刚刚写好的test.lsp:
测试一下:
没有问题,我们还可以试一试刚才写的reset-singleton-classes:
果然,foo1的单个实例被清空重建了。
小结:Common Lisp中的元类,有一点像Java中的抽象类,比如不能被实例化,可以被继承等等。但元类跟抽象类仍然有着本质的不同:元类的实例是类,不是对象,于是能动态地创建类,更加灵活。Singleton模式具体的实现机制,是在元类中维护了一个内部队列来达到目的。
张教主有这样一句名言:元编程是一种黑魔法,正派人士都很畏惧。
我们再来看看工厂模式。
下面是一个典型的Java工厂模式:
abstract class Pizza {
public abstract int getPrice(); // count the cents
}
class HamAndMushroomPizza extends Pizza {
public int getPrice() {
return 850;
}
}
class DeluxePizza extends Pizza {
public int getPrice() {
return 1050;
}
}
class HawaiianPizza extends Pizza {
public int getPrice() {
return 1150;
}
}
class PizzaFactory {
public enum PizzaType {
HamMushroom,
Deluxe,
Hawaiian
}
public static Pizza createPizza(PizzaType pizzaType) {
switch (pizzaType) {
case HamMushroom:
return new HamAndMushroomPizza();
case Deluxe:
return new DeluxePizza();
case Hawaiian:
return new HawaiianPizza();
}
throw new IllegalArgumentException("The pizza type " + pizzaType + " is not recognized.");
}
}
class PizzaLover {
/*
* Create all available pizzas and print their prices
*/
public static void main (String args[]) {
for (PizzaFactory.PizzaType pizzaType : PizzaFactory.PizzaType.values()) {
System.out.println("Price of " + pizzaType + " is " + PizzaFactory.createPizza(pizzaType).getPrice());
}
}
}
输出结果:
Price of HamMushroom is 850
Price of Deluxe is 1050
Price of Hawaiian is 1150
在Common Lisp中,类和函数都是first class types ,即一等公民:
- 可以被存储到变量中。
- 可以作为参数传给其它。
- 可以作为函数的返。
- 可以在运行时被创造。
因此,Common Lisp实现工厂模式就显得简单粗暴了,或者说,根本用不着工厂模式:
(defclass pizza ()
((price :accessor price)))
(defclass ham-and-mushroom-pizza (pizza)
((price :initform 850)))
(defclass deluxe-pizza (pizza)
((price :initform 1050)))
(defclass hawaiian-pizza (pizza)
((price :initform 1150)))
(defparameter *pizza-types*
(list 'ham-and-mushroom-pizza
'deluxe-pizza
'hawaiian-pizza))
载入测试一下:
小结:Common Lisp中由于函数是一等公民的缘故,相对于Java中工厂模式的实现要简单的多,不需要额外建一个工厂类,可以随心所欲地玩。函数式语言相当于OO语言来说,确实有一个优点——可以操纵的单元粒度更小更细,所以更加灵活,同时也更加容易失控。
最后,让我们看看迭代器模式,首先是Java中的实现:
import java.util.ArrayList;
import java.util.List;
interface Iterator {
public Object next();
public boolean hasNext();
}
class ConcreteIterator implements Iterator{
private List list = new ArrayList();
private int cursor =0;
public ConcreteIterator(List list){
this.list = list;
}
public boolean hasNext() {
if(cursor==list.size()){
return false;
}
return true;
}
public Object next() {
Object obj = null;
if(this.hasNext()){
obj = this.list.get(cursor++);
}
return obj;
}
}
interface Aggregate {
public void add(Object obj);
public void remove(Object obj);
public Iterator iterator();
}
class ConcreteAggregate implements Aggregate {
private List list = new ArrayList();
public void add(Object obj) {
list.add(obj);
}
public Iterator iterator() {
return new ConcreteIterator(list);
}
public void remove(Object obj) {
list.remove(obj);
}
}
public class Test {
public static void main(String[] args){
Aggregate ag = new ConcreteAggregate();
ag.add("小明");
ag.add("小红");
ag.add("小刚");
Iterator it = ag.iterator();
while(it.hasNext()){
String str = (String)it.next();
System.out.println(str);
}
}
}
核心思想是在内部封装一个Arraylist,只对外暴露Iterator的基本接口,跟C++ STL中迭代器的设计基本一致。
在Common Lisp中实现迭代器,我们只需要定义宏:
(defmethod make-cursor-for ((collection list))
#'(lambda (message)
(case message
(:next
(let ((r (first collection)))
(setf collection (cdr collection))
(values r (null collection))))
(:finished-p
(null collection)))))
(defmethod cursor-next ((cursor function))
(funcall cursor ':next))
(defmethod cursor-finished-p ((cursor function))
(funcall cursor ':finished-p))
测试一下:
小结:Common Lisp中由于自带序列,对迭代天然支持,所以相当于Java会更加简,天生的宏机制表现得畅快淋漓。
总结:无论在Java还是Common Lisp中,设计模式虽然有不同的表现形式,但目标都一致的:构造抽象屏障,控制抽象层次,降低模块之间的耦合,尽量编写通用的组件。由于函数在Common Lisp中是一等公民的缘故,因此相对Java来说,Common Lisp的操作粒度更小,我们可以构造各种精巧的小函数并利用宏机制让各个小函数互相组合起来工作,显得更加简洁、灵活和强悍。但同时,这种特点也容易使整个项目的可控性受到威胁。而且Common Lisp这种函数式的数学思维,确实显得过于抽象了,不如Java的OO机制来得更加直观、更加贴近生活。
在学习Java设计模式的过程中,除了开阔了自己的眼界,同时也勾起了我对函数式编程思想的美好怀念以及继续在课余时间学习SICP(http://book.douban.com/subject/1148282)的动力。
以前写Python的时候,我特别喜欢那4把宝剑——lambda、map、reduce、filter带来的随心所欲的编程快感。Java的设计模式对我开启了另一个世界的门——这个世界的疆域更加广阔,规则更加严谨,层次更加明显。
最后,让我们用一个迷人的小故事来结束本文吧:
在 ILC 2002 大会上前Lisp大神,当今的Python倡导者Peter Norvig,由于某些原因,做一个类似于马丁路德在梵蒂冈宣扬新教的主题演讲,因为他在演讲中大胆地声称Python就是一种Lisp。
讲完后进入提问环节,出乎我意料的是,Peter点了我过道另一侧,靠上面几排座位的一个老头,他衣着皱褶,在演讲刚开始的时候踱步进来,然后就靠在了那个座位上面。
这老头满头凌乱的白发,邋遢的白胡须,像是从旅行团中落下的游客,已经完全迷路了,闲逛到这里来歇歇脚,随便看看我们都在这里干什么。我的第一个念头是,他会因为我们的奇怪的话题感到相当失望;接着,我意识到这位 老头的年纪,想到斯坦福就在附近,而且我想那人也在斯坦福 —— 难道他是……
“嗨,John,有什么问题?” Peter说。
虽然这只是10个字左右的问题,我不会假装自己记住了Lisp之父约翰麦卡锡说的每一个字。他在问Python程序能 不能像处理数据一样,优雅地处理Python代码。
“不行。John, Python做不到。” Peter就回答了这一句,然后静静地等待,准备接受教授的质疑,但老人没有再说什么了。此时,无语已胜千言。