前言:
這幾天拜讀了“圖靈程序設(shè)計(jì)叢書(shū)”的《精通python設(shè)計(jì)模式》,個(gè)人感覺(jué)是一本不錯(cuò)的介紹python設(shè)計(jì)模式的專(zhuān)業(yè)書(shū)籍,本書(shū)介紹了16種設(shè)計(jì)模式,每種設(shè)計(jì)模式從基本簡(jiǎn)介、現(xiàn)實(shí)生活例子、軟件的例子、應(yīng)用案例、代碼實(shí)現(xiàn)五個(gè)方面一一介紹,上手簡(jiǎn)單,學(xué)習(xí)條例清晰。推薦閱讀!!!接下來(lái)就將我的讀書(shū)筆記整理在這。
一、設(shè)計(jì)模式簡(jiǎn)介:
設(shè)計(jì)模式重要的部分可能就是它的名稱(chēng)。給模式起名的好處是大家相互交流時(shí)有共同的詞匯。因此,如果你提交一些代碼進(jìn)行評(píng)審,同行評(píng)審者的反饋中提到“我認(rèn)為這個(gè)地方你可以使用一個(gè)策略模式來(lái)代替……”,即使你不知道或不記得策略模式是什么,也可以立即去查閱。
隨著編程語(yǔ)言的演進(jìn),一些設(shè)計(jì)模式(如單例)也隨之過(guò)時(shí),甚至成了反模式,另一些則被內(nèi)置在編程語(yǔ)言中(如迭代器模式)。另外,也有一些新的模式誕生(比如Borg/Monostate)
關(guān)于設(shè)計(jì)模式有一些誤解。第一個(gè)誤解是,一開(kāi)始寫(xiě)代碼就應(yīng)該使用設(shè)計(jì)模式。第二個(gè)誤解是設(shè)計(jì)模式應(yīng)隨處使用。這會(huì)導(dǎo)致方案很復(fù)雜,夾雜著多余的接口和分層,而其實(shí)往往一個(gè)更簡(jiǎn)單直接的方案就足夠了。設(shè)計(jì)模式并不是萬(wàn)能的,僅當(dāng)代碼確實(shí)存在壞味道、難以擴(kuò)展維護(hù)時(shí),才有使用的必要。
設(shè)計(jì)模式是被發(fā)現(xiàn),而不是被發(fā)明出來(lái)的
二、設(shè)計(jì)模式三大基本類(lèi)型
設(shè)計(jì)模式分三大部分:
- 第一部分介紹處理對(duì)象創(chuàng)建的設(shè)計(jì)模式,包括工廠(chǎng)模式、建造者模式、原型模式;
- 第二部分介紹處理一個(gè)系統(tǒng)中不同實(shí)體(類(lèi)、對(duì)象等)之間關(guān)系的設(shè)計(jì)模式, 包括外觀(guān)模式、享元模式等 ;
- 第三部分介紹處理系統(tǒng)實(shí)體之間通信的設(shè)計(jì)模式,包括責(zé)任鏈模式、觀(guān)察者模式等。
三、創(chuàng)建型設(shè)計(jì)模式
處理對(duì)象創(chuàng)建相關(guān)的問(wèn)題,目標(biāo)是當(dāng)直接創(chuàng)建對(duì)象(在Python中是通過(guò)_init_()函數(shù)實(shí)現(xiàn)的,)不太方便時(shí),提供更好的方式。
①. 工廠(chǎng)模式
簡(jiǎn)介:
在工廠(chǎng)設(shè)計(jì)模式中,客戶(hù)端可以請(qǐng)求一個(gè)對(duì)象,而無(wú)需知道這個(gè)對(duì)象來(lái)自哪里;也就是,使用哪個(gè)類(lèi)來(lái)生成這個(gè)對(duì)象。工廠(chǎng)背后的思想是簡(jiǎn)化對(duì)象的創(chuàng)建。與客戶(hù)端自己基于類(lèi)實(shí)例化直 接創(chuàng)建對(duì)象相比,基于一個(gè)中心化函數(shù)來(lái)實(shí)現(xiàn),更易于追蹤創(chuàng)建了哪些對(duì)象。通過(guò)將創(chuàng)建對(duì)象的代碼和使用對(duì)象的代碼解耦,工廠(chǎng)能夠降低應(yīng)用維護(hù)的復(fù)雜度。
工廠(chǎng)通常有兩種形式:
第一種是工廠(chǎng)方法(Factory Method),它是一個(gè)方法(或以地道的Python 術(shù)語(yǔ)來(lái)說(shuō),是一個(gè)函數(shù)),對(duì)不同的輸入?yún)?shù)返回不同的對(duì)象;
現(xiàn)實(shí)生活的例子:
現(xiàn)實(shí)中用到工廠(chǎng)方法模式思想的一個(gè)例子是塑料玩具制造。制造塑料玩具的壓塑粉都是一樣的,但使用不同的塑料模具就能產(chǎn)出不同的外形。比如,有一個(gè)工廠(chǎng)方法,輸入是目標(biāo)外形(鴨 子或小車(chē))的名稱(chēng),輸出則是要求的塑料外形。
軟件的例子:
Django框架使用工廠(chǎng)方法模式來(lái)創(chuàng)建表單字段。Django的forms模塊支持不同種類(lèi)字段(CharField、EmailField)的創(chuàng)建和定制(max_length、required)
應(yīng)用案例:
如果因?yàn)閼?yīng)用創(chuàng)建對(duì)象的代碼分布在多個(gè)不同的地方,而不是僅在一個(gè)函數(shù)方法中,你發(fā)現(xiàn)沒(méi)法跟蹤這些對(duì)象,那么應(yīng)該考慮使用工廠(chǎng)方法模式。工廠(chǎng)方法集中地在一個(gè)地方創(chuàng)建對(duì)象,使對(duì)象跟蹤變得更容易。注意,創(chuàng)建多個(gè)工廠(chǎng)方法也完全沒(méi)有問(wèn) 題,實(shí)踐中通常也這么做,對(duì)相似的對(duì)象創(chuàng)建進(jìn)行邏輯分組,每個(gè)工廠(chǎng)方法負(fù)責(zé)一個(gè)分組。例如,有一個(gè)工廠(chǎng)方法負(fù)責(zé)連接到不同的數(shù)據(jù)庫(kù)(MySQL、SQLite),另一個(gè)工廠(chǎng)方法負(fù)責(zé)創(chuàng)建要求的幾何對(duì)象(圓形、三角形)等,若需要將對(duì)象的創(chuàng)建和使用解耦,工廠(chǎng)方法也能派上用場(chǎng)。
.
另外一個(gè)值得一提的應(yīng)用案例與應(yīng)用性能及內(nèi)存使用相關(guān)。工廠(chǎng)方法可以在必要時(shí)創(chuàng)建新的對(duì)象,從而提高性能和內(nèi)存使用率。若直接實(shí)例化類(lèi)來(lái)創(chuàng)建對(duì)象,那么每次創(chuàng)建新對(duì)象就需要分配額外的內(nèi)存(除非這個(gè)類(lèi)內(nèi)部使用了緩存,一般情況下不會(huì)這 樣)。例如類(lèi)的兩次實(shí)例化的內(nèi)存地址不同
第二種是抽象工廠(chǎng)
第二種是抽象工廠(chǎng),它是一組用于創(chuàng)建一系列相關(guān)事物對(duì)象的工廠(chǎng)方法
抽象工廠(chǎng)設(shè)計(jì)模式是抽象方法的一種泛化。概括來(lái)說(shuō),一個(gè)抽象工廠(chǎng)是(邏輯上的)一組工廠(chǎng)方法,其中的每個(gè)工廠(chǎng)方法負(fù)責(zé)產(chǎn)生不同種類(lèi)的對(duì)象
現(xiàn)實(shí)生活的例子:
汽車(chē)制造業(yè)應(yīng)用了抽象工廠(chǎng)的思想。沖壓不同汽車(chē)模型的部件(車(chē)門(mén)、儀表盤(pán)、車(chē)篷、擋泥板及反光鏡等)所使用的機(jī)件是相同的。機(jī)件裝配起來(lái)的模型隨時(shí)可配置,且易于改變。
軟件的例子:
程序包django_factory是一個(gè)用于在測(cè)試中創(chuàng)建Django模型的抽象工廠(chǎng)實(shí)現(xiàn),可用來(lái)為支持測(cè)試專(zhuān)有屬性的模型創(chuàng)建實(shí)例。這能讓測(cè)試代碼的可讀性更高,且能避免共享不必要的代碼,故有其存在的價(jià)值
應(yīng)用案例:
為抽象工廠(chǎng)模式是工廠(chǎng)方法模式的一種泛化,所以它能提供相同的好處。這樣會(huì)產(chǎn)生一個(gè)問(wèn)題:我們?cè)趺粗篮螘r(shí)該使用工廠(chǎng)方法,何時(shí)又該使用抽象工廠(chǎng)?答案是, 通常一開(kāi)始時(shí)使用工廠(chǎng)方法,因?yàn)樗?jiǎn)單。如果后來(lái)發(fā)現(xiàn)應(yīng)用需要許多工廠(chǎng)方法,那么將創(chuàng)建 一系列對(duì)象的過(guò)程合并在一起更合理,從而最終引入抽象工廠(chǎng)。
工廠(chǎng)方法代碼實(shí)現(xiàn):
import xml.etree.ElementTree as etree
import json
class JSONConnector:
"""
類(lèi)JSONConnector解析JSON文件,通過(guò)parsed_data()方法以一個(gè)字典(dict)的形式 返回?cái)?shù)據(jù)。
修飾器property使parsed_data()顯得更像一個(gè)常規(guī)的變量,而不是一個(gè)方法
"""
def __init__(self, filepath):
print(444)
self.data = None
with open(filepath, mode='r', encoding='utf-8') as f:
self.data = json.load(f)
@property
def parsed_data(self):
return self.data
class XMLConnector:
"""
類(lèi)XMLConnector解析XML文件,通過(guò)parsed_data()方法以xml.etree.Element列表的形式返回所有數(shù)據(jù)
"""
def __init__(self, filepath):
self.tree = etree.parse(filepath)
@property
def parsed_data(self):
return self.tree
def connection_factory(filepath):
"""
函數(shù)connection_factory是一個(gè)工廠(chǎng)方法,基于輸入文件路徑的擴(kuò)展名返回一個(gè) JSONConnector或XMLConnector的實(shí)例
:param filepath:
:return: 返回一個(gè) JSONConnector或XMLConnector的實(shí)例
"""
print(filepath)
if filepath.endswith("json"):
connector = JSONConnector
elif filepath.endswith("xml"):
connector = XMLConnector
else:
raise ValueError('Cannot connect to {}'.format(filepath))
return connector(filepath)
def connect_to(filepath):
"""
函數(shù)connect_to()對(duì)connection_factory()進(jìn)行包裝,添加了異常處理
:param filepath:
:return:返回一個(gè)JSONConnector或XMLConnector的實(shí)例對(duì)象
"""
factory = None
try:
factory = connection_factory(filepath)
except ValueError as ve:
print(ve)
return factory
def main(filepath):
# 函數(shù)main()演示如何使用工廠(chǎng)方法設(shè)計(jì)模式。第一部分是確認(rèn)異常處理是否有效
data_factory = connect_to(filepath)
# 第二部分是得到數(shù)據(jù)格式對(duì)應(yīng)的處理
new_data = data_factory.parsed_data
# 第三部分是根據(jù)不同數(shù)據(jù)格式做特定的處理方法
if type(data_factory) == XMLConnector:
liars = new_data.findall(".//{}[{}='{}']".format('person', 'lastName', 'Liar'))
print('found: {} persons'.format(len(liars)))
for liar in liars:
print('first name: {}'.format(liar.find('firstName').text))
print('last name: {}'.format(liar.find('lastName').text))
[print('phone number ({})'.format(p.attrib['type']), p.text)
for p in liar.find('phoneNumbers')]
if type(data_factory) == JSONConnector:
for donut in new_data:
print('name: {}'.format(donut['name']))
print('price: ${}'.format(donut['ppu']))
[print('topping: {} {}'.format(t['id'], t['type'])) for t in donut['topping']]
if __name__ == '__main__':
main("person.xml")
抽象工廠(chǎng)代碼實(shí)現(xiàn):
"""
我們正在創(chuàng)造一個(gè)游戲,或者想在應(yīng)用中包含一個(gè)迷你游戲讓用戶(hù)娛樂(lè)娛樂(lè)。我們希望至少包含兩個(gè)游戲,
一個(gè)面向孩子,一個(gè)面向成人。在運(yùn)行時(shí),基于用戶(hù)輸入,決定該創(chuàng)建哪個(gè)游戲并運(yùn)行。游戲的創(chuàng)建部分由一個(gè)抽象工廠(chǎng)維護(hù)
"""
class Frog:
"""
孩子的游戲,我們將該游戲命名為FrogWorld。主人公是一只青蛙,喜歡吃蟲(chóng)子。每個(gè)英雄都需要一個(gè)好名字,在這個(gè)例子中,
這個(gè)名字在運(yùn)行時(shí)由用戶(hù)給定。方法interact_with()用于描述青蛙與障礙物(比如,蟲(chóng)子、迷宮或其他青蛙)之間的交互
"""
def __init__(self, name):
self.name = name
def __str__(self):
return self.name
def interact_with(self, obstacle):
print('{} the Frog encounters {} and {}!'.format(self, obstacle, obstacle.action()))
class Bug:
"""
障礙物可以有多種,但對(duì)于我們的例子,可以?xún)H僅是蟲(chóng)子。當(dāng)青蛙遇到一只蟲(chóng)子,只支持一 種動(dòng)作,那就是吃掉它!
"""
def __str__(self):
return 'a bug'
def action(self):
return 'eats it'
class FrogWorld:
"""
類(lèi)FrogWorld是一個(gè)抽象工廠(chǎng),其主要職責(zé)是創(chuàng)建游戲的主人公和障礙物。區(qū)分創(chuàng)建方法并使其名字通用(比如,make_character()
和make_obstacle()),這讓我們可以動(dòng)態(tài)改變當(dāng)前激活的工廠(chǎng)(也因此改變了當(dāng)前激活的游戲),而無(wú)需進(jìn)行任何代碼變更。
在一門(mén)靜態(tài)語(yǔ)言中, 抽象工廠(chǎng)是一個(gè)抽象類(lèi)/接口,具備一些空方法,但在Python中無(wú)需如此,因?yàn)轭?lèi)型是在運(yùn)行時(shí)檢測(cè)的
"""
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:
def __init__(self, name):
self.name = name
def __str__(self):
return self.name
def interact_with(self, obstacle):
print('{} the Wizard battles against {} and {}!'.format(self, obstacle, obstacle.action()))
class Ork:
def __str__(self):
return 'an evil ork'
def action(self):
return 'kills it'
class WizardWorld:
"""
WizardWorld游戲也類(lèi)似。在故事中唯一的區(qū)別是男巫戰(zhàn)怪獸(如獸人)而不是吃蟲(chóng)子!
"""
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:
"""
類(lèi)GameEnvironment是我們游戲的主入口。它接受factory作為輸入,
用其創(chuàng)建游戲的世界。方法play()則會(huì)啟動(dòng)hero和obstacle之間的交互
"""
def __init__(self, factory):
self.hero = factory.make_character()
self.obstacle = factory.make_obstacle()
def play(self):
self.hero.interact_with(self.obstacle)
def validate_age(name):
"""
函數(shù)validate_age()提示用戶(hù)提供一個(gè)有效的年齡。如果年齡無(wú)效,則會(huì)返回一個(gè)元組, 其第一個(gè)元素設(shè)置為False。
如果年齡沒(méi)問(wèn)題,元素的第一個(gè)元素則設(shè)置為T(mén)rue,但我們真正關(guān) 心的是元素的第二個(gè)元素,也就是用戶(hù)提供的年齡
:param name:
:return: ()
"""
try:
age = input('Welcome {}. How old are you? '.format(name))
age = int(age)
except ValueError as err:
print("Age {} is invalid, please try again...".format(age))
return (False, age)
return (True, age)
def main():
"""
該函數(shù)請(qǐng)求用戶(hù)的姓名和年齡,并根據(jù)用戶(hù)的年齡決定該玩哪個(gè)游戲
:return:
"""
name = input("Hello. What's your name? ")
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()
工廠(chǎng)模式總結(jié):
兩種模式都可以用于以下幾種場(chǎng)景:
(a)想要追蹤對(duì)象的創(chuàng)建時(shí),
(b)想要將對(duì)象的創(chuàng)建與使用解耦時(shí),
(c)想要優(yōu)化應(yīng)用的性能 和資源占用時(shí)。
.
工廠(chǎng)方法設(shè)計(jì)模式的實(shí)現(xiàn)是一個(gè)不屬于任何類(lèi)的單一函數(shù),負(fù)責(zé)單一種類(lèi)對(duì)象(一個(gè)形狀、 一個(gè)連接點(diǎn)或者其他對(duì)象)的創(chuàng)建。
.
抽象工廠(chǎng)設(shè)計(jì)模式的實(shí)現(xiàn)是同屬于單個(gè)類(lèi)的許多個(gè)工廠(chǎng)方法用于創(chuàng)建一系列種類(lèi)的相關(guān)對(duì)象(一輛車(chē)的部件、一個(gè)游戲的環(huán)境,或者其他對(duì)象)。
②.建造者模式
簡(jiǎn)介:
我們想要?jiǎng)?chuàng)建一個(gè)由多個(gè)部分構(gòu)成的對(duì)象,而且它的構(gòu)成需要一步接一步地完成。只有當(dāng)各個(gè)部分都創(chuàng)建好,這個(gè)對(duì)象才算是完整的。這正是建造者設(shè)計(jì)模式(Builder design pattern)的用武之地。建造者模式將一個(gè)復(fù)雜對(duì)象的構(gòu)造過(guò)程與其表現(xiàn)分離,這樣,同一個(gè)構(gòu)造 過(guò)程可用于創(chuàng)建多個(gè)不同的表現(xiàn)。
實(shí)際例子:
HTML頁(yè)面生成問(wèn)題可以使用建造者模式來(lái)解決。該模式中,有兩個(gè)參與者:建造者(builder)和指揮者(director)。建造者負(fù)責(zé)創(chuàng)建復(fù)雜對(duì)象的各個(gè)組成部分。在HTML例子中,這些組成部分是頁(yè)面標(biāo)題、文本標(biāo)題、內(nèi)容主體及頁(yè)腳。指揮者使用一個(gè)建造者實(shí)例控制建造的過(guò)程。對(duì)于 HTML示例,這是指調(diào)用建造者的函數(shù)設(shè)置頁(yè)面標(biāo)題、文本標(biāo)題等。使用不同的建造者實(shí)例讓我們可以創(chuàng)建不同的HTML頁(yè)面,而無(wú)需變更指揮者的代碼。
現(xiàn)實(shí)生活的例子:
快餐店使用的就是建造者設(shè)計(jì)模式。
軟件的例子:
django-widgy是一個(gè) Django的第三方樹(shù)編輯器擴(kuò)展,可用作內(nèi)容管理系統(tǒng)(Content Management System,CMS)。它 包含一個(gè)網(wǎng)頁(yè)構(gòu)建器,用來(lái)創(chuàng)建具有不同布局的HTML頁(yè)面
.
django-query-builder是另一個(gè)基于建造者模式的Django第三方擴(kuò)展庫(kù),該擴(kuò)展庫(kù)可用于動(dòng)態(tài)地構(gòu)建SQL查詢(xún)。使用它,我們能夠控制一個(gè)查詢(xún)的方方面面,并能創(chuàng)建不同種類(lèi)的查詢(xún),從簡(jiǎn) 單的到非常復(fù)雜的都可以
應(yīng)用場(chǎng)景:
如果我們知道一個(gè)對(duì)象必須經(jīng)過(guò)多個(gè)步驟來(lái)創(chuàng)建,并且要求同一個(gè)構(gòu)造過(guò)程可以產(chǎn)生不同的表現(xiàn),就可以使用建造者模式。這種需求存在于許多應(yīng)用中,例如頁(yè)面生成器(HTML頁(yè)面生成器之類(lèi))、文檔轉(zhuǎn)換器以及用戶(hù)界面(User Interface, UI)表單創(chuàng)建工具
代碼實(shí)現(xiàn)
工廠(chǎng)模式買(mǎi)電腦
MINI14 = '1.4GHz Mac mini'
class AppleFactory:
"""
工廠(chǎng)模式買(mǎi)電腦
"""
class MacMini14:
def __init__(self):
self.memory = 4 # 單位為GB
self.hdd = 500 # 單位為GB
self.gpu = 'Intel HD Graphics 5000'
def __str__(self):
info = ('Model: {}'.format(MINI14), 'Memory: {}GB'.format(self.memory),
'Hard Disk: {}GB'.format(self.hdd), 'Graphics Card: {}'.format(self.gpu))
return '\n'.join(info)
def build_computer(self, model):
if (model == MINI14):
return self.MacMini14()
else:
print("I dont't know how to build {}".format(model))
if __name__ == '__main__':
afac = AppleFactory()
mac_mini = afac.build_computer(MINI14)
print(mac_mini)
建造者模式定制電腦
class Computer:
def __init__(self, serial_number):
self.serial = serial_number
self.memory = None # 單位為GB
self.hdd = None # 單位為GB
self.gpu = None
def __str__(self):
info = ('Memory: {}GB'.format(self.memory), 'Hard Disk: {}GB'
.format(self.hdd), 'Graphics Card: {}'.format(self.gpu))
return '\n'.join(info)
class ComputerBuilder:
def __init__(self):
self.computer = Computer('AG23385193')
def configure_memory(self, amount):
self.computer.memory = amount
def configure_hdd(self, amount):
self.computer.hdd = amount
def configure_gpu(self, gpu_model):
self.computer.gpu = gpu_model
class HardwareEngineer:
def __init__(self):
self.builder = None
def construct_computer(self, memory, hdd, gpu):
self.builder = ComputerBuilder()
[
i for i in (self.builder.configure_memory(memory),
self.builder.configure_hdd(hdd),
self.builder.configure_gpu(gpu)
)
]
@property
def computer(self):
return self.builder.computer
def main():
engineer = HardwareEngineer()
engineer.construct_computer(hdd=500, memory=8, gpu='GeForce GTX 650 Ti')
computer = engineer.computer
print(computer)
if __name__ == '__main__':
main()
工廠(chǎng)模式和建造者模式的的區(qū)別:
在工廠(chǎng)模式下,會(huì)立即返回一個(gè)創(chuàng)建好的對(duì)象;而在建造者模式下,僅在需要時(shí)客戶(hù)端代碼才顯式地請(qǐng)求指揮者返回最終的對(duì)象。
.
簡(jiǎn)單明了的例子:
買(mǎi)電腦的例子也許有助于區(qū)分建造者模式和工廠(chǎng)模式。
假設(shè)你想購(gòu)買(mǎi)一臺(tái)新電腦,如果決定購(gòu)買(mǎi)一臺(tái)特定的預(yù)配置的電腦型號(hào),則是在使用工廠(chǎng)模式。
假設(shè)你想定制一臺(tái)新電腦,使用的即是建造者模式。你是指揮者,向制造商(建造者)提供指令說(shuō)明心中理想的電腦規(guī)格。
建造者模式的總結(jié):
我們學(xué)習(xí)了如何使用建造者設(shè)計(jì)模式。可以在工廠(chǎng)模式(工廠(chǎng)方法或抽象工廠(chǎng))不適用的一些場(chǎng)景中使用建造者模式創(chuàng)建對(duì)象。在以下幾種情況下,與工廠(chǎng)模式相比,建造者模式是更好的選擇。
1. 想要?jiǎng)?chuàng)建一個(gè)復(fù)雜對(duì)象(對(duì)象由多個(gè)部分構(gòu)成,且對(duì)象的創(chuàng)建要經(jīng)過(guò)多個(gè)不同的步驟, 這些步驟也許還需遵從特定的順序)。
2. 要求一個(gè)對(duì)象能有不同的表現(xiàn),并希望將對(duì)象的構(gòu)造與表現(xiàn)解耦。
3. 想要在某個(gè)時(shí)間點(diǎn)創(chuàng)建對(duì)象,但在稍后的時(shí)間點(diǎn)再訪(fǎng)問(wèn)
③.原型模式
簡(jiǎn)介:
原型設(shè)計(jì)模式(Prototype design pattern)幫助我們創(chuàng)建對(duì)象的克隆,其最簡(jiǎn)單的形式就是一個(gè)clone函數(shù),接受一個(gè)對(duì)象作為輸入?yún)?shù),返回輸入對(duì)象的一個(gè)副本。在Python中,這可以使用copy.deepcopy()函數(shù)來(lái)完成。
現(xiàn)實(shí)生活的例子:
原型設(shè)計(jì)模式無(wú)非就是克隆一個(gè)對(duì)象。有絲分裂,即細(xì)胞分裂的過(guò)程,是生物克隆的一個(gè)例子。
另一個(gè)著名的(人工)克隆例子是多利羊
軟件的例子:
很多Python應(yīng)用都使用了原型模式,但幾乎都不稱(chēng)之為原型模式,因?yàn)閷?duì)象克隆是編程語(yǔ)言的一個(gè)內(nèi)置特性。
.
可視化工具套件(Visualization Toolkit,VTK)是原型模式的一個(gè)應(yīng)用。VTK是一個(gè)開(kāi)源的跨平臺(tái)系統(tǒng),用于三維計(jì)算機(jī)圖形/圖片處理以及可視化。VTK使用原型模式來(lái)創(chuàng)建幾何元素(比如,點(diǎn)、線(xiàn)、六面體等)的克隆。
.
music21也是使用原型模式的項(xiàng)目。根據(jù)該項(xiàng)目頁(yè)面所述,“music21是一組工具,幫助學(xué)者和其他積極的聽(tīng)眾快速簡(jiǎn)便地得到音樂(lè)相關(guān)問(wèn)題的答案”。 music21工具套件使用原型模式來(lái)復(fù)制音符和總譜。
應(yīng)用案例:
當(dāng)我們已有一個(gè)對(duì)象,并希望創(chuàng)建該對(duì)象的一個(gè)完整副本時(shí),原型模式就派上用場(chǎng)了。在我們知道對(duì)象的某些部分會(huì)被變更但又希望保持原有對(duì)象不變之時(shí),通常需要對(duì)象的一個(gè)副本。在這樣的案例中,重新創(chuàng)建原有對(duì)象是沒(méi)有意義的。
.
另一個(gè)案例是,當(dāng)我們想復(fù)制一個(gè)復(fù)雜對(duì)象時(shí),使用原型模式會(huì)很方便。對(duì)于復(fù)制復(fù)雜對(duì)象,我們可以將對(duì)象當(dāng)作是從數(shù)據(jù)庫(kù)中獲取的,并引用其他一些也是從數(shù)據(jù)庫(kù)中獲取的對(duì)象。若通過(guò)多次重復(fù)查詢(xún)數(shù)據(jù)來(lái)創(chuàng)建一個(gè)對(duì)象,則要做很多工作。在這種場(chǎng)景下使用原型模式要方便得多。
代碼實(shí)現(xiàn)
from collections import OrderedDict
import copy
class Book:
"""
Book類(lèi)展示了一種有趣的技術(shù)可避免可伸縮構(gòu)造器問(wèn)題。在__init__() 方法中,僅有三個(gè)形參是固定的:
name、authors和price,但是使用rest變長(zhǎng)列表,調(diào)用者 能以關(guān)鍵詞的形式(名稱(chēng)=值)傳入更多的參數(shù)。
self.__dict__.update(rest)一行將rest 的內(nèi)容添加到Book類(lèi)的內(nèi)部字典中,成為它的一部分。
"""
def __init__(self, name, authors, price, **rest):
'''rest的例子有: 出版商、長(zhǎng)度、 標(biāo)簽、出版日期等等'''
self.name = name
self.authors = authors
self.price = price # 單位為美元
self.__dict__.update(rest)
def __str__(self):
"""
我們并不知道所有被添加參數(shù)的名稱(chēng),但又需要訪(fǎng)問(wèn)內(nèi)部字典將這些參數(shù) 應(yīng)用到__str__()中,并且字典的內(nèi)容并不遵循
任何特定的順序,所以使用一個(gè)OrderedDict來(lái)強(qiáng)制元素有序,否則,每次程序執(zhí)行都會(huì)產(chǎn)生不同的輸出。
:return: str
"""
mylist = []
ordered = OrderedDict(sorted(self.__dict__.items()))
for i in ordered.keys():
mylist.append('{}: {}'.format(i, ordered[i]))
if i == 'price':
mylist.append('$')
mylist.append('\n')
return ''.join(mylist)
class Prototype:
"""
Prototype類(lèi)實(shí)現(xiàn)了原型設(shè)計(jì)模式。Prototype類(lèi)的核心是clone()方法,該方法使用我們 熟悉的copy.deepcopy()
函數(shù)來(lái)完成真正的克隆工作。但Prototype類(lèi)在支持克隆之外做了一 點(diǎn)更多的事情,它包含了方法register()和unregister(),
這兩個(gè)方法用于在一個(gè)字典中追 蹤被克隆的對(duì)象。注意這僅是一個(gè)方便之舉,并非必需。
"""
def __init__(self):
self.objects = dict()
def register(self, identifier, obj):
self.objects[identifier] = obj
def unregister(self, identifier):
del self.objects[identifier]
def clone(self, identifier, **attr):
"""
clone()方法和Book類(lèi)中的__str__使用了相同的技巧,但這次是因?yàn)閯e的原因。使 用變長(zhǎng)列表attr,
我們可以?xún)H傳遞那些在克隆一個(gè)對(duì)象時(shí)真正需要變更的屬性變量
:param identifier:版本號(hào)
:param attr: 新添加的屬性
:return: obj
"""
found = self.objects.get(identifier)
if not found:
raise ValueError('Incorrect object identifier: {}'.format(identifier))
obj = copy.deepcopy(found)
obj.__dict__.update(attr)
return obj
def main():
"""
main()函數(shù)以實(shí)踐的方式展示了本節(jié)開(kāi)頭提到的《C程序設(shè)計(jì)語(yǔ)言》一書(shū)克隆的例子。克隆 該書(shū)的第一個(gè)版本來(lái)創(chuàng)建第二個(gè)版本,
我們僅需要傳遞已有參數(shù)中被修改參數(shù)的值,但也可以傳遞額外的參數(shù)。在這個(gè)案例中,edition就是一個(gè)新參數(shù),
在書(shū)的第一個(gè)版本中并不需要,但對(duì) 于克隆版本卻是很有用的信息。
:return:
"""
b1 = Book('The C Programming Language', ('Brian W. Kernighan', 'Dennis M.Ritchie'), price=118,
publisher='Prentice Hall', length=228, publication_date='1978-02-22',
tags=('C', 'programming', 'algorithms', 'data structures'))
prototype = Prototype()
cid = 'k&r-first'
prototype.register(cid, b1)
b2 = prototype.clone(cid, name='The C Programming Language(ANSI)', price=48.99, length=274,
publication_date='1988-04-01', edition=2)
for i in (b1, b2):
print(i)
print("ID b1 : {} != ID b2 : {}".format(id(b1), id(b2)))
if __name__ == '__main__':
main()
原型模式的總結(jié):
深副本與淺副本。
深副本:原始對(duì)象的所有數(shù)據(jù)都被簡(jiǎn)單地復(fù)制到克隆對(duì)象中,沒(méi)有例外。
淺副本:則依賴(lài)引用,我們可以引入數(shù)據(jù)共享和寫(xiě)時(shí)復(fù)制一類(lèi)的技術(shù)來(lái)優(yōu)化性能(例如,減小克隆對(duì)象的創(chuàng)建時(shí)間)和內(nèi)存使用。如果可用資源有限(例如,嵌入式系統(tǒng))或性能至關(guān)重 要(例如,高性能計(jì)算),那么使用淺副本可能更佳。
.
在Python中,可以使用copy.copy函數(shù)進(jìn)行淺復(fù)制。以下內(nèi)容引用自Python官方文檔,說(shuō)明了淺副本copy.copy和深副本(copy.deepcopy())之間的區(qū)別。
淺副本構(gòu)造一個(gè)新的復(fù)合對(duì)象后,(會(huì)盡可能地)將在原始對(duì)象中找到的對(duì)象的引用插入新對(duì)象中。
深副本構(gòu)造一個(gè)新的復(fù)合對(duì)象后,會(huì)遞歸地將在原始對(duì)象中找到的對(duì)象的副本插入新對(duì)象中。
.
原型模式用于創(chuàng)建對(duì)象的完全副本。確切地說(shuō),創(chuàng)建一個(gè)對(duì)象的副本可以指代以下兩件事情。
第一種:當(dāng)創(chuàng)建一個(gè)淺副本時(shí),副本依賴(lài)引用
第二種:當(dāng)創(chuàng)建一個(gè)深副本時(shí),副本復(fù)制所有東西
.
第一種情況中:
我們關(guān)注提升應(yīng)用性能和優(yōu)化內(nèi)存使用,在對(duì)象之間引入數(shù)據(jù)共享,但需要小心地修改數(shù)據(jù),因?yàn)樗凶兏鼘?duì)所有副本都是可見(jiàn)的。淺副本在本章中沒(méi)有過(guò)多介紹,但也許 你會(huì)想試驗(yàn)一下。
第二種情況中:
我們希望能夠?qū)σ粋€(gè)副本進(jìn)行更改而不會(huì)影響其他對(duì)象。對(duì)于我們之前看到的蛋糕食譜示例這類(lèi)案例,這一特性是很有用的。這里不會(huì)進(jìn)行數(shù)據(jù)共享,所以需要關(guān)注因?qū)ο?克隆而引入的資源耗用問(wèn)題。