Scott's Blog

学则不固, 知则不惑

0%

Python 设计模式-抽象工厂与组合模式

抽象工厂模式让你的代码一键部署为不同区域的和语言,组合模式则擅长处理树状结构的问题。

这是设计模式系列文章的一部分,点击查看该系列的其他文章。

抽象工厂模式

想让你的系统根据配置或平台的问题有多个可能的实现,就可以利用抽象工厂模式。

你调用抽象工厂会返回一个对象,这个对象的实现会基于你的需求而变化。

  • 对于一个在线的商城,它对于不同的国家有不同的语言、货币以及税收的计算方式。
  • 对于一套 GUI 工具,在 Windows 上可能返回的是 WinForm,在 Mac 上返回的则是 Cocoa 组件。
  • 对于 Django,它会根据当前站点的配置而返回相关的对象以对不同数据库后端的支持。

想象一个格式化日期与货币的需求,我们需要支持中英文两种情况下的货币和日期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ChinaDateFormatter:
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 (" {} 年 {} 月 {} 日 ".format(y, m, d))


class USADateFormatter:
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 ("{}-{}-{}".format(y, m, d))

使用方式如下所示:

1
2
3
4
5
6

ChinaDateFormatter().format_date('21', '1', '1') # ' 2021 年 01 月 01 日 '
ChinaDateFormatter().format_date('21', '11', '27') # ' 2021 年 11 月 27 日 '

USADateFormatter().format_date('21', '1', '1') # '2021-01-01'
USADateFormatter().format_date('21', '1', '1') # '2021-01-01'

再来定义两个处理货币的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class ChinaCurrencyFormatter:
def format(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(str(base))):
# i 计算位数,千位 == 3
if i and not i % 3:
digits.append(',')
digits.append(c)
base = ''.join(reversed(digits))

return "¥ {}.{} 元".format(base, cents)

class USACurrencyFormatter:
def format(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(str(base))):
# i 计算位数,千位 == 3
if i and not i % 3:
digits.append(',')
digits.append(c)
base = ''.join(reversed(digits))

return "$ {}.{}".format(base, cents)

使用方式如下所示:

1
2
ChinaCurrencyFormatter().format(1432, 5)  # '¥ 143,958,766,111,111.05 元'
USACurrencyFormatter().format(1432, 5) # '$ 143,958,766,111,111.05'

将上面的代码按照国家组织在一起:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class USAFormatterFactory:
def create_date_formatter(self):
return USADateFormatter()


def create_currency_formatter(self):
return USACurrencyFormatter()


class ChinaFormatterFactory:
def create_date_formatter(self):
return ChinaDateFormatter()


def create_currency_formatter(self):
return ChinaCurrencyFormatter()

在使用的时候,可以直接使用字典去找对应的抽象工厂:

1
2
3
4
factory_map = {
'US': USAFormatterFactory,
'China': ChinaFormatterFactory
}

设置区域为中国:

1
2
3
4
5
country_code = 'China'
formatter_factor = factory_map.get(country_code)()
formatter_factor.create_date_formatter().format_date('21', '1', '1')

# ' 2021 年 01 月 01 日 '

设置区域为美国:

1
2
3
4
5
country_code = 'US'
formatter_factor = factory_map.get(country_code)()
formatter_factor.create_date_formatter().format_date('21', '1', '1')

# '2021-01-01'

在实际的项目架构中,我们会有一个后端的模块来支持对不同国家提供服务,它的结构可能是这样的:

1
2
3
4
5
6
localize/
__init__.py
backends/
__init__.py
USA.py
China.py

那么我们可以在 localize 下面的 __init__.py 文件中,动态的选择区域:

1
2
3
4
5
6
7
from .backends import *

if country_code == 'China':
current_backend = 'China'

elif:
pass

组合模式

组合模式一般通过组建来构造复杂的树状结构,它在文件夹和文件夹树中的应用比较多。

文件目录中通常有两种类型的对象,文件和文件夹。首先来定义这两个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Folder:
def __init__(self, name):
self.name = name
self.children = []


def add_child(self, child):
pass


def move(self, new_path):
pass


def copy(self, new_path):
pass


def delete(sejlf):
pass


class File:
def __init__(self, name, contents):
self.name = name
self.contents = contents


def move(self, new_path):
pass


def copy(self, new_path):
pass

def delete(self):
pass

把一些常用的方法抽象到基类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Compoment:
def __init__(self, name):
self.name = name

def move(self, new_path):
"""
get_path 方法会在外部实现
"""
new_folder = get_path(new_path)
del self.parent.children[self.name]
new_folder.children[self.name] = self
self.parent = new_folder


def delete(self):
del self.parent.children[self.name]

这样 File 和 Folder 类就可以少去一些代码了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Folder(Compoment):
def __init__(self, name):
super().__init__(name)
self.children = {}


def add_child(self, child):
child.parent = self
self.children[child.name] = child


def copy(self, new_path):
pass


class File(Compoment):
def __init__(self, name, contents):
super().__init__(name)
self.contents = contents


def copy(self, new_path):
pass

通过这种方式,我们使用的时候,可以进行任意的组合:

1
2
3
4
5
6
7
8
9
10
root = Folder('.')
folder1 = Folder('F1')
folder2 = Folder('F2')
hello_file = File('hello', 'hello scott')

folder2.add_child(hello_file)
folder1.add_child(folder2)
root.add_child(folder1)

root.children['F1'].children['F2'].children['hello'].contents

当你在编程的时候,遇到了树状结构的时候,可以想想是否可以应用组合模式。