软件设计大师总是要比初学者更加清楚该如何设计软件,因为他们手中掌握着设计模式这一法宝。作为一种高级的软件复用形式,设计模式是众多优秀软件设计师集体智慧的结晶,能够很好地指导软件设计过程。本系列文章讲述如何在用Python开发软件时应用各种设计模式,此次介绍的是创建型工厂方法(Factory
Method)模式。
一、简介
工厂方法(Factory Method)模式又称为虚拟构造器(Virtual Constructor)模式或者多态工厂(Polymorphic
Factory)模式,属于类的创建型模式。在工厂方法模式中,父类负责定义创建对象的公共接口,而子类则负责生成具体的对象,这样做的目的是将类的实例化操作延迟到子类中完成,即由子类来决定究竟应该实体化哪一个类。
在简单工厂模式中,一个工厂类处于对产品类进行实例化的中心位置上,它知道每一个产品类的细节,并决定何时哪一个产品类应当被实例化。简单工厂模式的优点是能够使客户端独立于产品的创建过程,并且在系统中引入新产品时无需对客户端进行修改,缺点是当有新产品要加入到系统中时,必须对工厂类进行修改,以加入必要的处理逻辑。简单工厂模式的致命弱点就是处于核心地位的工厂类,因为一旦它无法确定要对哪个类进行实例化时,就无法使用该模式,而工厂方法模式则可以很好地避免这一问题。
考虑这样一个应用程序框架(Framework),它可以用来浏览各种格式的文档,如TXT、DOC、PDF、HTML等,设计时为了让软件的体系结构能够尽可能地通用,定义了Application和Document这两个抽象父类,客户必须通过它们的子类来处理某一具体类型的文档。例如,要想利用该框架来编写一个PDF文件浏览器,必须先定义PDFApplication和PDFDocument这两个类,它们应该分别继承于Application和Document。
Application的职责是对Document进行管理,并且在需要时创建它们,比如当用户从菜单中选择Open或者New的时候,Application就要负责创建一个Document的实例。显而易见,被实例化的特定Document子类是与具体应用相关的,因此Application无法预测哪个Document的子类将被实例化,它只知道一个新的Document何时(When)被创建,但并不知道哪种(Which)具体的Document将被创建。此时若仍坚持使用简单工厂模式会出现一个非常尴尬的局面:框架必须实例化类,但它只知道不能被实例化的抽象类。
解决的办法是使用工厂方法模式,它封装了哪一个Document子类将被创建的信息,并且能够将这些信息从框架中分离出来。如图1所示,Application的子类重新定义了Application的抽象方法createDocument(),并返回某个恰当的Document子类的实例。我们称createDocument()是一个工厂方法(factory
method),因为它非常形象地描述了类的实例化过程,即负责"生产"一个对象。
图1
简单说来,工厂方法模式的作用就是可以根据不同的条件生成各种类的实例,这些实例通常属于多个相似的类型,并且具有共同的父类。工厂方法模式将这些实例的创建过程封装了起来,从而简化了客户程序的编写,并改善了软件体系结构的可扩展性,使得将来能够以最小的代价加入新的子类。工厂方法这一模式适合在如下场合中运用:
- 当无法得知必须创建的对象属于哪个类的时候,或者无法得知属于哪个类的对象将被返回的时候,但前提是这些对象都符合一定的接口标准。
- 当一个类希望由它的子类来决定所创建的对象的时候,其目的是使程序的可扩展性更好,在加入其他类时更具弹性。
- 当创建对象的职责被委托给多个帮助子类(helper subclass)中的某一个,并且希望将哪个子类是代理者这一信息局部化的时候。
需要说明的是,使用工厂方法模式创建对象并不意味着一定会让代码变得更短(实事上往往更长),并且可能需要设计更多的辅助类,但它的确可以灵活地、有弹性地创建尚未确定的对象,从而简化了客户端应用程序的逻辑结构,并提高了代码的可读性和可重用性。
二、模式引入
工厂方法这一模式本身虽然并不复杂,但却是最重要的设计模式之一,无论是在COM、CORBA或是EJB中,都可以随处见到它的身影。面向对象的一个基本思想是在不同的对象间进行责权的合理分配,从本质上讲,工厂方法模式是一种用来创建对象的多态方法(polymorphic
method),它在抽象父类中声明用来创建对象的方法接口,而具体子类则通过覆盖该方法将对象的创建过程局部化,包括是否实例化一个子类,以及是否对它进行初始化等等。从某种程度上说,工厂方法可以看成是构造函数的特殊化,其特殊性表现在能够用一致的方法来创建不同的对象,而不用担心当前正在对哪个类进行实例化,因为究竟创建哪个类的对象将取决于它的子类。
假设我们打算开发一个用于个人信息管理(Personal Information Manager,PIM)的软件,它可以保存日常工作和生活中所需的各种信息,包括地址本、电话簿、约会提醒、日程安排等等。很显然,PIM用户界面(User
Interface)的设计将是比较复杂的,因为必须为每种信息的输入、验证和修改都提供单独的界面,以便同用户进行交互。比较简单的做法是在PIM中为各种信息的处理编写相应的用户界面,但代价是将导致软件的可扩展性非常差,因为一旦今后要加入对其他信息(比如银行帐户)进行管理的功能时,就必须对PIM进行修改,添加相应的用户界面,从而最终导致PIM变得越来越复杂,结构庞大而难以维护。改进的办法是将处理各种信息的用户界面从PIM中分离出来,使PIM不再关心用户如何输入数据,如何对用户输入进行验证,以及用户如何修改信息等,所有的这些都交由一个专门的软件模块来完成,而PIM要做的只是提供一个对这些个人信息进行管理的总体框架。
在具体实现时可以设计一个通用接口Editable,并且让所有处理特定个人信息(如通信地址和电话号码)的用户界面都继承于它,而PIM则通过Editable提供的方法getEditor()获得Editor的一个实例,并利用它来对用户输入进行统一的处理。例如,当用户完成输入之后,PIM可以调用Editor中的方法getContent()来获取用户输入的数据,或者调用resetUI()来清除用户输入的数据。在采用这一体系结构之后,如果要扩展PIM的功能,只需添加与之对应的Editable和Editor就可以了,而不用对PIM本身进行修改。
现在离目标还有一步之遥,由于Editable和Editor都只是通用的接口,但PIM却需要对它们的子类进行实例化,此时自然应该想到运用工厂方法模式,为PIM定义一个EditableFactory接口来创建Editable的对象。这样一来,整个PIM的体系结构就将如图2所示。
图2
Editable接口定义了一个公共的构造性方法(builder method)getEditor(),它返回一个Editor对象,其完整的代码如清单1所示。任何一项个人信息都拥有自己独立的用户界面(Editor),负责获取数据并在需要的时候进行修改,而PIM唯一要做事情的只是通过Editable来获得Editor,并利用它来对用户输入的数据进行相应的操作。
代码清单1:editable.py
class Editable:
""" 个人信息用户界面的公共接口 """
# 获得个人信息编辑界面
def getEditor(self):
pass
|
Editor接口给出了处理所有个人信息的公共接口,其完整的代码如清单2所示。PIM通过调用getUI()方法能够获得与用户进行交互的UI组件,根据当前正在处理的个人信息的不同,这些组件可能简单到只是一个文本输入框,也可以复杂到是一个包含了多个图形控件(Widget)的对话框。利用Editor提供的getContent()、commitChanges()和resetUI()方法,PIM还可以获取、提交或者清空用户输入的个人信息。在引入Editor之后,
PIM就能够从处理特定个人信息的用户界面中解脱出来,从而可以将注意力集中在如何对这些信息进行统一管理的问题上。
代码清单2:editor.py
class Editor:
""" 用户使用特定的Editor来编辑个人信息 """
# 获取代表用户界面(UI)的对象
def getUI(self):
pass
# 获取用户输入的数据
def getContent(self):
pass
# 提交用户输入的数据
def commitChanges(self):
pass
# 清空用户输入的数据
def resetUI(self):
pass
|
EditableAddress是Editable的一个具体实现,PIM使用它来处理个人地址信息,其完整的代码如清单3所示。
代码清单3:editableaddress.py
from editor import Editor
from editable import Editable
import Tkinter
class EditableAddress(Editable):
""" 用于处理个人地址信息的Editable """
# 构造函数
def __init__(self, master):
self.master = master
self.name = ""
self.province = ""
self.city = ""
self.street = ""
self.zipcode = ""
self.editor = AddressEditor(self)
# 获取相关联的Editor
def getEditor(self):
return self.editor
class AddressEditor(Editor, Tkinter.Frame):
""" 用于处理个人地址信息的Editor """
# 构造函数
def __init__(self, owner):
Tkinter.Frame.__init__(self, owner.master)
self.owner = owner
self.name = Tkinter.StringVar()
self.province = Tkinter.StringVar()
self.city = Tkinter.StringVar()
self.street = Tkinter.StringVar()
self.zipcode = Tkinter.StringVar()
self.createWidgets()
# 构造用户界面
def createWidgets(self):
# 姓名
nameFrame = Tkinter.Frame(self)
nameLabel = Tkinter.Label(nameFrame, text="Name:")
nameEntry = Tkinter.Entry(nameFrame, textvariable=self.name)
nameLabel.config(anchor=Tkinter.E, width=8, pady=3)
nameLabel.pack(side=Tkinter.LEFT)
nameEntry.pack(side=Tkinter.LEFT)
nameFrame.pack()
# 省份
provinceFrame = Tkinter.Frame(self)
provinceLabel = Tkinter.Label(provinceFrame, text="Province:")
provinceEntry = Tkinter.Entry(provinceFrame, textvariable=self.province)
provinceLabel.config(anchor=Tkinter.E, width=8, pady=3)
provinceLabel.pack(side=Tkinter.LEFT)
provinceEntry.pack(side=Tkinter.LEFT)
provinceFrame.pack()
# 城市
cityFrame = Tkinter.Frame(self)
cityLabel = Tkinter.Label(cityFrame, text="City:")
cityEntry = Tkinter.Entry(cityFrame, textvariable=self.city)
cityLabel.config(anchor=Tkinter.E, width=8, pady=3)
cityLabel.pack(side=Tkinter.LEFT)
cityEntry.pack(side=Tkinter.LEFT)
cityFrame.pack()
# 街道
streetFrame = Tkinter.Frame(self)
streetLabel = Tkinter.Label(streetFrame, text="Street:")
streetEntry = Tkinter.Entry(streetFrame, textvariable=self.street)
streetLabel.config(anchor=Tkinter.E, width=8, pady=3)
streetLabel.pack(side=Tkinter.LEFT)
streetEntry.pack(side=Tkinter.LEFT)
streetFrame.pack()
# 邮编
zipcodeFrame = Tkinter.Frame(self)
zipcodeLabel = Tkinter.Label(zipcodeFrame, text="ZIP Code:")
zipcodeEntry = Tkinter.Entry(zipcodeFrame, textvariable=self.zipcode)
zipcodeLabel.config(anchor=Tkinter.E, width=8, pady=3)
zipcodeLabel.pack(side=Tkinter.LEFT)
zipcodeEntry.pack(side=Tkinter.LEFT)
zipcodeFrame.pack()
# 重载Editor中的方法,获取代表用户界面(UI)的对象
def getUI(self):
return self
# 重载Editor中的方法,获取用户输入的数据
def getContent(self):
content = " Name: " + self.name.get() + "\n"
content += "Province: " + self.province.get() + "\n"
content += " City: " + self.city.get() + "\n"
content += " Street: " + self.street.get() + "\n"
content += "ZIP Code: " + self.zipcode.get()
return content
# 重载Editor中的方法,提交用户输入的数据
def commitChanges(self):
self.owner.name = self.name.get()
self.owner.province = self.province.get()
self.owner.city = self.city.get()
self.owner.street = self.street.get()
self.owner.zipcode = self.zipcode.get()
# 重载Editor中的方法,清空用户输入的数据
def resetUI(self):
self.name.set("")
self.province.set("")
self.city.set("")
self.street.set("")
self.zipcode.set("")
|
EditablePhone是Editable的另一个具体实现,PIM使用它来处理个人电话号码,其完整的代码如清单4所示。
代码清单4:editablephone.py
from editor import Editor
from editable import Editable
import Tkinter
class EditablePhone(Editable):
""" 用于处理个人电话号码的Editable """
# 构造函数
def __init__(self, master):
self.master =master
self.areaCode = "";
self.phoneNumber = ""
self.editor = PhoneEditor(self)
# 获取相关联的Editor
def getEditor(self):
return self.editor
class PhoneEditor(Editor, Tkinter.Frame):
""" 用于处理个人电话号码的Editor """
# 构造函数
def __init__(self, owner):
self.owner = owner
Tkinter.Frame.__init__(self, self.owner.master)
self.areaCode = Tkinter.StringVar()
self.phoneNumber = Tkinter.StringVar()
# 构造用户界面
codeLabel = Tkinter.Label(self, text="Area Code:")
codeEntry = Tkinter.Entry(self, textvariable=self.areaCode)
codeLabel.config(anchor=Tkinter.E, width=12, pady=3)
codeLabel.grid(row=0, column=0)
codeEntry.grid(row=0, column=1)
numberLabel = Tkinter.Label(self, text="Phone Number:")
numberEntry = Tkinter.Entry(self, textvariable=self.phoneNumber)
numberLabel.config(anchor=Tkinter.E, width=12, pady=3)
numberLabel.grid(row=1, column=0)
numberEntry.grid(row=1, column=1)
# 重载Editor中的方法,获取代表用户界面(UI)的对象
def getUI(self):
return self
# 重载Editor中的方法,获取用户输入的数据
def getContent(self):
content = " Area Code: " + self.areaCode.get() + "\n"
content += "Phone Number: " + self.phoneNumber.get() + "\n"
return content
# 重载Editor中的方法,提交用户输入的数据
def commitChanges(self):
self.owner.areaCode = self.areaCode.get()
self.owner.phoneNumber = self.phoneNumber.get()
# 重载Editor中的方法,清空用户输入的数据
def resetUI(self):
self.areaCode.set("")
self.phoneNumber.set("")
|
EditableFactory接口是在PIM中应用工厂方法模式的核心,其完整的代码如清单5所示。与简单工厂模式中负责创建所有对象的"超级类"不同,EditableFactory只定义了如何实例化Editable的工厂方法createEditable(),并不掌握它们的生杀大权,真正负责完成创建工作的是EditableFactory的子类。
代码清单5:editablefactory.py
class EditableFactory:
""" 用于创建Editable的工厂类 """
# 实例化Editable对象
def createEditable(self, master):
pass
|
EditableAddressFactory是EditableFactory的一个具体实现,PIM使用它来实例化EditableAddress对象,其完整的代码如清单6所示。
代码清单6:editableaddressfactory.py
from editablefactory import EditableFactory
from editableaddress import EditableAddress
class EditableAddressFactory(EditableFactory):
""" 用于创建EditableAddress的工厂类 """
# 重载EditableFactory中的方法,实例化EditableAddress对象
def createEditable(self, master):
address = EditableAddress(master)
return address
|
EditablePhoneFactory是EditableFactory的另一个具体实现,PIM使用它来实例化EditablePhone对象,其完整的代码如清单7所示。
代码清单7:editablephonefactory.py
from editablefactory import EditableFactory
from editablephone import EditablePhone
class EditablePhoneFactory(EditableFactory):
""" 用于创建EditablePhone的工厂类 """
# 重载EditableFactory中的方法,实例化EditablePhone对象
def createEditable(self, master):
phone = EditablePhone(master)
return phone
|
所有这些辅助类都定义好之后,接下去就可以编写PIM类了,它提供了一个对各种个人信息进行统一管理的框架,其完整的代码如清单8所示。
代码清单8:pim.py
from editablephone import EditablePhone
from editableaddressfactory import EditableAddressFactory
from editablephonefactory import EditablePhoneFactory
import Tkinter
class PIM:
""" 个人信息管理 """
# 构造函数
def __init__(self):
mainFrame = Tkinter.Frame()
mainFrame.master.title("PIM")
# 命令按钮
addressButton = Tkinter.Button(mainFrame, width=10, text="Address")
phoneButton = Tkinter.Button(mainFrame, width=10, text="Phone")
commitButton = Tkinter.Button(mainFrame, width=10, text="Commit")
resetButton = Tkinter.Button(mainFrame, width=10, text="Reset")
addressButton.config(command=self.addressClicked)
phoneButton.config(command=self.phoneClicked)
commitButton.config(command=self.commitClicked)
resetButton.config(command=self.resetClicked)
addressButton.grid(row=0, column=1, padx=10, pady=5, stick=Tkinter.E)
phoneButton.grid(row=1, column=1, padx=10, pady=5, stick=Tkinter.E)
commitButton.grid(row=2, column=1, padx=10, pady=5, stick=Tkinter.E)
resetButton.grid(row=3, column=1, padx=10, pady=5, stick=Tkinter.E)
# 用来容纳各类Editor的容器
self.editorFrame = Tkinter.Frame(mainFrame)
self.editorFrame.grid(row=0, column=0, rowspan=4)
self.editorFrame.grid_configure(stick=Tkinter.N, pady=15)
self.editor = Tkinter.Frame(self.editorFrame)
self.editor.grid()
# 个人信息显示区域
self.content = Tkinter.StringVar()
self.contentLabel = Tkinter.Label(mainFrame, width=50, height=5)
self.contentLabel.configure(textvariable=self.content)
self.contentLabel.configure(anchor=Tkinter.W, font="Arial 10 italic bold")
self.contentLabel.configure(relief=Tkinter.RIDGE, pady=5, padx=10)
self.contentLabel.grid(row=4, column=0, columnspan=2)
mainFrame.pack()
mainFrame.mainloop()
# Address按钮的回调函数
def addressClicked(self):
address = EditableAddressFactory().createEditable(self.editorFrame)
self.editor.grid_remove()
self.editor = address.getEditor()
self.editor.getUI().grid()
# Phone按钮的回调函数
def phoneClicked(self):
phone = EditablePhoneFactory().createEditable(self.editorFrame)
self.editor.grid_remove()
self.editor = phone.getEditor()
self.editor.getUI().grid()
# Commit按钮的回调函数
def commitClicked(self):
content = self.editor.getContent()
self.content.set(content)
# Reset按钮的回调函数
def resetClicked(self):
self.editor.resetUI()
# 主函数
if (__name__ == "__main__"):
app = PIM()
|
图3是PIM在运行时的界面效果。
图3
三、一般结构
工厂方法模式是简单工厂模式的进一步抽象和推广,它不仅保持了简单工厂模式能够向客户隐藏类的实例化过程这一优点,而且还通过多态性克服了工厂类过于复杂且不易于扩展的缺点。在工厂方法模式中,处于核心地位的工厂类不再负责所有产品的创建,而是将具体的创建工作交由子类去完成。工厂方法模式中的核心工厂类经过功能抽象之后,成为了一个抽象的工厂角色,仅负责给出具体工厂子类必须实现的接口,而不涉及哪种产品类应当被实例化这一细节。工厂方法模式的一般性结构如图4所示,图中为了简化只给出了一个产品类和一个工厂类,但在实际系统中通常需要设计多个产品类和多个工厂类。
图4
工厂方法模式的实质是将对象的创建延迟到其子类实现,即由子类根据当前情况动态决定应该实例化哪一个产品类。从上图可以看出,工厂方法模式涉及到抽象工厂角色、具体工厂角色、抽象产品角色和具体产品角色四个参与者。
- 抽象工厂(Creator)角色 是工厂方法模式的核心,它负责定义创建抽象产品对象的工厂方法。抽象工厂不能被外界直接调用,但任何在模式中用于创建产品对象的工厂类都必须实现由它所定义的工厂方法。
- 具体工厂(Concrete Creator)角色 是工厂方法模式的对外接口,它负责实现创建具体产品对象的内部逻辑。具体工厂与应用密切相关,可以被外界直接调用,创建所需要的产品。
- 抽象产品(Product)角色 是工厂方法模式所创建的所有对象的父类,它负责描述所有具体产品共有的公共接口。
- 具体产品(Concrete Product)角色 是工厂方法模式的创建目标,所有创建的对象都是充当这一角色的某个具体类的实例。
抽象工厂角色负责声明工厂方法(factory method),用来"生产"抽象产品,以下是抽象工厂的示例性Python代码:
代码清单9:creator.py
class Creator:
""" 抽象工厂角色 """
# 创建抽象产品的工厂方法
def factoryMethod(self):
pass
|
具体工厂角色负责创建一个具体产品的实例,并将其返回给调用者。具体工厂是与具体产品相关的,实现时一般常用的做法是为每个具体产品定义一个具体工厂。以下是具体工厂的示例性Python代码:
代码清单10:concretecreator.py
class ConcreteCreator(Creator):
""" 具体工厂角色 """
# 创建具体产品的工厂方法
def factoryMethod(self):
product = ConcreteProduct()
return product
|
抽象产品角色的主要目的是为所有的具体产品提供一个共同的接口,通常只需给出相应的声明就可以了,而不用给出具体的实现。以下是抽象产品类的示例性Python代码:
代码清单11:product.py
class Product:
""" 抽象产品角色 """
# 所有产品类的公共接口
def interface(self):
pass
|
具体产品角色充当最终的创建目标,一般来讲它是抽象产品类的子类,实现了抽象产品类中定义的所有工厂方法,实际应用时通常会具有比较复杂的业务逻辑。以下是具体产品类的示例性Python代码:
代码清单12:concreteproduct.py
class ConcreteProduct(Product):
""" 具体产品角色 """
# 公共接口的实现
def interface(self):
print "Concrete Product Method"
|
在应用工厂方法模式时,通常还需要再引入一个客户端角色,由它负责创建具体的工厂对象,然后再调用工厂对象中的工厂方法来创建相应的产品对象。以下是客户端的示例性Python代码:
代码清单13:client.py
class Client:
""" 客户端角色 """
def run(self):
creator = ConcreteCreator()
product = creator.factoryMethod()
product.interface()
# 主函数
if (__name__ == "__main__"):
client = Client()
client.run()
|
在这个简单的示意性实现里,充当具体产品和具体工厂角色的类都只有一个,但在真正的实际应用中,通常遇到的都是同时会有多个具体产品类的情况,此时相应地需要提供多个具体工厂类,每个具体工厂都负责生产对应的具体产品。
工厂方法模式的活动序列如图5所示,客户端Client首先创建ConcreteCreator对象,然后调用ConcreteCreator对象的工厂方法factoryMethod(),由它负责"生产"出所需要的ConcreteProduct对象。
图5
四、实际运用
使用工厂方法模式可以在不修改具体工厂角色的情况下引入新的产品,这一点无疑使得工厂方法模式具有比简单工厂模式更好的可扩展性。在开发实际的软件系统时,通常是先设计产品角色,然后才开始设计工厂角色,而复杂的需求导致将在抽象产品和具体产品之间形成非常庞大的树状结构,如图6所示。
图6
在上面的产品等级结构中,出现了多于一个的抽象产品类,以及多于两个的类层次,这是在构造真实系统中经常遇到的情况。在为这一软件体系结构应用工厂方法模式时,通常的做法是按照产品的等级结构再设计一个相同的工厂等级结构,如图7所示。
图7
定义工厂角色的目的是为了创建相应的产品角色,因此整个系统的架构将如图8所示。这一结构常常被称为平行的类层次(parallel
class hierarchies),它使得一个类能够将它的一些职责委托给另一个独立的类,而工厂方法则是联系两者之间的纽带。工厂方法模式并没有限制产品等级的层数,虽然前面给出的一般性结构中只有两个层次(抽象产品层和具体产品层),但在实际运用时却往往需要更加复杂的产品层次。
图8
在工厂方法模式的一般性结构中,每当具体工厂类中的工厂方法被请求时,都会调用具体产品类的构造函数来创建一个新的产品实例,然后再将这个实例提供给客户端。但在实际软件系统中应用工厂方法模式时,工厂方法所做的事情可能更加复杂,其中最常见到的一种情况是循环使用产品对象。所采用的策略是将工厂对象创建的所有产品对象登记到一个对象池(object
pool)中,这样每当客户请求工厂方法创建相应的产品对象时,可以先从对象池中查询符合条件的产品对象,如果对象池中恰巧有这样的对象,那就直接将这个产品对象返回给客户端;如果对象池中没有这样的对象,那就创建一个新的满足要求的产品对象,将其登记到对象池中,然后再返回给客户端。
工厂方法模式依赖于工厂角色和产品角色的多态性,但在实际运用时这个模式可能出现退化,其表现就是多态性的丧失。在工厂方法模式中,所有的具体工厂对象应该共享一个抽象的超类,或者换句话说,应当有多个具体工厂类作为一个抽象工厂类的子类存在于工厂等级结构中,但如果工厂等级结构中只有一个具体工厂类的话,那么抽象工厂角色可以省略。当抽象工厂角色被省略时,工厂方法模式就发生了退化,这一退化表现为工厂角色多态性的丧失,退化后的模式仍然可以发挥部分工厂方法模式的作用,通常被称为退化的工厂方法模式。退化的工厂方法模式在很大程度上与简单工厂模式相似,如图9所示,实际运用时可以考虑用简单工厂模式进行替代。
图9
在工厂方法模式中,从工厂方法返回的应当是抽象产品类型,而不是具体产品类型,因为只有这样才能保证产品角色的多态性。也就是说,调用工厂方法的客户端可以针对抽象产品类进行编程,而不必依赖于具体产品类。在实际运用时有可能会出现一种很特殊的情况,那就是工厂方法只需要返回一个具体产品类,此时工厂方法模式的功能同样会发生退化,但这一退化将表现为产品角色多态性的丧失,如图10所示。严格说来,当工厂方法模式出现这一退化时,就不能再称为工厂方法模式了,因为客户端从工厂方法的静态类型就可以判断出将要得到的是什么类型的对象,而这一点恰好违背了工厂方法模式的初衷。
图10
五、优势和不足
在工厂方法模式中,工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节。工厂方法模式的核心是一个抽象工厂类,各种具体工厂类通过从抽象工厂类中将工厂方法继承下来,使得客户可以只关心抽象产品和抽象工厂,完全不用理会返回的是哪一种具体产品,也不用关心它是如何被具体工厂创建的。
基于工厂角色和产品角色的多态性设计是工厂方法模式的关键,它使得工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。工厂方法模式之所以又被称为多态工厂模式,显然是因为所有的具体工厂类都具有同一抽象父类。
使用工厂方法模式的另一个优点是在系统中加入新产品时,不需要对抽象工厂和抽象产品提供的接口进行修改,而只要添加一个具体工厂和具体产品就可以了,没有必要修改客户端,也没有必须修改其他的具体工厂和具体产品,系统的可扩展性非常好。优秀的面向对象设计鼓励使用封装(Encapsulation)和委托(Delegation)来构造软件系统,而工厂方法模式则是使用了封装和委托的典型例子,其中封装是通过抽象工厂来体现的,而委托则是通过抽象工厂将创建对象的责任完全交给具体工厂来体现的。
使用工厂方法模式的缺点是在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,当两者都比较简单时,系统的额外开销相对较大。
六、小结
工厂方法模式的核心思想是定义一个用来创建对象的公共接口,由工厂而不是客户来决定需要被实例化的类,它通常在构造系统整体框架时被用到。工厂方法模式看上去似乎比较简单,但是内涵却极其深刻,抽象、封装、继承、委托、多态等面向对象设计中的理论都得到了很好的体现,应用范围非常广泛。
七、参考资料
- 《面向模式的软件体系结构 卷1:模式系统》,Frank Buschmann等著,贲可荣等译,北京:机械工业出版,2003。
- 《设计模式 可复用面向对象软件的基础》,Erich Gamma等著,李英军等译,北京:机械工业出版,2000。
- 《设计模式解析》,Alan Shalloway等著,北京:中国电力出版社,2003。
- 《深入学习:Python程序开发》,Andre Lessa著,张晓晖等译,北京:电子工业出版社,2001。
- 《Python语言入门》,Mark Lutz等著,陈革等译,2001。
- 《Python编程金典》,H.M.Deitel等著,周靖译,2003。
- 从Python官方网站(http://www.python.org)上可以了解到所有关于Python的知识。
- 在网站http://hillside.net/patterns/上可以了解到许多与模式相关的信息。
八、关于作者
肖文鹏,北京理工大学计算机系的一名硕士研究生,主要从事操作系统和分布式计算环境的研究,喜爱Linux和Python。你可以通过xiaowp@263.net与他取得联系。
|