Python语言与设计模式
设计模式是一个抽象层次,描述了在一个特定的环境中用来解决一般设计问题的对象和类之间的交互关系,其主要目的是充分利用语言的特性,设计可复用 的、能够适应需求变更的软件[9]。设计模式是一种设计思想,语言是实现思想的工具。因此,不同语言的特性影响了设计模式的实现,有些语言更容易实现设计 模式,而有些语言则比较难。GoF在设计模式一书中选用了两种面向对象语言—C++和Smalltalk实现软件开发中常用的23种设计模式,其中C++ 为主,Smalltalk为辅,重点突出了两种语言不同的语言特性对实现设计模式的影响。C++语言的运行时多态性的基础是虚函数机制,指向基类的指针可 以指向它的任何派生类,在实现设计模式时充分利用了C++这一特性,结合继承机制,建立类和对象的层次关系,使C++最大程度的具有动态特性,将绑定关系 尽可能推迟到运行时确定。
在GoF的23种模式中,部分设计模式是专门为静态语言提出的,有些模式在动态语言中语言一级就提供直接的支持,如Command模式,动态语言提供的函数式编程将函数本身看作是类对象。
Python 是一种完全面向对象的动态语言,提供了与传统面向对象语言截然不同的对象模型,影响了设计模式的实现和使用。Python中类也是对象,类和类的对象都有 可供操作的特殊属性,在运行时还可以修改类的结构和定义,这些特性使Python具有强大的“内省”能力,利用这种能力程序员可以创建高级的、动态的和灵 活的应用程序,可以更容易实现设计模式。本部分选取了几种常见的设计模式,尝试用Python语言实现,并与C++的实现方式进行比较,进一步体现动态语 言中的“动态性”及其具体应用。
5.1 抽象工厂(Abstract Factory)
抽象工厂模式提供了一个不需要指定具体类就可以创建一系列相互关联或相互依赖的对象的接口。抽象工厂隔离了具体类,客户代码只需通过抽象接口创建对象,不需要访问具体的类。参考GoF的设计模式一书,对书中实现迷宫工厂的C++代码用Python实现如下:
- 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、Wall、Room和Door的MazeFactory接口,接下来创建一个魔法迷宫工厂 EnchantedFactory,EnchantedFactory继承于MazeFactory,并通过MakeRoom和MakeDoor接口创建 了具有富有个性的EnchantedRoom和EnchantedDoor。
- class EnchantedFactory(MazeFactory):
- def MakeRoom(self, n):
- return EnchantedRoom(n)
- def MakeDoor(self, r1, r2):
- return EnchantedDoor(r1, r2)
这段代码只是对C++代码的简 单翻译,没有运用Python的语言特色。从上述的代码中可以看出,抽象工厂难以向MazeFactory中添加新的产品,假如迷宫中还需要创建陷阱 (Trap),就必须在MazeFactory接口中增加MakeTrap方法,这样就造成了MazeFactory接口的不稳定,继承 MazeFactory的所有子类的接口也随着基类的接口改变而改变。
工厂方法(Factory Method)解决了通过引入一个的Make操作将创建所有产品类型的操作统一化,Make操作中有一个参数可以唯一标识创建对象的类型。然而,用C++ 语言实现的工厂方法仍然存在局限性,这种局限性不利于构建可复用的软件。因为创建所有的产品类型都是通过Make接口的,为了保持Make接口的返回值对 所有产品的兼容性,就不得不迫使所有产品类型必须继承于一个公共的基类,然后Make接口返回该基类,这样保证了Make返回的类型都可以转换成特定的产 品类型。但是,同一系列不同类型的产品在逻辑上可能不存在明确的公共基类,比如MazeFactory中的Maze和Wall,而且,使用公共基类导致了 大量的向下强制转换,这种转换往往是不安全的,有时还不可行。[9]Pyhon语言的动态类型特性为解决该问题提供良好的方案,Python允许一个变量 在运行时绑定到不同类型的对象上,所以不必要求不同类型的产品具有公共基类,Make接口不必声明其返回类型,调用时具体的返回值类型在运行时交给解释器 去完成。Python实现工厂方法的代码如下:
- class Maze:…
- class Wall:…
- …
- class MazeFactory(object):
- def make(self, typename, *args):
- if typename == 'maze': return Maze()
- elif typename == 'wall': return Wall()
- elif typename == 'room':
- return Room(args[0])
- elif typename == 'door': ]
- return Door(args[0], args[1])
self是MazeFactory实例对象的引用参数,typename标识创建对象的类型,*args是创建具体对象时所需的参数列表。魔法迷宫的代码:
- class EnchantedFactory(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(EnchantedFactory, self).make(typename, args)
make方法中的return super(EnchantedFactory, self).make(typename, args)表示调用父类的操作创建其它类型的对象。
那么创建一个具体的EnchantedFactory实例的代码:
- mf = EnchantedFactory()
- mz = mf.make('maze')
- r1 = mf.make('room', 1)
- r2 = mf.make('room', 2)
- dr = mf.make('door', r1, r2)
当需要在MazeFactory添加一个Trap新类型时,只需要在Make方法中添加标示新类型的参数即可:
elif typename == “trap”: return Trap()
这 种做法不但保持了MazeFactory对外接口的稳定性,而且不需要类型的向下转换。但这里同样存在一个问题:每添加一个新类型,都要修改Make的实 现代码。能不能不用修改Make的代码即可添加一个新类型呢?原型模式(Prototype)提供了一种更好的解决方案——编制产品字典。
5.2 原型模式(Prototype)
原 型模式使用一个原型实例指定创建对象的类型,并且通过复制原型创建新的对象。原型模式的优点是可以在运行时动态的增加和删除产品类型,减少了子类化,还可 以动态的配置应用程序。使用原型管理器(Prototype Manager)可以方便实现运行时类型的增加和删除,管理器中有个类型的注册表,注册表是个关联存储结构的表,对于给定类型的键值可以唯一确定一个类 型,增加一个新类型时就是在表中注册该类型,客户程序在使用一个类型前先访问注册表检索它的原型。实现迷宫MazeFactory原型的Python代码 如下:
- 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, instance):
- self.index[typename] = instance
- def unregisttype(self, typename):
- del self.index[typename]
在 MazeFactory中,数据成员self.index={…}是个字典类型,存放MazeFactory产品类型,方法registtype和 unregisttype实现了产品类型的动态增加和删除,参数instance表示需要添加或删除类型的实例名。假如创建了一个MazeFactory 实例mf=MazeFactory(),实例Trap的定义如下:
- class Trap:
- def __init__(self, radius, height):
- self.radius = radius
- self.height = height
- …
向mf中添加实例Trap的代码:mf.registtype(‘trap’,Trap),而相应的删除代码为mf.unregisttype(‘trap’,Trap)。
显然,这种实现方式便于动态管理类型,具有良好的可扩展性。
5.3 单件模式(Singleton)
单件模式提供了一种将系统中类的实例个数限制为一个的机制,保证了一个类只有一个实例,并提供了该实例的一个全局访问点。程序的不同模块通常会共享同一个 对象。单件模式隐藏了实际的全局变量,对外提供了访问的接口,是一种很好的访问全局变量的方法。首先我们来看一下C++是怎样实现单件模式的。 Singleton类的定义如下:
- class Singleton {
- public:
- static Singleton* Instance();
- protected:
- Singleton();
- private:
- static Singleton* _instance;
- };
- //对应的实现:
- Singleton* Singleton::_instance = 0;
- Singleton* Singleton::Instance ()
- {
- if (_instance == 0)
- {
- _instance = new Singleton;
- }
- return _instance;
- }
在单件类中,静态数据成员_instance指向已经创建的实例,静态成员函数Instance()为单 件类提供了全局访问点,客户程序只能通过 Instance()接口创建单件类,如果_instance不为0,则直接返回已创建的实例,注意Singleton类的构造函数声明为 protected属性防止客户程序不通过Instance()接口创建它,保证了单件类的唯一性。
Python语言中的类和函数的定义可以在运行时改变,借助这一语言特性给出实现Singleton模式Python版本:
- class Singleton(object):
- def __new__(cls):
- cls.instance = object.__new__(cls)
- cls.__new__ = cls.Instance
- cls.instance.init()
- return cls.instance
- def Instance(cls,type):
- return cls.instance
- Instance = classmethod(Instance)
- def init(self):
- pass
Singleton 类重载了object的内置方法__new__,object是Python中所有数据类型的基类,包括内置数据类型和用户自定义类型,所有的数据类型都 继承了object的属性和方法。在Python中,类方法和静态方法是两个不同的概念。类的静态方法相当于C++中的静态成员函数,调用方式也类似。而 类方法隐式地将类本身作为第一个参数,在声明和调用的格式方面与静态方法都不同。而__new__方法是类的一个静态方法,不是一个类方法,它在调用类的 初始化方法__init__之前调用,创建对象的第一步就是调用__new__方法。__new__方法的第一个参数必须是一个类,并返回该类的一个新的 实例,其余的参数是调用__init__所需的参数。[11] getInstance是Singleton类的类方法,定义了Singleton实例的全局访问点。在重载的__new__方法中,首先调用了父类 object的__new__方法返回一个新的Singleton的实例cls.instance,参数cls实际是对Singleton类本省的引 用,instance是类的一个数据成员,保存了当前的单件对象。语句:
cls.__new__ = cls.getInstance是将getInstance赋给__new__方法,执行后,Singleton类的__new__方法变成了 getInstance。第一次创建Singleton实例对象时,调用__new__方法生成Singleton的一个新的实例,试图再次创建 Singleton实例对象时,调用的__new__的方法实际上被“偷偷的“调包成getInstance,__new__方法的代码不再被执行,而是 执行getInstance方法返回已经创建的实例对象cls.instance,从而保证了只存在一个Singleton实例对象。 cls.instance.init()说明了__new__方法在__init__之前调用,为了进一步初始化Singleton子类。
Singleton的子类继承了Singleton的__new__方法,每个子类也是单件的,只能有一个实例对象,无论调用多少次构造函数,子类重载了init方法,例如:
- >>> class MySingleton(Singleton):
- def init(self):
- print "call init..."
- def __init__(self):
- print "call __init__..."
- print "Initilizing My singleton"
- >>> a = MySingleton() #创建一个新的MySingleton实例对象
- call init... #调用父类Singleton的__new__方法
- call __init__... #调用自己的__init__方法
- Initilizing My singleton
- >>> b = MySingleton() #试图再创建一个MySingleton对象
- call __init__... #没有调用__init__方法,说明__new__被“调包”
- Initilizing My singleton
- >>> a == b #a和b实际上指向同一个实例对象
- True
在 C++中子类化Singleton相对比较麻烦,不能直接继承Singleton将子类定义为单件类,有三种方法创建Singleton的子类:在 Singleton的Instance操作中设置一个参数指定需要创建的单件;将Instance操作从父类分离到它的各个子类中;使用单件注册表。这些 方法都可以子类化单件类,但没有Python版本来得直观。Python中利用了在运行时可以改变类定义的动态特性,在运行时将自身的静态方法 __new__改成另外一个方法getInstance,这种“偷梁换柱”的做法确实为某些应用带来了便利。
5.4 代理模式(Proxy)
从面向对象设计的角度看,限制访问属性给一些旧问题提供了一种新的解决办法。代理模式就是一个很好的例子。代理模式用于隔离对象和访问它的客户,比如引用计数、不同等级的授权访问以及对象的惰性赋值等。代理模式的结构如下:
客户程序不需要直接访问实际的对象,换句话说,代理替代了实际的对象,客户通过代理去访问实际的对象。在C++中,这就意味着Proxy和 RealSubject必须要有一个公共的基类。在Python中,通过提供相同的方法接口,Proxy可以达到冒充Subject的效果。以下 Python代码中Proxy类是基于小型的通用包装类,它的主要功能就是为多个特定代理的实现提供一个基类,在Proxy类中可以重载 __gettattr__方法处理不同的方法。
- #Proxy Base Class
- class Proxy:
- def __init__( self, subject ):
- self.__subject = subject
- def __getattr__( self, name ):
- return getattr( self.__subject, name )
- #Subject class
- class RGB:
- def __init__( self, red, green, blue ):
- self.__red = red
- self.__green = green
- self.__blue = blue
- def Red( self ):
- return self.__red
- def Green( self ):
- return self.__green
- def Blue( self ):
- return self.__blue
- # More specific proxy implementation
- class NoBlueProxy( Proxy ):
- def Blue( self ):
- return 0
考虑以下情况:首先我们需要直接访问RGB类的实例对象,然后使用一个通用的代理实例作为一个包装类,最后传递给NoBlueProxy类:
- >>> rgb = RGB( 100, 192, 240 )
- >>> rgb.Red()
- 100
- >>> proxy = Proxy( rgb )
- >>> proxy.Green()
- 192
- >>> noblue = NoBlueProxy( rgb )
- >>> noblue.Green()
- 192
- >>> noblue.Blue()
- 0
代理模式在Python中应用很广泛,Python语言提供的机制中有一些就是代理模式实现的,比如垃圾收集中的简单引用计数。[10]
5.5命令模式(Command)
用 过集成开发环境的人都知道,开发基于窗口图形界面的应用程序时,一般要用到按钮和菜单等控件对象响应用户的输入,但在集成环境的工具箱提供的按钮和菜单并 没有显式地实现该请求,也就是按钮和菜单不知道关于请求的操作和请求的接受者的任何信息,这些请求特定于具体应用,只用控件的使用者才知道该由哪个对象响 应哪个操作,工具箱的设计者无法知道请求的接受者和执行的操作。那么工具箱的设计者是如何实现按钮和菜单的这种功能的呢?用Command模式。
Command 模式解耦了调用操作的对象(如按钮、菜单)和实现该操作的对象(如文档)。C++利用继承组合机制实现Command模式,通过定义一个带有 Execute接口的Command抽象类,特定应用相关的Command派生于此类,在Command类的子类显式定义接受者的对象。Command类 在调用操作的对象(Invoker)和实现该操作的对象(Receiver)之间充当了桥梁作用, Invoker请求某个Command类,由Command类的Execute接口执行Receiver的具体操作并将返回结果告诉Invoker,对于 Invoker根本不知道是谁执行了该操作,也不需要知道,从而实现了两者的解耦。而Python具有运行时可以改变类的结构、函数的定义的动态特性,很 简单地就实现了Command模式:
- class Button:
- def click(self):pass
- class document:
- def open(self):
- print "open document..."
- btn = Button()
- doc = document()
- btn.click = doc.open
执行btn.click()时实际上相当于调用了doc.open()的方法,实现了Button和document两者的解耦,比C++的继承组合机制要简单。
有 时一个按钮要求执行一系列命令,这种宏命令(MacroCommand)在应用中也是很常见的。Python支持lamda匿名函数定义,运用函数式编程 方式也可以很简便的实现MacroCommand模式。我们先来看看用C++是怎样实现的。C++用一个命令列表管理命令系列,执行宏命令实际上是遍历一 次命令列表:
- class Command {
- public:
- virtual ~Command();
- virtual void Execute() = 0;
- protected:
- Command();
- };
- class MacroCommand : public Command {
- public:
- MacroCommand();
- virtual ~MacroCommand();
- virtual void Add(Command*);
- virtual void Remove(Command*);
- virtual void Execute();
- private:
- List<Command*>* _cmds;//命令列表
- };
- void MacroCommand::Execute ()
- {
- ListIterator<Command*> i(_cmds);
- for (i.First(); !i.IsDone(); i.Next()) //遍历命令列表
- {
- Command* c = i.CurrentItem();
- c->Execute();
- }
- }
在Python中,假设已经定义了一个Button类和一个Document类,Button类有一个Click方法,Document包含Paste和Replace方法,以下代码实现了点击按钮后同时执行Paste和Replace操作:
- doc = Document()
- btn = Button()
- macrcocmd = lambda cmds : map(lambda cmd:cmd(),cmds)
- btn.Click = lambda: macrcocmd([doc.Paste, doc.Replace])
函数map是个Python内置函数,其作用是将列表cmds作为参数传递给匿名函数lambda cmd : cmd()执行并返回一个元组作为匿名函数macrocmd的参数,调用btn.Click()执行了匿名函数macrocmd(),该函数接受元组 [doc.Paste, doc.Replace] 作为它的输入参数,执行doc.Paste()和doc.Replace()命令。
以上我们只选取了GoF的23种模式的5种模式,展示了如何利用Python语言的动态特性实现设计模式。Python的灵活性和动态性为实现一些不同 的、优美的解决方案提供了一个良好的基础。Python的无类型化解决了静态语言实现工厂方法中需要不安全的强制类型转换等问题,既减少了程序中类的设 计,又避免了静态语言中的向下转换问题。原型模式为创建不同类型的对象提供一种更好的方法,利用了Python 可以运行时改变类的定义的特性,可以动态地增删类型,提高了程序的可扩展性。而单件模式则利用了类的静态方法__new__以及动态改变类的结构和定义的 特点,巧妙地实现了限制了单件类的实例对象个数的功能。在Proxy模式中,充分体现了Python中同一个类的多个实例对象之间可以拥有不同的结构以及 可以监视和限制属性访问的语言机制,这些机制为个实现多个通用类提供了一个相当完美的解决方法。在C++中不支持匿名函数,命令模式初步展现了 Python中lamda匿名函数在实现某些问题的简便性。其他的模式,如职责链(Chain Of Responsibility)、策略模式(Strategy)、装饰模式(Decorator),利用Python语言特性,也可以写出Python的 实现版本。