本文參考《設計模式——可復用面向對象軟件基礎》
完整代碼請看: https://github.com/ubwshook/PythonStudy/tree/master/design_patterns
談及設計模式就不得不的提起《設計模式——可復用面向對象軟件基礎》厉斟,這是一部設計模式的經(jīng)典書籍璃岳,它歸納了常用的23中設計模式郊尝。設計模式使人們可以更加簡單和方便地復用成功的設計和體系結構鹅很。 該書中主要是采用C++作為示例語言醉者,雖然設計模式是一種編程思想啼器,但是不同語言所提供的特性會讓設計模式的實現(xiàn)非常的不同玛追。Python的一等函數(shù)以及其他動態(tài)特性都會使設計模式的實現(xiàn)產(chǎn)生變化税课。
我們將對書中提到設計模式使用Python語言進行分析重構,所使用的例子盡可能與書中相同豹缀,下面讓我們一起開始Python設計模式巡禮的第一站——創(chuàng)建型模式伯复!
依據(jù)設計模式的目的可分為 創(chuàng)建型(Creational)、結構型(Structural)邢笙、行為型(Beaviroal) 三種啸如。創(chuàng)建型模式與對象的創(chuàng)建有關; 結構型模式處理類或對象的組合; 行為型對類或對象怎么交互和怎樣分配職責進行描述。
0x0 . 迷宮問題描述以及初始Dome
迷宮使我們都玩過的一種游戲氮惯,我們現(xiàn)在來關注一個迷宮怎么創(chuàng)建叮雳。我們將一個迷宮定義為一系列的房間,一個房間要知道他的鄰居妇汗;可能的鄰居要么是另一個房間帘不,要么是一堵墻、或者是到另一個房間的一扇門杨箭。類Room寞焙、Door、Wall定義了我們所有的例子中使用到的構建。
首先我們定義一個MapSite類:
class MapSite(object):
"""
類MapSite是所有迷宮組件的公共抽象類捣郊。我了簡化例子辽狈,MapSite僅定義了一個操作Enter,它取決于你在進入什么呛牲。
如果你進入一個房間刮萌,那么你的位置會發(fā)生改變。如果你試圖進入一扇門娘扩,那么這兩件事中就有一件會發(fā)生:如果們是開
著的着茸,你進入另一個房間。如果門是關著的琐旁,那么你就會碰壁涮阔。
"""
def enter(self):
"""
Enter是為更加復雜的有些操作提供了一個簡單基礎。例如灰殴,如果你在一個房間中說“向東走”澎语,
游戲只能確定直接在東邊是哪一個MapSite并對它調(diào)用Enter。特定子類的Enter操作將計算
出你的位置是發(fā)生改變验懊,還是碰壁。
:return:
"""
return
Room是MapSite的一個子類尸变,用來定義房間:
class Room(MapSite):
"""
Room是MapSite的一個具體子類义图,而MapSite定義了迷宮中構件之間的主要關系。
Room是指向其他MapSite對象的引用召烂,并保存一個房間號碱工,用來標識迷宮中的房間
"""
def __init__(self, room_no):
super(Room, self).__init__()
self._room_no = room_no
self._sides = {'East': None, 'West': None, 'North': None, 'South': None }
def get_side(self, direction):
return self._sides[direction]
def set_side(self, direction, map_site):
if direction not in ['East', 'West', 'North', 'South']:
print("Dirction is invalid!")
self._sides[direction] = map_site
def get_no(self):
return self._room_no
Room中初始化的時候會設置房間號。我們可以使用set_side和get_side來設置或獲取房間某一面的對象奏夫。
定義Wall類怕篷,Wall來表示墻,墻這里比較簡單沒有額外的東西:
class Wall(MapSite):
"""
這個類描述的是墻酗昼,在demo中是比較簡單的
"""
def __init__(self):
super(Wall, self).__init__()
定義Door類廊谓,Door表示一扇門,需要知道門聯(lián)通了那兩個房間:
class Door(MapSite):
"""
這個類描述的是門這個對象
"""
def __init__(self, room1: Room, room2: Room):
super(Door, self).__init__()
self._room1 = room1
self._room2 = room2
self._is_open = 0
def other_side_from(self, room):
"""
獲取一個房間另一面的房間
:param room:
:return: 輸入room另一面的room
"""
if room.get_no() == self._room1.get_no():
return self._room2
elif room.get_no() == self._room2.get_no() :
return self._room1
else:
print("Room is wrong!")
return None
定義一個Maze類麻削,用來表示房間的集合蒸痹。可以從中按照房間號去獲取room對象:
class Maze(object):
"""
迷宮中房間集合的類呛哟,可以向迷宮中添加或者獲取房間
"""
def __init__(self):
self.rooms = {}
def add_room(self, room: Room):
self.rooms[room.get_no()] = room
def get_room(self, room_no):
return self.rooms[room_no]
我們的最終目標是為迷宮游戲創(chuàng)建迷宮叠荠,這里有一個迷宮游戲類MazeGame,其中crete_maze函數(shù)是創(chuàng)建一個迷宮的方法, 簡單創(chuàng)建一個迷宮如下, 創(chuàng)建兩個房間扫责,兩個房間用door連接榛鼎,這是一個非常簡單迷宮。
class MazeGame(object):
"""
Maze游戲類,沒有完整去實現(xiàn)功能者娱,我們主要關注創(chuàng)建型模式抡笼,所以這里只描述創(chuàng)建函數(shù)的實現(xiàn)。
"""
@staticmethod
def create_maze():
maze = Maze()
room1 = Room(1)
room2 = Room(2)
door = Door(room1, room2)
maze.add_room(room1)
maze.add_room(room2)
room1.set_side('North', Wall())
room1.set_side('East', door)
room1.set_side('South', Wall())
room1.set_side('West', Wall())
room2.set_side('North', Wall())
room2.set_side('East', Wall())
room2.set_side('South', Wall())
room2.set_side('West', door)
return maze
對于一個僅有兩個房間的迷宮來說肺然,這套代碼是相當?shù)膹碗s的蔫缸。而它真正的問題是不靈活,它對迷宮的布局采用了硬編碼际起,這意味著改變布局拾碌,就要改變這個成員函數(shù)。
考慮另一種情況街望,你想設計一個試了魔法的迷宮校翔,這里面的Room、Wall灾前、Door等組件都與基礎的不同防症,那么怎樣跟容易改變create_maze以讓它用這些新類型的對象創(chuàng)建迷宮。
創(chuàng)建型的設計模式將根據(jù)場景的不同進行實現(xiàn)哎甲。
0x1 . ABSTRACT FACTORY(抽象工廠)
1.意圖
提供一個創(chuàng)建一系列相關或者相互依賴對象的接口蔫敲,而無需指定它們具體的類。
2.別名
Kit
3.適用性:
- 一個系統(tǒng)要獨立與它的產(chǎn)品創(chuàng)建炭玫、組合和表示時奈嘿。
- 一個系統(tǒng)要由多個產(chǎn)品系列中的一個來配置時。
- 當你要強調(diào)一些列相關產(chǎn)品對象的設計以便進行聯(lián)合使用時吞加。
- 當你提供一個產(chǎn)品類庫裙犹,而只是想顯示它們的接口而不是實現(xiàn)時。
4.實現(xiàn)概要
將工廠作為單件衔憨,為創(chuàng)建函數(shù)設計接口叶圃,用于接收工廠實例為創(chuàng)建參數(shù)。工廠類可以被繼承践图,從而實現(xiàn)使用不同組件來進行創(chuàng)建掺冠。
5.效果:
- 它分離了具體的類:因為一個工廠封裝了創(chuàng)建對象的責任和過程,它將客戶與類的實現(xiàn)分離平项。
- 它使得替換產(chǎn)品系列變得很容易:只要改變工廠類赫舒,就可以更換產(chǎn)品系列接癌。
- 有利于產(chǎn)品的一致性:一個應用只能使用同一系列中的對象。
- 難以支持新種類的產(chǎn)品:抽象工廠確定了可以被創(chuàng)建的組件的集合荔燎,如果要支持新的組件的創(chuàng)建琐簇,就需要修改工廠接口婉商,可能影響多有子類的改變丈秩。
5.代碼例示:
依然是迷宮問題蘑秽,我們先設計一個MazeFactory,這個工廠類將提供各個組件的創(chuàng)建方法:
class MazeFactory(object):
"""
工廠類埂材,定義各個組件如何生成
可以被覆寫,定制不同工廠類
"""
def make_maze(self):
return Maze()
def make_wall(self):
return Wall()
def make_door(self, room1: Room, room2: Room):
return Door(room1, room2)
def make_room(self, room_no: int):
return Room(room_no)
為了使用factory我們的MazeFactory的create_maze方法將factory作為參數(shù),進行迷宮的創(chuàng)建:
class MazeGame(object):
"""
Maze游戲類, 這里設計的create_maze函數(shù)可以接收工廠對象進行類型初始化挤牛。
"""
def create_maze(self, factory):
maze = factory.make_maze()
room1 = factory.make_room(1)
room2 = factory.make_room(2)
door = factory.make_door(room1, room2)
maze.add_room(room1)
maze.add_room(room2)
room1.set_side('North', factory.make_wall())
room1.set_side('East', door)
room1.set_side('South', factory.make_wall())
room1.set_side('West', factory.make_wall())
room2.set_side('North', factory.make_wall())
room2.set_side('East', factory.make_wall())
room2.set_side('South', factory.make_wall())
room2.set_side('West', door)
return maze()
我們可以設計自己的工程類,從而定制自己的迷宮組件诫硕,比如我想要將迷宮的組件改為施加魔法的門或房間的時候,我就可以繼承MazeFactory類藕届,覆寫里面生成組件的方法休偶。
class EnchantedMazeFactory(MazeFactory):
"""
用于創(chuàng)建施加了魔法迷宮的工廠類踏兜,可以生成施加魔法的
"""
def make_door(self, room1: Room, room2: Room):
return EnchantedDoor(room1, room2)
def make_room(self, room_no: int):
return EnchantedRoom(room_no)
去創(chuàng)建一個施加了魔法的迷宮就變得很容易:
# 創(chuàng)建一個施加了魔法的迷宮
game = MazeGame()
game.create_maze(EnchantedMazeFactory())
0x2 . BUILDER(生成器)——對象創(chuàng)建型模型
1.意圖
將一個復雜對象的構建與它的表示分離计技,使得同樣的構建過程可以創(chuàng)建不同的表示。
2.適用性
- 當創(chuàng)建復雜對象的算應該獨立于該對象組成部分以及他們的裝配方式時睡雇。
- 當構造過程必須云訊被構造的對象有不同的表示時。
3.實現(xiàn)概要
builder會把組件的構造封裝在自己類內(nèi)部观蓄,對外不體現(xiàn)構建過程,只需要不斷生成組件亲茅,組件之間的聯(lián)系,也被封裝在builder內(nèi)部袭祟,使用者只需要調(diào)用builder的方法去生成即可胚膊。
4.效果
- 它使你可以改變一個產(chǎn)品的內(nèi)部表示:builder接口隱藏了產(chǎn)品的表示和內(nèi)部結構,同時也隱藏了該產(chǎn)品是如何裝配的喻犁。因為產(chǎn)品通過抽象接口構造的,你在改變產(chǎn)品內(nèi)部表示時所要做的只是定義一個新的生成器。
- 它將構造代碼和表示代碼分離:客戶不需要知道定義產(chǎn)品內(nèi)部結構的類的所有信息慨蛙,
- 它使你可以對構造過程進行更精細的控制: builder實在導向者的控制下一步步構造產(chǎn)品的异袄,能夠更好反應產(chǎn)品的構造過程封孙。這使你可以更加精細的控制構建過程。
5.代碼示例:
首先我們創(chuàng)建一定抽象類來定義一個builder必須要實現(xiàn)的方法锋勺,任何構造Maze的類都必須繼承它并實現(xiàn)對應的方法:
class MazeBuilder(object, metaclass=ABCMeta):
"""
迷宮生成器的抽象類贮勃,用于定義迷宮生成器必須實現(xiàn)的方法
"""
@abstractmethod
def build_maze(self):
pass
@abstractmethod
def build_room(self, room_no: int):
pass
@abstractmethod
def build_door(self, room1_no: int, room2_no: int):
pass
@abstractmethod
def get_maze(self):
pass
接下來我們來具體實現(xiàn)一個builder,這里我們把它稱為標準生成器:
class StandardMazeBuilder(MazeBuilder):
"""
一個標準迷宮生成器苏章,繼承于Mazebuilder
"""
def __init__(self):
"""
初始化一個字典用于存放迷宮中的房間
"""
self._current_maze = {}
def build_maze(self):
"""
每次調(diào)用創(chuàng)建迷宮就會初始化迷宮房間字典
:return:
"""
self._current_maze = {}
def build_room(self, room_no):
"""
創(chuàng)建一個房間寂嘉,并將其加入迷宮房間字典中
:param room_no: 房間編號
:return:
"""
if room_no not in self._current_maze.keys():
room = Room(room_no)
self._current_maze[room_no] = room
for direction in DIRECTIONS:
room.set_side(direction, Wall())
def build_door(self, room1_no, room2_no):
"""
為兩個房間之間創(chuàng)建一道門奏瞬,通過_common_wall確定門所在的方位
:param room1_no: 房間1編號
:param room2_no: 房間2編號
:return:
"""
room1 = self._current_maze[room1_no]
room2 = self._current_maze[room2_no]
door = Door(room1, room2)
room1.set_side(self._common_wall(room1, room2), door)
room2.set_side(self._common_wall(room2, room1), door)
def get_maze(self):
"""
返回迷宮信息
:return: 返回迷宮字典
"""
return self._current_maze
def _common_wall(self, room1: Room, room2: Room):
"""
假設迷宮寬度為8個房間,如果相鄰或者在迷宮中模8相等,才能獲得方位。
:param room1: 房間1編號
:param room2: 房間2編號
:return:
"""
room1_no = room1.get_no()
room2_no = room2.get_no()
if room2_no == room1_no + 1 and room1_no % 8 != 0:
return 'East'
if room1_no == room2_no + 1 and room2_no % 8 != 0:
return 'West'
if room1_no % 8 == room2_no % 8 and room1_no // 8 == room2_no // 8 + 1:
return 'North'
if room1_no % 8 == room2_no % 8 and room2_no // 8 == room1_no // 8 + 1:
return 'South'
return None
StandardMazeBuilder繼承了MazeBuilder并實現(xiàn)了抽象接口方法,整個maze的構建都在類的內(nèi)部進行趾唱,客戶只需要添加組件,builder會為用戶創(chuàng)建迷宮:
class MazeGame(object):
"""
迷宮游戲類涌乳,這里僅僅實現(xiàn)創(chuàng)建迷宮的方法
"""
def create_maze(self, builder):
builder.build_maze()
builder.build_room(1)
builder.build_room(2)
builder.build_door(1, 2)
return builder.get_maze()
0x3 . FACTORY METHOD(工廠方法)
1.意圖:
定義一個用于創(chuàng)建對象的接口,讓子類決定實例化哪一個類甜癞。Factory Method使一個類的實例化延遲到其子類夕晓。
2.別名:
虛構造器(virtual Constructor)
3.適用性:
- 當一個類不知道它所必須創(chuàng)建的對象的類的時候。
- 當一個類希望由它的子類來指定它所創(chuàng)建的對象的時候悠咱。
- 當類將創(chuàng)建對象的職責委托給多個幫助子類中的某一個蒸辆,并且你希望將哪一個幫助子類時代理者這一信息局部化的時候。
4.效果:
- 為子類提供掛鉤
- 連接平行類層次
5.實現(xiàn)概要
通俗的說析既,就是抽象類方法的應用躬贡,繼承+覆寫某些函數(shù)。
6.代碼示例:
工廠方法模式會再MazeGame類內(nèi)部定義各組件創(chuàng)建方法眼坏,并在創(chuàng)建迷宮函數(shù)create_maze中去調(diào)用:
class MazeGame(object):
def make_maze(self):
return Maze()
def make_room(self, room_no):
return Room(room_no)
def make_door(self, room1: Room, room2: Room):
return Door(room1, room2)
def make_wall(self):
return Wall()
def create_maze(self):
maze = self.make_maze()
room1 = self.make_room(1)
room2 = self.make_room(2)
door = self.make_door(room1, room2)
maze.add_room(room1)
maze.add_room(room2)
room1.set_side('North', self.make_wall())
room1.set_side('East', door)
room1.set_side('South', self.make_wall())
room1.set_side('West', self.make_wall())
room2.set_side('North', self.make_wall())
room2.set_side('East', self.make_wall())
room2.set_side('South', self.make_wall())
room2.set_side('West', door)
return maze
如果想要設計一種具有帶炸彈房間和被炸毀的墻的迷宮拂玻,需要繼承MazeGame()類覆寫其中的創(chuàng)建函數(shù)。
class BombedMazeGame(MazeGame):
def make_wall(self):
return BoomedWall()
def make_room(self, room_no):
return RoomWithBomb
0x4 . PROTOTYPE(原型)
1.意圖
用原型實例指定創(chuàng)建對象的種類宰译,并通過copy這些原型創(chuàng)建新的對象檐蚜。
2.適用性:
- 當實例化的類實在運行時刻指定時,例如沿侈,通過動態(tài)裝載闯第;
- 為了避免創(chuàng)建一個與產(chǎn)品類層次平行的工廠類層次時;
- 當一個類的實例只能有幾個不同狀態(tài)組合中的一種時缀拭。建立相應數(shù)目的原型并克隆他們可能比每次用合適的狀態(tài)手工實例化該類方便一些咳短。
3.效果
- 運行時刻增加或刪除產(chǎn)品填帽,運行的時候可以修改原型
- 改變值以制定新對象
- 改變結構以指定新對象
- 減少子類構造
- 用類動態(tài)配置應用
4.實現(xiàn)概要
為每個組件都設置原型,在創(chuàng)建時復制原型即可咙好。
5.代碼示例:
我們將構造一個MazePrototypeFactory類篡腌,它是一個使用原型模式的工廠類,其中的創(chuàng)建組件的方法均是對原型的copy
class MazePrototypeFactory(MazeFactory):
"""
使用原型模式的抽象工廠
"""
def __init__(self, room, wall, door, maze):
"""
初始化函數(shù)設置原型
:param room: 房間原型
:param wall: 墻壁原型
:param door: 門原型
:param maze: 迷宮原型
"""
self._prototype_maze = maze
self._prototype_room = room
self._prototype_wall = wall
self._prototype_door = door
def make_wall(self):
return deepcopy(self._prototype_wall)
def make_room(self, room_no):
"""
利用原型構建房間敷扫,這里使用的原型哀蘑,需要調(diào)用初始化之后才能使用,這與之前不同葵第。
:param room_no: 房間號
:return:
"""
room = deepcopy(self._prototype_room)
room.init(room_no)
return room
def make_door(self, room1, room2):
door = deepcopy(self._prototype_door)
door.init(room1, room2)
return door
def make_make(self):
return deepcopy(self._prototype_maze)
可以看出其中room绘迁、door與之前不同,增加了額外的初始化卒密,所以原先的類定義也有所修改:
class Room(MapSite):
"""
Room是MapSite的一個具體子類缀台,而MapSite定義了迷宮中構件之間的主要關系。
Room是指向其他MapSite對象的引用哮奇,并保存一個房間號膛腐,用來標識迷宮中的房間
"""
def __init__(self):
super(Room, self).__init__()
self._room_no = None
self._sides = {}
for direction in DIRECTIONS:
self._sides[direction] = None
def init(self, room_no):
self._room_no = room_no
def get_side(self, direction):
return self._sides[direction]
def set_side(self, direction, map_site):
if direction not in ['East', 'West', 'North', 'South']:
print("Dirction is invalid!")
self._sides[direction] = map_site
def get_no(self):
return self._room_no
class Door(MapSite):
"""
這個類描述的是門這個對象
"""
def __init__(self):
super(Door, self).__init__()
self._room1 = None
self._room2 = None
self._is_open = 0
def init(self, room1, room2):
self._room1 = room1
self._room2 = room2
def other_side_from(self, room):
"""
獲取一個房間另一面的房間
:param room:
:return: 輸入room另一面的room
"""
if room.get_no() == self._room1.get_no():
return self._room2
elif room.get_no() == self._room2.get_no():
return self._room1
else:
print("Room is wrong!")
return None
下面我們就用原型工程類,來建立一個迷宮:
game = MazeGame()
# 為工廠添加原型
factory = MazePrototypeFactory(Room(), Wall(), Door(), Maze())
maze = game.create_maze(factory)
0X5 SINGLETON(單例模式)
1.意圖
保證一個類僅有一個實例鼎俘,并提供一個訪問它的全局訪問點哲身。
2.適用性:
- 當類只能有一個實例而且客戶可以從一個眾所周知的訪問點訪問它
- 當這個唯一實例應該是通過子類化擴展的,并且客戶應該無需更改代碼就能使用一個擴展的實例時贸伐。
3.效果
- 對唯一實例的受控訪問
- 縮小名空間
4.代碼示例
這里我們不再使用迷宮作為討論對象勘天,在《python cookbook》中提供了非常易用的Singleton類, 只要繼承它, 就會成為單例捉邢。
# python 3 代碼實現(xiàn)
class Singleton(type):
def __init__(self, *args, **kwargs):
self.__instance = None
super().__init__(*args, **kwargs)
def __call__(self, *args, **kwargs):
if self.__instance is None:
# 如果 __instance 不存在脯丝,創(chuàng)建新的實例
self.__instance = super().__call__(*args, **kwargs)
return self.__instance
else:
# 如果存在,直接返回
return self.__instance
class Spam(metaclass=Singleton):
def __init__(self):
print('Creating Spam')
a = Spam()
b = Spam()
print(a is b) # 這里輸出為 True
這里通過元類(metaclass)實現(xiàn)了三件事:
- 攔截類的創(chuàng)建
- 修改類的定義
- 返回修改后的類
這個Singleton的元類使用了__call__方法使其能夠模擬函數(shù)的行為伏伐, 例子中我們構造了一個Singleton元類宠进,并使用call方法使其能夠模擬函數(shù)的行為。構造類 Spam 時藐翎,將其元類設為Singleton材蹬,那么創(chuàng)建類對象 Spam 時,會走到Singleton的__call__方法中吝镣。
Singleton元類僅僅會初始化一次堤器,這樣繼承它的類,就具有了單例模式赤惊。
單例模式還有其他一些實現(xiàn)方法,我們不在這里一一贅述凰锡。