精通Python设计模式第二版 第 1 章 工厂模式 学习笔记
工厂模式
工厂模式的思想意在简化对象的创建过程。调用者(客户端)不清楚对象的来源以及生成对象的方式,调用者只需要调用某个方法或者实例某个类 既可以获得想要对象。这些对象根据不同的生产需求而产生
两种形式
- 工厂方法:一个方法(或者说函数)根据不同的输入参数返回不同的对象
- 抽象工厂:一组用于创建一系列相关对象的工厂方法
工厂方法的实现
让我们想象一下这样一个需求,我们有一些数据文件需要分析,分别是XML和JSON。这两种文件需要不容的库解析数据,但是调用者不想关心,然后区分处理这两种数据。这种场景需求就可以使用工厂方法来解决,工厂方法会帮我们做好不同数据格式的数据解析。
让我们先看一下 需要解析的文件数据
movies.json
[
{
"title": "After Dark in Central Park",
"year": 1900,
"director": null,
"cast": null,
"genre": null
},
{
"title": "Boarding School Girls' Pajama Parade",
"year": 1900,
"director": null,
"cast": null,
"genre": null
},
{
"title": "Buffalo Bill's Wild West Parad",
"year": 1900,
"director": null,
"cast": null,
"genre": null
},
{
"title": "Caught",
"year": 1900,
"director": null,
"cast": null,
"genre": null
},
{
"title": "Clowns Spinning Hats",
"year": 1900,
"director": null,
"cast": null,
"genre": null
},
{
"title": "Capture of Boer Battery by British",
"year": 1900,
"director": "James H. White",
"cast": null,
"genre": "Short documentary"
},
{
"title": "The Enchanted Drawing",
"year": 1900,
"director": "J. Stuart Blackton",
"cast": null,
"genre": null
},
{
"title": "Family Troubles",
"year": 1900,
"director": null,
"cast": null,
"genre": null
},
{
"title": "Feeding Sea Lions",
"year": 1900,
"director": null,
"cast": "Paul Boyton",
"genre": null
}
]
person.xml
<persons>
<person>
<firstName>John</firstName>
<lastName>Smith</lastName>
<age>25</age>
<address>
<streetAddress>21 2nd Street</streetAddress>
<city>New York</city>
<state>NY</state>
<postalCode>10021</postalCode>
</address>
<phoneNumbers>
<phoneNumber type="home">212 555-1234</phoneNumber>
<phoneNumber type="fax">646 555-4567</phoneNumber>
</phoneNumbers>
<gender>
<type>male</type>
</gender>
</person>
<person>
<firstName>Jimy</firstName>
<lastName>Liar</lastName>
<age>19</age>
<address>
<streetAddress>18 2nd Street</streetAddress>
<city>New York</city>
<state>NY</state>
<postalCode>10021</postalCode>
</address>
<phoneNumbers>
<phoneNumber type="home">212 555-1234</phoneNumber>
</phoneNumbers>
<gender>
<type>male</type>
</gender>
</person>
<person>
<firstName>Patty</firstName>
<lastName>Liar</lastName>
<age>20</age>
<address>
<streetAddress>18 2nd Street</streetAddress>
<city>New York</city>
<state>NY</state>
<postalCode>10021</postalCode>
</address>
<phoneNumbers>
<phoneNumber type="home">212 555-1234</phoneNumber>
<phoneNumber type="mobile">001 452-8819</phoneNumber>
</phoneNumbers>
<gender>
<type>female</type>
</gender>
</person>
</persons>
接下来,让我们看看如何实现这样一个工厂方法
1. 先找到能解析这两种数据的库
import json
import xml.etree.ElementTree as etree
2. 解析数据的方法
类JSONDataExtractor用于解析JSON文件,它有parsed_data()方法,返回一个包含所有数据的字典。
类XMLDataExtractor则用于解析XML文件
class JSONDataExtractor:
"""
JSONDataExtractor : 用于解析JSON文件
Attributes:
"""
def __init__(self, filepath):
self.data = dict() # 包含所有数据的字典
with open(filepath, 'r', encoding='utf8') as f:
self.data = json.load(f)
@property
def parsed_data(self):
return self.data
class XMLDataExtractor():
"""
XMLDataExtractor : 用于解析XML文件
Attributes:
"""
def __init__(self, filepath):
self.tree = etree.parse(filepath)
@property
def parsed_data(self):
return self.tree
3. 工厂方法
根据输入文件的拓展名,返回一个JSONDataExtractor或XMLDataExtractor的实例
def dataextraction_factory(filepath):
'''
工厂方法 根据输入文件的拓展名,返回一个JSONDataExtractor或XMLDataExtractor的实例
:param filepath:
:return:
'''
if filepath.endswith('json'):
extractor = JSONDataExtractor
elif filepath.endswith('xml'):
extractor = XMLDataExtractor
else:
raise ValueError('Cannot extract data from {}'.format(filepath))
return extractor(filepath)
4. 最后添加一些异常处理机制
def extract_data_from(filepath):
'''
dataextraction_factory() 方法的装饰器。它增加了异常处理机制
:param filepath:
:return:
'''
factory_obj = None
try:
factory_obj = dataextraction_factory(filepath)
except ValueError as e:
print(e)
return factory_obj
5. 工厂函数的使用
def main():
# 展示如果使用 工厂方法处理JSON文件
json_factory = extract_data_from('data/movies.json')
json_data = json_factory.parsed_data
print(f'Found: {len(json_data)} movies')
for movie in json_data:
print(f"Title: {movie['title']}")
year = movie['year']
if year:
print(f"Year: {year}")
director = movie['director']
if director:
print(f"Director: {director}")
genre = movie['genre']
if genre:
print(f"Genre: {genre}")
print()
# 展示如何使用工厂方法处理XML文件
xml_factory = extract_data_from('data/person.xml')
xml_data = xml_factory.parsed_data
liars = xml_data.findall(f".//person[lastName='Liar']")
print(f'found: {len(liars)} persons')
for liar in liars:
firstname = liar.find('firstName').text
print(f'first name: {firstname}')
lastname = liar.find('lastName').text
print(f'last name: {lastname}')
[print(f"phone number ({p.attrib['type']}):", p.text)
for p in liar.find('phoneNumbers')]
print()
print()
if __name__ == '__main__':
main()
获得的打印如下
Found: 9 movies
Title: After Dark in Central Park
Year: 1900
Title: Boarding School Girls' Pajama Parade
Year: 1900
Title: Buffalo Bill's Wild West Parad
Year: 1900
Title: Caught
Year: 1900
Title: Clowns Spinning Hats
Year: 1900
Title: Capture of Boer Battery by British
Year: 1900
Director: James H. White
Genre: Short documentary
Title: The Enchanted Drawing
Year: 1900
Director: J. Stuart Blackton
Title: Family Troubles
Year: 1900
Title: Feeding Sea Lions
Year: 1900
found: 2 persons
first name: Jimy
last name: Liar
phone number (home): 212 555-1234
first name: Patty
last name: Liar
phone number (home): 212 555-1234
phone number (mobile): 001 452-8819
工厂方法完整代码
factory_method.py
import json
import xml.etree.ElementTree as etree
class JSONDataExtractor:
"""
JSONDataExtractor : 用于解析JSON文件
Attributes:
"""
def __init__(self, filepath):
self.data = dict() # 包含所有数据的字典
with open(filepath, 'r', encoding='utf8') as f:
self.data = json.load(f)
@property
def parsed_data(self):
return self.data
class XMLDataExtractor():
"""
XMLDataExtractor : 用于解析XML文件
Attributes:
"""
def __init__(self, filepath):
self.tree = etree.parse(filepath)
@property
def parsed_data(self):
return self.tree
def dataextraction_factory(filepath):
'''
工厂方法 根据输入文件的拓展名,返回一个JSONDataExtractor或XMLDataExtractor的实例
:param filepath:
:return:
'''
if filepath.endswith('json'):
extractor = JSONDataExtractor
elif filepath.endswith('xml'):
extractor = XMLDataExtractor
else:
raise ValueError('Cannot extract data from {}'.format(filepath))
return extractor(filepath)
def extract_data_from(filepath):
'''
dataextraction_factory() 方法的装饰器。它增加了异常处理机制
:param filepath:
:return:
'''
factory_obj = None
try:
factory_obj = dataextraction_factory(filepath)
except ValueError as e:
print(e)
return factory_obj
def main():
# sqlite_factory = extract_data_from('data/person.sq3')
# print()
# 展示如果使用 工厂方法处理JSON文件
json_factory = extract_data_from('data/movies.json')
json_data = json_factory.parsed_data
print(f'Found: {len(json_data)} movies')
for movie in json_data:
print(f"Title: {movie['title']}")
year = movie['year']
if year:
print(f"Year: {year}")
director = movie['director']
if director:
print(f"Director: {director}")
genre = movie['genre']
if genre:
print(f"Genre: {genre}")
print()
# 展示如何使用工厂方法处理XML文件
xml_factory = extract_data_from('data/person.xml')
xml_data = xml_factory.parsed_data
liars = xml_data.findall(f".//person[lastName='Liar']")
print(f'found: {len(liars)} persons')
for liar in liars:
firstname = liar.find('firstName').text
print(f'first name: {firstname}')
lastname = liar.find('lastName').text
print(f'last name: {lastname}')
[print(f"phone number ({p.attrib['type']}):", p.text)
for p in liar.find('phoneNumbers')]
print()
print()
if __name__ == '__main__':
main()
抽象工厂的实现
假设我们正在创建一个游戏,或者想将一个迷你游戏作为应用程序的一部分来取悦用户。我们希望至少包括两款游戏:一款儿童游戏,一款成人游戏。我们将根据用户的输入,在运行时决定创建和启动哪款游戏
1. 儿童游戏的创建
男主角是一只喜欢吃虫子的青蛙,每一个主角都需要一个好的名字
class Frog():
"""
Frog :
Attributes:
"""
def __init__(self, name):
self.name = name
def __str__(self):
return self.name
def interact_with(self, obstacle):
'''
用于描述 青蛙和障碍物之间的交互
:param obstacle:
:return:
'''
act = obstacle.action()
msg = f'{self} the Frog encounters {obstacle} and {act} !'
print(msg)
class Bug():
"""
Bug :
Attributes:
"""
def __str__(self):
return 'a bug'
def action(self):
'''青蛙只支持一种行为:吃掉虫子'''
return 'eats it'
2. 儿童游戏的抽象工厂
它的主要任务是创建游戏中的主角与障碍物。保持创建方法的独立性以及名称的通用性
class FrogWorld():
"""
FrogWorld : 抽象工厂。它的主要任务是创建游戏中的主角与障碍物
Attributes:
"""
def __init__(self, name):
print(self)
self.player_name = name
def __str__(self):
return '\n\n\t------ Frog World ------'
def make_character(self):
return Frog(self.player_name)
def make_obstacle(self):
return Bug()
3. 成人游戏的创建
跟儿童游戏的区别是,这是一个巫师打兽人的游戏
class Wizard():
"""
Wizard :
Attributes:
"""
def __init__(self, name):
self.name = name
def __str__(self):
return self.name
def interact_with(self, obstacle):
act = obstacle.action()
msg = f'{self} the Wizard battles against {obstacle} and {act}!'
print(msg)
class Ork():
"""
Ork :
Attributes:
"""
def __str__(self):
return 'an evil ork'
def action(self):
return 'kills it'
4. 成人游戏的抽象工厂
情况与儿童游戏类似
class WizardWorld():
"""
WizardWorld :
Attributes:
"""
def __init__(self, name):
print(self)
self.player_name = name
def __str__(self):
return '\n\n\t------ Wizard World ------'
def make_character(self):
return Wizard(self.player_name)
def make_obstacle(self):
return Ork()
5. 游戏的主入口
接收一个工厂作为输入,并使用它来创建游戏世界
class GameEnvironment():
"""
GameEnvironment : 游戏的主入口。它接收一个工厂作为输入,并使用它来创建游戏世界
Attributes:
"""
def __init__(self, facotry):
self.hero = facotry.make_character()
self.obstacle = facotry.make_obstacle()
def play(self):
'''
初始化主角与障碍物之间的交互
:return:
'''
self.hero.interact_with(self.obstacle)
6. 游戏创建逻辑
def validate_age(name):
'''
提示用户给出一个有效年龄。如果年龄无效,则返回一个首元素为False的元祖
:param name:
:return:
'''
try:
age = input(f'Welcome {name}. How old are you? \n')
age = int(age)
except ValueError as err:
print(f'Age {age} is invalid, please try again...')
return (False, age)
return (True, age)
7. 游戏运行逻辑
def main():
'''
询问用户姓名和年龄,并根据用户的年龄决定应该玩哪款游戏
:return:
'''
name = input("Hello, What's your name? \n")
valid_input = False
while not valid_input:
valid_input, age = validate_age(name)
game = FrogWorld if age < 18 else WizardWorld
environment = GameEnvironment(game(name))
environment.play()
if __name__ == '__main__':
main()
获取打印如下
Hello, What's your name?
coco
Welcome coco. How old are you?
19
------ Wizard World ------
coco the Wizard battles against an evil ork and kills it!
抽象工厂完整代码
class Frog():
"""
Frog :
Attributes:
"""
def __init__(self, name):
self.name = name
def __str__(self):
return self.name
def interact_with(self, obstacle):
'''
用于描述 青蛙和障碍物之间的交互
:param obstacle:
:return:
'''
act = obstacle.action()
msg = f'{self} the Frog encounters {obstacle} and {act} !'
print(msg)
class Bug():
"""
Bug :
Attributes:
"""
def __str__(self):
return 'a bug'
def action(self):
'''青蛙只支持一种行为:吃掉虫子'''
return 'eats it'
class FrogWorld():
"""
FrogWorld : 抽象工厂。它的主要任务是创建游戏中的主角与障碍物
Attributes:
"""
def __init__(self, name):
print(self)
self.player_name = name
def __str__(self):
return '\n\n\t------ Frog World ------'
def make_character(self):
return Frog(self.player_name)
def make_obstacle(self):
return Bug()
class Wizard():
"""
Wizard :
Attributes:
"""
def __init__(self, name):
self.name = name
def __str__(self):
return self.name
def interact_with(self, obstacle):
act = obstacle.action()
msg = f'{self} the Wizard battles against {obstacle} and {act}!'
print(msg)
class Ork():
"""
Ork :
Attributes:
"""
def __str__(self):
return 'an evil ork'
def action(self):
return 'kills it'
class WizardWorld():
"""
WizardWorld :
Attributes:
"""
def __init__(self, name):
print(self)
self.player_name = name
def __str__(self):
return '\n\n\t------ Wizard World ------'
def make_character(self):
return Wizard(self.player_name)
def make_obstacle(self):
return Ork()
class GameEnvironment():
"""
GameEnvironment : 游戏的主入口。它接收一个工厂作为输入,并使用它来创建游戏世界
Attributes:
"""
def __init__(self, facotry):
self.hero = facotry.make_character()
self.obstacle = facotry.make_obstacle()
def play(self):
'''
初始化主角与障碍物之间的交互
:return:
'''
self.hero.interact_with(self.obstacle)
def validate_age(name):
'''
提示用户给出一个有效年龄。如果年龄无效,则返回一个首元素为False的元祖
:param name:
:return:
'''
try:
age = input(f'Welcome {name}. How old are you? \n')
age = int(age)
except ValueError as err:
print(f'Age {age} is invalid, please try again...')
return (False, age)
return (True, age)
def main():
'''
询问用户姓名和年龄,并根据用户的年龄决定应该玩哪款游戏
:return:
'''
name = input("Hello, What's your name? \n")
valid_input = False
while not valid_input:
valid_input, age = validate_age(name)
game = FrogWorld if age < 18 else WizardWorld
environment = GameEnvironment(game(name))
environment.play()
if __name__ == '__main__':
main()