(2024.04.14 Sun @KLN)
工廠模式為公共接口創(chuàng)建具體的實(shí)現(xiàn)方法(create concrete implements of a common interface),它將創(chuàng)建對(duì)象的過程和依賴該對(duì)象接口的代碼分離。
Case Study
以指定的格式,將Song
對(duì)象轉(zhuǎn)換為string
。該過程經(jīng)常被稱作序列化(serialising)。該函數(shù)最簡(jiǎn)單的實(shí)現(xiàn)方式如下:
# In serializer_demo.py
import json
import xml.etree.ElementTree as et
class Song:
def __init__(self, song_id, title, artist):
self.song_id = song_id
self.title = title
self.artist = artist
class SongSerializer:
def serialize(self, song, format):
if format == 'JSON':
song_info = {
'id': song.song_id,
'title': song.title,
'artist': song.artist
}
return json.dumps(song_info)
elif format == 'XML':
song_info = et.Element('song', attrib={'id': song.song_id})
title = et.SubElement(song_info, 'title')
title.text = song.title
artist = et.SubElement(song_info, 'artist')
artist.text = song.artist
return et.tostring(song_info, encoding='unicode')
else:
raise ValueError(format)
類Song
提供了歌的進(jìn)屬性,即id
,title
和artist
。類SongSerializer
將歌轉(zhuǎn)化為string表達(dá),具體的形式由參數(shù)format
指定。.seraliaze()
方法這里支持兩種格式JSON
和XML
,遇到其他格式則返回ValueError
錯(cuò)誤。
使用上面代碼
>>> song = Song('1', 'Water of Love', 'Dire Straits')
>>> serializer = SongSerializer()
>>> serializer.serialize(song, 'JSON')
'{"id": "1", "title": "Water of Love", "artist": "Dire Straits"}'
>>> serializer.serialize(song, 'XML')
'<song id="1"><title>Water of Love</title><artist>Dire Straits</artist></song>'
>>> serializer.serialize(song, 'YAML')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 18, in serialize
ValueError: YAML
該案例用于演示目的,想象在具體項(xiàng)目中,可能會(huì)提供多種format
可選,即參數(shù)指定的實(shí)現(xiàn)方式可以有多種,將導(dǎo)致代碼難以維護(hù)。
Optimisation
到目前為止,該案例的實(shí)現(xiàn)方式不容易維護(hù),下面對(duì)其做優(yōu)化。記得SOLID原則中的single responsibility principle
提到,每個(gè)模塊、類、方法應(yīng)該只做單一的、定義清晰的(well-defined)的職責(zé),只做一件事,因一個(gè)原因而改變。
.serialize()
方法不符合single responsibility principle
原則,發(fā)生如下情況將修改該方法:
- 引入新的格式:在if-else結(jié)構(gòu)中加入新分支
-
Song
對(duì)象改變:Song
對(duì)象一旦修改了屬性,則.serialize()
方法需要對(duì)每個(gè)格式做修改 - 格式的表達(dá)變化:僅就目前代碼中
JSON
和XML
的表達(dá)而言,一旦其實(shí)現(xiàn)方法出現(xiàn)變化,則方法需要修改。
理想情況是需求中的任何改變都不會(huì)導(dǎo)致.serialize()
方法做出修改。
Looking for a Common Interface
觀察復(fù)雜的條件判斷代碼,首先需要識(shí)別出每個(gè)執(zhí)行路徑(邏輯路徑)的共同目標(biāo)(common goal of each of the execution path/logical paths)。
比如前面代碼每個(gè)邏輯路徑都將song
對(duì)象轉(zhuǎn)換為不同格式的string
表達(dá)。基于該目標(biāo),需要找到一個(gè)共同接口(common interface),該接口可用于替換每個(gè)路徑上的邏輯。在該案例中,這個(gè)接口輸入song
對(duì)象并輸出一個(gè)string
。
有了共同接口,就可為每個(gè)邏輯路徑提供獨(dú)立的實(shí)現(xiàn)方法。
接下來就需要提供一個(gè)獨(dú)立的部分用于決定使用哪種具體的實(shí)現(xiàn)方法。該獨(dú)立部分根據(jù)format的值返回其對(duì)應(yīng)的實(shí)現(xiàn)方式。
下面對(duì)代碼進(jìn)行重構(gòu)(refactor),即the process of changing a software system in such a way that does not alter the external behavior of the code yet improves its internal structure。
Refactoring
目標(biāo):建立接口/函數(shù),對(duì)其輸入Song
對(duì)象并返回string
表達(dá)。
第一步將其中一個(gè)邏輯路徑重構(gòu)為這種接口。加入一個(gè)新的方法._serialize_to_json()
,并將JSON序列化的內(nèi)容放在新方法中
class SongSerializer:
def serialize(self, song, format):
if format == 'JSON':
return self._serialize_to_json(song)
# The rest of the code remains the same
def _serialize_to_json(self, song):
payload = {
'id': song.song_id,
'title': song.title,
'artist': song.artist
}
return json.dumps(payload)
接著實(shí)現(xiàn)另一個(gè)操作邏輯
class SongSerializer:
def serialize(self, song, format):
if format == 'JSON':
return self._serialize_to_json(song)
elif format == 'XML':
return self._serialize_to_xml(song)
else:
raise ValueError(format)
def _serialize_to_json(self, song):
payload = {
'id': song.song_id,
'title': song.title,
'artist': song.artist
}
return json.dumps(payload)
def _serialize_to_xml(self, song):
song_element = et.Element('song', attrib={'id': song.song_id})
title = et.SubElement(song_element, 'title')
title.text = song.title
artist = et.SubElement(song_element, 'artist')
artist.text = song.artist
return et.tostring(song_element, encoding='unicode')
重構(gòu)后的代碼已經(jīng)便于閱讀和維護(hù),但仍然可以進(jìn)一步優(yōu)化,下面用基本的工廠方法實(shí)現(xiàn)。
工廠方法的基本實(shí)現(xiàn)
工廠方法的核心是提供一個(gè)獨(dú)立的部分,用于決定在指定參數(shù)的情況下該選擇哪個(gè)具體的實(shí)現(xiàn)方式。在本案例中,該指定參數(shù)就是format
。
為完成工廠方法的實(shí)現(xiàn),加入新方法._get_serializer()
,輸入?yún)?shù)為format
,該方法將根據(jù)format
值放回匹配的序列化方法。
class SongSerializer:
def _get_serializer(self, format):
if format == 'JSON':
return self._serialize_to_json
elif format == 'XML':
return self._serialize_to_xml
else:
raise ValueError(format)
注意該._get_serializer()
犯法不會(huì)調(diào)用具體的實(shí)現(xiàn)方法,僅僅返回函數(shù)對(duì)象本身。
接下來可以改變SongSerializer
類的.serialize()
方法,調(diào)用._get_serializer()
方法完成工廠方法的實(shí)現(xiàn),如下
class SongSerializer:
def serialize(self, song, format):
serializer = self._get_serializer(format)
return serializer(song)
def _get_serializer(self, format):
if format == 'JSON':
return self._serialize_to_json
elif format == 'XML':
return self._serialize_to_xml
else:
raise ValueError(format)
def _serialize_to_json(self, song):
payload = {
'id': song.song_id,
'title': song.title,
'artist': song.artist
}
return json.dumps(payload)
def _serialize_to_xml(self, song):
song_element = et.Element('song', attrib={'id': song.song_id})
title = et.SubElement(song_element, 'title')
title.text = song.title
artist = et.SubElement(song_element, 'artist')
artist.text = song.artist
return et.tostring(song_element, encoding='unicode')class SongSerializer:
def serialize(self, song, format):
serializer = self._get_serializer(format)
return serializer(song)
def _get_serializer(self, format):
if format == 'JSON':
return self._serialize_to_json
elif format == 'XML':
return self._serialize_to_xml
else:
raise ValueError(format)
def _serialize_to_json(self, song):
payload = {
'id': song.song_id,
'title': song.title,
'artist': song.artist
}
return json.dumps(payload)
def _serialize_to_xml(self, song):
song_element = et.Element('song', attrib={'id': song.song_id})
title = et.SubElement(song_element, 'title')
title.text = song.title
artist = et.SubElement(song_element, 'artist')
artist.text = song.artist
return et.tostring(song_element, encoding='unicode')
.serialize()
方法是應(yīng)用代碼,它依賴一個(gè)接口完成序列化任務(wù),被稱作模式的客戶端部分(the client component of the pattern)。在本案例中,產(chǎn)品定義為輸入Song
對(duì)象且返回string
表達(dá)的一個(gè)函數(shù)。._serialize_to_json()
和._serialize_to_xml()
方法都是產(chǎn)品中的具體實(shí)現(xiàn)。最終._get_serializer()
方法稱為創(chuàng)建器(creator),決定使用哪種實(shí)現(xiàn)方法。
到目前為止的代碼中所有方法都是SongSerializer
類的成員,但注意到新加入的方法并沒有使用self
參數(shù)。這表明他們不該成為SongSerializer
類的方法而該成為外部函數(shù)。
class SongSerializer:
def serialize(self, song, format):
serializer = get_serializer(format)
return serializer(song)
def get_serializer(format):
if format == 'JSON':
return _serialize_to_json
elif format == 'XML':
return _serialize_to_xml
else:
raise ValueError(format)
def _serialize_to_json(song):
payload = {
'id': song.song_id,
'title': song.title,
'artist': song.artist
}
return json.dumps(payload)
def _serialize_to_xml(song):
song_element = et.Element('song', attrib={'id': song.song_id})
title = et.SubElement(song_element, 'title')
title.text = song.title
artist = et.SubElement(song_element, 'artist')
artist.text = song.artist
return et.tostring(song_element, encoding='unicode')
工廠方法的機(jī)制總是相同:
- 一個(gè)客戶端(client)(
SongSerializer.serializer()
)依賴接口的具體實(shí)現(xiàn),并要求創(chuàng)建者(creator)(.get_serializer()
)來實(shí)現(xiàn),并傳入識(shí)別符(identifier)(format
)。 - 創(chuàng)建者根據(jù)客戶端傳入的參數(shù)值返回具體的實(shí)現(xiàn),客戶端由創(chuàng)建者提供的對(duì)象完成任務(wù)。
執(zhí)行如下
>>> song = Song("1", "Water of Love", "Dire Straits")
>>> serializer = SongSerializer()
>>> serializer.serialize(song, 'JSON')
'{"id": "1", "title": "Water of Love", "artist": "Dire Straits"}'
>>> serializer.serialize(song, 'XML')
'<song id="1"><title>Water of Love</title><artist>Dire Straits</artist></song>'
>>> serializer.serialize(song, 'YAML')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in serialize
File "<stdin>", line 7, in get_serializer
ValueError: YAML
盤點(diǎn)
工廠方法的核心在于提供一個(gè)獨(dú)立的部分,該部分將根據(jù)特定參數(shù)來決定才用哪個(gè)具體的方法。該參數(shù)在上面案例中就是format
。
工廠方法的使用場(chǎng)景:應(yīng)用(客戶端)依賴于某個(gè)接口(產(chǎn)品)用于執(zhí)行特定任務(wù),且該接口提供任務(wù)的多種具體實(shí)現(xiàn)方式。開發(fā)者提供參數(shù)用于決定具體采用哪種實(shí)現(xiàn)方式,并在創(chuàng)建器(creator)中使用。
Case Study: Car Factory
(2024.04.20 Sat @KLN)
場(chǎng)景:需要支持不同車型和諸如啟動(dòng)、停止的功能,但在運(yùn)行之前不知道需要哪些車型。
Brute force approach:運(yùn)行時(shí)(at runtime),得到車型名字時(shí),用if-else/switch語句決定創(chuàng)建哪個(gè)車型。
from cars.ford import Ford
from cars.ferrari import Ferrari
from cars.generic import car
def get_car(car_name):
if car_name == 'Ford':
return Ford()
elif car_name == 'Ferrari':
return Ferrari()
else:
return GenericCar()
for car_name in ['Jeep', 'Ferrari', 'Tesla']:
car = get_car(car_name)
car.start()
car.stop()
這段代碼正常工作,但缺少可擴(kuò)展性,加入新車型需要修改實(shí)現(xiàn)并加入更多車型的import命令,這將破壞SOLID中的open/close原則。此外,直接實(shí)例化car類,破壞了SOLID中的dependency-inversion原則,因?yàn)橐蕾嚵诉@些類的實(shí)現(xiàn)。
對(duì)上面代碼重構(gòu),按照這個(gè)框架流程圖實(shí)現(xiàn)。
在框架中,看到
AbsCars
類,一個(gè)抽象類,其中的start
和stop
指明了在實(shí)現(xiàn)時(shí)需要具體實(shí)現(xiàn)這些方法。另有三個(gè)子類,Jeep
,Ford
和Ferrari
,均實(shí)現(xiàn)了AbsCars
類。
另有一個(gè)CarFactory
類,用于創(chuàng)建需要的車類實(shí)例。
import abc
class AbsCars(abc.ABC):
@abc.abstractmethod
def start(self):
pass
@abc.abstractmethod
def stop(self):
pass
start
和stop
都被聲明為抽象類方法,在繼承時(shí)需要實(shí)現(xiàn)。Ford
類如下,其他車類都類似。
from cars.abscars import AbsCars
class Ford(AbsCars):
def start(self):
if is_fuel_present:
print("ford engine is now running")
return
print("Low on fuel")
def stop(self):
print("Ford engine shutting down")
一個(gè)通用的類可表示為
from cars.abscars import AbsCars
class GenericCar(AbsCars):
def __init__(self, car_name):
self.name = car_name
def start(self):
print(f"{self.name} engine starting!")
def stop(self):
print(f"{self.name} engine stopping!")
定義CarFactory
類如下
from inspect import getmembers, isclass, isabstract
import cars
class CarFactory():
cars = {}
def __init__(self):
self.load_cars()
def load_cars(self):
classes = getmembers(cars, lambda m: isclass(m) and not isabstract(m))
for name, _type in classes:
if isclass(_type) and issubclass(_type, autos.AbsCars):
self.cars.update([[name, _type]])
def create_instance(self, car_name):
if car_name in self.cars:
return self.cars[car_name]()
else:
return cars.GenericCar(car_name)
load_cars
方法用于構(gòu)建cars
字典,先在cars
包中找到不是抽象類的所有類,再找到AbsCars
類的子類,并將其加入到cars
字典。
create_instance
方法從cars
字典中找到車名,如果有則返回類的實(shí)例,如果沒有則返回GenericCars
類的實(shí)例。
在主代碼中,僅僅引入和實(shí)例化AutoCars
類,并遍歷(loop through)車名字,為每個(gè)車名調(diào)用create_instance
方法。
from cars.carfactory import CarFactory
factory = CarFactory()
for car_name in ['Ford', 'Ferrari', 'Tesla']:
car = factory.create_instance(car_name)
car.start()
car.stop()
通過這種方法,在加入新車型時(shí)僅需要增加AbsCars
的子類,而不需要修改過多代碼。
Other Cases
(2024.04.20 Sat)
在工廠方法中可將不同邏輯路徑(logical path)的內(nèi)容置于擁有共同接口的不同的函數(shù)/類中,使用創(chuàng)建者作為具體的實(shí)現(xiàn)。輸入條件中的參數(shù)決定了使用哪種具體的實(shí)現(xiàn)。
從外部數(shù)據(jù)構(gòu)建相關(guān)對(duì)象:一個(gè)應(yīng)用需要從外部數(shù)據(jù)源或庫(kù)獲取雇員信息。數(shù)據(jù)包括雇員角色或類型,manager,office clerk,sales associate之類。應(yīng)用存儲(chǔ)一個(gè)識(shí)別符用于代表雇員類型,可使用工廠方法創(chuàng)建具體的雇員對(duì)象
相同特征的多種實(shí)現(xiàn)方式:圖像處理應(yīng)用需要將衛(wèi)星圖片從一種坐標(biāo)系統(tǒng)轉(zhuǎn)換到另一種,轉(zhuǎn)換方式根據(jù)準(zhǔn)確度的級(jí)別有多種實(shí)現(xiàn)算法。該應(yīng)用允許用戶選擇不同的具體算法,工廠方法根據(jù)用戶選擇提供了具體實(shí)現(xiàn)。
在共同接口下整合相似特征:仍然考慮前一個(gè)圖像處理的案例,某應(yīng)用需要對(duì)圖像應(yīng)用一個(gè)filter,可根據(jù)用戶輸入來識(shí)別/指定filter,工廠方法提供具體的filter實(shí)現(xiàn)。
整合相關(guān)應(yīng)用的外部服務(wù):音樂播放器應(yīng)用需要整合多個(gè)外部服務(wù),允許用戶選擇音樂來源。該應(yīng)用可以定義個(gè)公共接口,使用工廠方法根據(jù)用戶選擇創(chuàng)建正確的整合路徑。
上述情況的共同之處在于,都定義了一個(gè)依賴于共同接口(稱作產(chǎn)品)的客戶端,都提供了用于識(shí)別產(chǎn)品的具體實(shí)現(xiàn)的方式,也就都可以使用工廠方法。
Reference
1 realpython: factory method python
2 Refactoring: Improving the Design of Existing Code
3 dev點(diǎn)to, Factory Design Pattern in Python, Khushboo Parasrampuria
`