今儿迁主机的时候,顺便把旧文档分类整理了一下。发现自己在04年还写过这么一篇…… 居然还投给了《程序员》…… 那时候真是太有闲情逸致了,我操……
读了读,除了“检视”这种港台腔外。发现我那会儿就有把简单事写复杂的天赋…… 恩,以下愿者上钩……
——时光倒流的分割线———
动态语言中的设计模式
Grady Booch在为《Design Patterns》一书所写的序言中谈到,“软件领域中的设计模式为开发人员提供了一种使用专家设计经验的有效途径”。 从设计模式出现的那一天起,这众多的模式就成为面向对象软件开发过程中一笔宝贵的财富,而对设计模式的总结工作也从来没有停止过。先是GoF的《Design Patterns》中23种经典模式的编目和命名,而后广大开发人员在此基础上不断扩充。这种种工作都为人们积累了重要的设计经验。
然而尽管设计模式本身应该与具体语言实现无关,但设计模式是从实践中总结而来的,因此在成文过程中它又不可避免的与特定的语言相关联。目前由于在工业中C++、Java等静态语言占据主导地位,所以人们总结出来的设计模式通常也是基于这些语言的。
如今计算机硬件仍然依照摩尔定律所提出的趋势飞速发展,软件的复杂度也越来越高,相对而言软件开发效率却一直没有质的飞跃。因此执行效率相对低下但开发效率较高的动态语言愈发受到人们的关注。但是想在动态语言中使用目前总结出的大量设计模式却存在一定的困难,因为静态语言与动态语言的语义差异很大,如果只是照搬目前这些设计模式并不能充分发挥动态语言的语言特性,有时候把某些静态语言的模式运用到动态语言上甚至是作茧自缚、画蛇添足了。
Python作为一种完全面向对象的动态语言,因为其简洁、易用、灵活的特点已经越来越受到开发人员的青睐。以下行文中就将用Python实践几种简单的模式,以此来逐步展示Python作为动态语言所具有的语言特性对于设计模式实现的影响。
1. 运用动态类型特性实现灵活性更高的Abstract Factory
不妨让我们从《Design Patterns》中所提到的第一种模式Abstract Factory开始。
实际上同样是面向对象的语言,把C++的代码翻译成Python的代码并不难。下面Python代码就是由《Design Patterns》中提供的用于创建迷宫相关产品的抽象工厂类MazeFactory翻译得到:
class MazeFactory:
def makemaze(self):
return Maze()
def makewall(self):
return Wall()
def makeroom(self, n):
return Room(n)
def makedoor(self, r1, r2):
return Door(r1, r2)
这个类就可以用于创建Maze的基本产品了,同时我们可以继承这个类已得到其他系列产品的工厂,比如魔法迷宫EnchantedMaze的抽象工厂:
class EnchantedMazeFactory(MazeFactory):
def makeroom(self, n):
return EnchantedRoom (n)
def makedoor(self, r1, r2):
return EnchantedDoor (r1, r2)
这样我们就不再需要将有关Maze风格的代码硬编码到具体Maze的创建过程中,而当创建不同风格的Maze时只需要替换不同的Factory就行了。
不过这么简单的翻译C++代码很无趣,而且也不能充分体现Python作为动态语言为编程人员带来的便利。下面让我们来看一看Python语言中的动态特性能为我们做些什么。
略加察看后我们就会发现这样实现的MazeFactory具有一个不足之处:很难向MazeFactory里添加新种类的产品。当需要支持新类型的产品时(比如一个陷阱产品Trap以及EnchantedTrap),我们需要修改MazeFactory以及它的所有子类,这是一件很枯燥的事情。而且我们还不得不改变MazeFactory的接口,这更是我们不愿意看到的。
对于这个问题GoF的书中向我们介绍了一个比一般Abstract Factory更灵活的解决办法,即只实现一个统一的make函数,通过传递给make不同的参数来确定要创建的产品的类型。
然而对于C++这样的静态语言,实现这种解决方法很困难。因为为了确定make返回值的类型,我们不得不将所有的产品继承自一个公共基类。但是Abstract Factory考虑的只是产品的风格系列,而同一系列不同类型的产品间逻辑上可能不存在明确的公共基类——比如Maze和Wall。而且即使使用公共基类也会有导致大量的向下强制类型转换出现。这些都是我们编程时希望避免的。
此时动态语言中动态类型特性的优势充分体现出来。动态类型允许一个变量在运行时刻绑定到不同类型的对象上,也就是说所有变量——包括函数的参数和返回值——都可以是任意类型的。因此我们不必要求Maze、Room等不同类型产品具有公共基类,也能很容易就能实现这种拥有统一make函数的工厂:
class Maze:…
class Wall:…
class Room:…
…
class MazeFactory(object):(注1)
def make(self, typename, *args):
if typename == 'maze': return Maze()
elif typename == 'wall': return Wall()
elif typename == 'room': return Room(args[0])
else typename == 'door': return Door(args[0], args[1])
class EnchantedMazeFactory(MazeFactory):
def make(self, typename, *args):
if typename == 'room': return EnchantedRoom (args[0])
elif typename == 'door': return EnchantedDoor (args[0], args[1])
else: return super(EnchantedMazeFactory, self).make(typename, args)
而创建Maze的代码可能是:
mf = EnchantedMazeFactory ()
mz = mf.make('maze')
r1 = mf.make('room', 1)
mz.addroom(r1)
make函数第三个参数*args表示把从第三个开始以后所有的参数接受为一个list,产品的构造函数从这个list中取出相应的参数用于构造对象。super是内置函数,用于返回一个变量的基类对象。
这样利用Python动态类型的特性(注2),我们用很小的代价实现了一个具有更多灵活性的MazeFactory了。现在当我们要支持新的产品类型时,我们只需要添加标示新产品的参数,而保持了MazeFactory接口的稳定。
不过估计你看到这一堆if、else会感到很不爽(其实就是个switch),我也是。更好的做法是编制一个产品的字典,利用这个字典来索引要产生类型,后面的部分我们将具体介绍这个方法。
动态类型是动态语言的一个基本特性,它能简化类层次、去除不必要的强制类型转换,提高语言表达能力以及自由性。实际上有很多模式都因为静态类型检查的限制造成了不必要的类层次和强制类型转换。比如一个用于遍历包含许多不同类型元素容器的Iterator,就可能要求所有的元素都继承自同一基类以便实现getCurrentItem,而实际上这些不同类型的元素逻辑上并不需要一个公共基类。利用动态类型特性可以使这种模式的实现更加简单灵活。
2. 动态语言不需要一些静态语言中的设计模式
前面提到我们需要编制一个产品的字典,利用这个字典索引要产生类型,这样我们就可以通过类似index['room']
的代码来创建产品了。同时我还希望赋予这个MazeFactory动态配置产品类型的能力。用什么模式?Prototype?没错,这的确是运用Prototype的地方,让我们来实现它:
import copy
class MazeFactory:
def __init__(self):
self.index = {'maze': Maze(),
'wall': Wall(),
'room': Room(),
'door': Door()}
def make(self, typename,):
return copy.deepcopy(self.index[typename])
def registtype(self, typename, instance):
self.index[typename] = instance
def unregisttype(self, typename):
del self.index[typename]
copy是Python的一个内部模块,其中包含deepcopy函数用于深拷贝对象。registtype、unregisttype函数则用来动态增删产品类型。
似模似样,还不错。不过Python中有没有更好的做法呢?
首先检视一下Prototype的初衷。因为C++并不提供类一级的对象——也就是说类本身不是对象,这就给生成产品以及动态控制产品类型造成了一定的困难,Proto Type模式的出现就是为了解决这个问题。然而像Smalltalk一样,对于Python来说类本身(也包括函数,模块)也是一个对象——type类的对象,可以对对象进行的每样操作都可以使用到类上。利用这点便利,我们不使用Prototype模式就可以实现这个MazeFactory:
class MazeFactory:
def __init__(self):
self.index = {'maze': Maze,
'wall': Wall,
'room': Room,
'door': Door}
def make(self, typename, *args):
return apply(self.index[typename], args)
def registtype(self, typename, type):
self.index[typename] = type
def unregisttype(self, typename):
del self.index[typename]
apply是一个内置函数,它接受两个参数一个函数、一个参数list,它的作用是把args作为参数传给类构造函数运行。
现在当我们要添加新的产品类型时,在字典中添加新的索引值对即可。比如我要在MazeFactory中添加一个新产品Trap,只需要在index里加入:'trap': Trap
。而动态添加类型时也只需要传入相应的类即可:mf.registtype('trap', Trap)
可以看到我们已经得到了一个静态、动态都比较容易管理产品类型很灵活的工厂。
设计模式中一部分模式的作用就是让程序更灵活,拥有更多的动态特性。而动态语言可能在语言一级就已经支持了这些特性,所以这部分模式也就不需要了。Prototype是一个例子,类似的还有Template Method。实际上因为上面提到的动态类型特性,任何函数都可以接受任意类型的变量做参数,所以Template Method模式也就没有任何价值了。
3. 运行时刻修改类结构来实现Singleton
Singleton是一个非常有用的模式(注3),比如前面的MazeFactory就很有可能需要做成一个单件。让我们来尝试实现这个模式。首先还是按照C++的方法来实现一下:
class Singleton(object):
instance = None
def __new__(cls):
if cls.instance is None:
cls.instance = object.__new__(cls)
return cls.instance
类的__new__方法是一个特定的函数,它在对象创建前被执行返回一个cls类的对象。instance则负责保存单件对象。
依循C++的思路,这个程序完全正确,但是看起来缺少一点感觉。让我们查看一下Singleton对象创建的过程,第一次构造对象时创建一个新的对象,以后构造时则不再创建而只是返回第一次创建的结果。
可以看到这是一个典型的运行时刻修改类结构的例子。Python来做自是手到擒来:
class Singleton(object):
def __new__(cls):
cls.instance = object.__new__(cls)
cls.__new__ = cls._new
return cls.instance
def _new(type, cls):
return cls.instance
_new = classmethod(_new)(注4)
classmethod函数的作用是把一个成员方法变成一个类方法。
整个类的代码很简单,第一次进行构造时创建对象,同时用_new函数替换__new__函数,而以后__new__(实际上就是_new)只是返回第一次创建的对象。
能够在运行时刻修改程序结构是动态语言区别于静态语言的一个重要特点,Python中它主要表现在可以动态添加、删除、修改对象(包括类和函数)的方法和变量。这个特性可以用来帮助实现Singleton、Decorator、Strategy等一些要求运行时刻动态改变对象职能的模式。
4. 用Functional Programming简化Command模式
最后要谈到的一点就是Functional Programming。在设计模式里谈到FP可能不很切合。如果我们注意一下会发现GoF《Design Patterns》的副标题是“Elements of Reusable Object-Oriented Software”(可复用面向对象软件的基础)。实际上我们日常谈到的设计模式主要是针对OO Programming来说——因为工业中主要应用的是面向对象编程语言。而FP是区别于OOP的另一种编程风格。
但是Python作为一种面向对象的动态语言,它对FP和OOP的编程都提供了良好的支持。在OO的设计模式中运用一些Functional Programming的思想也会收获一定的好处。下面以Command模式为例讲述这一点。
因为有时候请求操作的对象不知道被请求操作对象的任何信息(打开文档的按钮对象不会知道任何文档对象的信息)。Command模式用以解决这个问题,根本目的在于把被调用操作的对象与实现该操作的对象解耦。而Command模式的实现方式是“把函数层面的任务提升到了类的层面”(注5)。因为FP中函数本身就是第一类对象,所以我们不必再像C++中那样运用继承组合来迂回地实现这一点。FP实现Command模式是非常简单的。
class Button:
def click(self):pass
这样我们就已经有了一个Button类。看起来什么都没有?别急,先让我们实现一个用于打开文档的Button看看:
opendoc = Button()
opendoc.click = doc.open
现在当opendoc这个按钮被点击时,仅仅需要执行opendoc.click,就等价调用了doc.open这个函数了。这段代码看似简单,但观察下会发现Button和Document已经被很好地解耦,只是FP的实现中间没有那么多迂回,而思路、效果与Command模式是一样的。
再来看看MacroCommand。MacroCommand的目的在于一次执行一系列的Command——也就是函数。有了FP的办法,我们也不必再用Composite模式搞得很复杂,而是利用匿名函数lambda关键字和内置FP函数map实现为:
marcocommand = lambda commands: map(lambda command: command(), commands)
map依次把commands中每个的command传给第一个参数lambda command: command()
,而这个参数本身也是一个函数,用来执行传入的command。现在如果我们要实现一个Find & Replace的Button,我们只要这么写:
findreplace = Button()
findreplace.click = lambda: marcocommand([doc.find, doc.replace])
当然Function Programming本身是很复杂的,仅仅在设计模式中一些简单的运用不能充分体现FP的思维方法和能力。但FP作为区别于OOP的编程风格,将其适当用在设计模式中,的确可以优雅简单地实现一些原本复杂的模式。而不同编程风格的尝试和融合也有利于拓宽我们的思路。
结语
通过Python的模式实践,我们可以看到动态语言的种种特性对各种设计模式的实现方式的影响,它们在简化了部分模式实现的同时更带来了较大的灵活性。当然天下没有免费的午餐,动态语言在语言一级提供更大灵活性是以执行效率的下降为代价的,个中的利弊还要广大开发人员去权衡。
本文只是浮光掠影地进行了一些简单的讲述,其中不免有许多疏漏。要想更好地掌握动态语言中设计模式的应用,必须要对各种设计模式以及动态语言的特性有着清晰的了解,并在实践中不断尝试才能做到。
注1: Python中继承自object的class表示一个type,否则是一个classobj。一些特定的操作必须是type类型才能执行,比如super函数要求传入一个type类型的参数、__new__函数也只有type类型才支持。如果需要用到这样操作,则必须让你的class继承自object。
注2: 值得注意的是Python实际上是一种强类型动态类型的语言,而不是弱类型的语言。关于强弱类型的具体解释可以参考这篇文章《Typing: Strong vs. Weak, Static vs. Dynamic》By Aahz。
注3: 实际上由于Singleton模式不能很好地适用于多线程环境,多线程环境中使用更多的可能是Singleton的变种Double-checked locking模式。不过理解了Python中Singleton实现,DCL也是大同小异,所以这里仍然用基本的Singleton模式来解说。
注4: Python2.4中提供新的语法Decorators for Functions and Methods来使类似操作更容易的实现。具体的语法和相关说明见PEP 318。
注5: 语自《Agile Software Development》By Robert Martin
2004-11-25左右成稿
博主时隔多年后要是再来一篇 《动态语言与设计模式》就好了(如果能用js/coffee描述那就完美了~ ^_^)