抽象工厂模式一般用在我们的一个系统根据配置和平台的问题拥有多个可能实现的情况。调用代码从抽象工厂中请求对象,但不知道哪个类的对象会被返回。返回的底层实现可能取决于多种因素,如当前位置、操作系统和本地配置。
实际工作中常见的抽象工厂模式的例子包括独立于操作系统的工具包、数据库后端,以及针对具体国家的格式化或计算器代码。一个独立于操作系统的GUI工具包可能会使用一个抽象工厂模式使它能在Windows下返回一套winform组件,在mac下返回Cocoa组件,在Genome下返回GTK控件以及在KDE下返回QT控件。
抽象工厂的UML类图较难理解,通过例子可以使我们更容易理解抽象工厂模式。让我们创建一套依赖于特定语言环境的格式化工具来帮助我们格式化日期和货币。这将是可以挑选特定工厂的抽象工厂类,以及一对包含实际具体例子的工厂,一个是法国,另一个是美国。他们每一个都可以创建格式化的日期和时间对象,并且可以被查询,格式化成指定的值。下面是它的结构图:在Python中,我们没有必要实现任何接口类,这样我们就可以启用DateFormatter,CurrencyFormatter和FormatterFactory。格式化类本身是非常简单的:
class FranceDateFormatter(object):
def format_date(self, y, m, d):
y, m, d = (str(x) for x in (y,m,d))
y = '20'+y if len(y) == 2 else y
m = '0'+m if len(m)==1 else m
d = '0'+d if len(d)==1 else d
return('{0}/{1}/{2}'.format(d,m,y))
class USADateFormatter(object):
def format_date(self, y, m, d):
y, m, d = (str(x) for x in (y,m,d))
y = '20'+y if len(y) == 2 else y
m = '0'+m if len(m)==1 else m
d = '0'+d if len(d)==1 else d
return('{0}-{1}-{2}'.format(d,m,y))
class FranceCurrencyFormatter(object):
def format_currency(self, base, cents):
base, cents = (str(x) for x in (base, cents))
if len(cents)==0:
cents='00'
elif len(cents)==1:
cents = '0'+cents
digits = []
for i,c in enumerate(reversed(base)):
if i and not i%3:
digits.append(' ')
digits.append(c)
base = ''.join(reversed(digits))
return '{0}({1}'.format(base, cents)
class USACurrencyFormatter(object):
def format_currency(self, base, cents):
base, cents = (str(x) for x in (base, cents))
if len(cents)==0:
cents='00'
elif len(cents)==1:
cents = '0'+cents
digits = []
for i,c in enumerate(reversed(base)):
if i and not i%3:
digits.append(',')
digits.append(c)
base = ''.join(reversed(digits))
return '${0}({1}'.format(base, cents)
这些类使用一些基本的字符串操作来吧各种可能的输入(整数、不同长度的字符串及其他)变为如下格式:这里的代码可以对输入做更多的验证,但是我们这里只是为了勒脚抽象工厂模式,没必要做那些验证。现在我们已经设置了格式化程序,我们只需要创建格式化工厂:
class USAFormatterFactory(object):
def create_date_formatter(self):
return USADateFormatter()
def create_currency_formatter(self):
return USACurrencyFormatter()
class FranceFormatterFactory(object):
def create_date_formatter(self):
return FranceDateFormatter()
def create_currency_formatter(self):
return FranceCurrencyFormatter()
现在,我们只需要创建可以挑选出合适格式化工具的代码。由于这些事情只需要被创建一次,我们可以让它变成一个单例,只不过单例在Python中并不是非常有用。那就让我们将当前的格式化器变成模块级别变量:
country_code = 'US'
factory_map = {
'US': USAFormatterFactory,
'FR': FranceFormatterFactory
}
formatter_factory = factory_map.get(country_code)()
在这个例子中,我们硬编码了当前国家代码;实际上,我们需要仔细考虑语言环境,操作系统或配置文件来选择代码。它通过字典将国家代码与工厂类联系在一起。因此,我们只要从字典中选择正确的类并将它实例化就可以了。
当我们要为更多的国家添加支持时,可以很容易看出需要做什么事情:我们只需要创建新的格式化类和抽象工厂本身。
抽象工厂通常返回一个单例对象,虽然这不是必要的;在我们代码中,每当它被调用时,都会返回格式化工具的一个新实例。格式化工具没有理由不能被储存为实例变量并且每个工厂都要返回同一个实例。
回头再看看这些例子,我们再一次发现,似乎有很多适用于工厂的样板代码在Python中似乎是不需要的。通常,每个工厂类型(如美国和法国)使用单独的模块都可以很容易地满足对抽象工厂的需求,然后确保在工厂模块中,我们访问的是正确的模块。这种模块的封装结构通常是这样的:
localize/
__init__.py
backends/
__init__.py
USA.py
France.py
...
这里的诀窍是localize包中的__init__.py
包含了将所有请求重定向到正确后端的代码。有多种方式可以这样做。显然,我们在后端模块中重复每个方法,然后通过__init__.py
作为代理模块来进行路由选择。但我们可以做的更为整洁。如果我们指导后端永远不会动态的改变(即不会重新启动),我们可以简单地把一些if语句放到__init__.py
中来检查当前的国家代码,并在语句中使用通常是不可接受的from .backends.USA import *
, 从适当的后端导入所有变量。或者,我们可以将它导入一个后端。并设置current_backend
变量指向一个特定的模块:
from .backends import USA, France
if country_code == "US":
current_backend = USA
根据我们的解决方案,我们的客户端代码只需要简单的调用localize.format_date
(from ... import \*
)或localize.current_backend.format_date
来获得当前国家的格式化日期。最终的结果是比原来的抽象工厂模式更加Python化,而且在多数情况下同样灵活。
参考:
《Python3 面向对象编程》